@friggframework/core 2.0.0-next.8 → 2.0.0-next.80
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/CLAUDE.md +694 -0
- package/README.md +959 -50
- package/application/commands/README.md +451 -0
- package/application/commands/credential-commands.js +245 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +210 -0
- package/application/commands/scheduler-commands.js +263 -0
- package/application/commands/user-commands.js +283 -0
- package/application/index.js +73 -0
- package/assertions/index.js +0 -3
- package/core/CLAUDE.md +690 -0
- package/core/Worker.js +60 -24
- package/core/create-handler.js +79 -8
- package/credential/repositories/credential-repository-documentdb.js +304 -0
- package/credential/repositories/credential-repository-factory.js +54 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +269 -0
- package/credential/repositories/credential-repository-postgres.js +287 -0
- package/credential/repositories/credential-repository.js +300 -0
- package/credential/use-cases/get-credential-for-user.js +25 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +154 -0
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +136 -0
- package/database/encryption/README.md +839 -0
- package/database/encryption/documentdb-encryption-service.md +3575 -0
- package/database/encryption/encryption-schema-registry.js +268 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/index.js +21 -21
- package/database/prisma.js +182 -0
- package/database/repositories/health-check-repository-documentdb.js +138 -0
- package/database/repositories/health-check-repository-factory.js +48 -0
- package/database/repositories/health-check-repository-interface.js +82 -0
- package/database/repositories/health-check-repository-mongodb.js +89 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-health-use-case.js +29 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +83 -0
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/run-database-migration-use-case.js +139 -0
- package/database/use-cases/test-encryption-use-case.js +253 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +94 -0
- package/database/utils/mongodb-schema-init.js +108 -0
- package/database/utils/prisma-runner.js +477 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/encrypt/Cryptor.js +34 -168
- package/encrypt/index.js +1 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +15 -7
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +335 -0
- package/generated/prisma-mongodb/index-browser.js +317 -0
- package/generated/prisma-mongodb/index.d.ts +22955 -0
- package/generated/prisma-mongodb/index.js +360 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +362 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +342 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +357 -0
- package/generated/prisma-postgresql/index-browser.js +339 -0
- package/generated/prisma-postgresql/index.d.ts +25131 -0
- package/generated/prisma-postgresql/index.js +382 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +345 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +364 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +57 -0
- package/handlers/backend-utils.js +262 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +15 -0
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +326 -0
- package/handlers/routers/health.js +516 -0
- package/handlers/routers/integration-defined-routers.js +45 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/user.js +63 -0
- package/handlers/routers/websocket.js +57 -0
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +352 -0
- package/handlers/workers/dlq-processor.js +63 -0
- package/handlers/workers/integration-defined-workers.js +23 -0
- package/index.js +82 -46
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
- package/infrastructure/scheduler/index.js +33 -0
- package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
- package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +364 -55
- package/integrations/integration-router.js +375 -179
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-documentdb.js +219 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +144 -0
- package/integrations/repositories/integration-repository-mongo.js +330 -0
- package/integrations/repositories/integration-repository-postgres.js +385 -0
- package/integrations/repositories/process-repository-documentdb.js +243 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +105 -0
- package/integrations/tests/doubles/test-integration-repository.js +112 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/delete-integration-for-user.js +101 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +88 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/utils/map-integration-dto.js +37 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/logs/logger.js +0 -4
- package/{module-plugin → modules}/index.js +0 -10
- package/modules/module-factory.js +56 -0
- package/modules/module.js +256 -0
- package/modules/repositories/module-repository-documentdb.js +335 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +408 -0
- package/modules/repositories/module-repository-postgres.js +453 -0
- package/modules/repositories/module-repository.js +345 -0
- package/modules/requester/api-key.js +52 -0
- package/modules/requester/oauth-2.js +396 -0
- package/{module-plugin → modules}/requester/requester.js +4 -2
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +71 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +177 -0
- package/modules/use-cases/refresh-entity-options.js +72 -0
- package/modules/use-cases/test-module-auth.js +72 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +82 -50
- package/prisma-mongodb/schema.prisma +362 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +345 -0
- package/queues/queuer-util.js +103 -21
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +43 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +40 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +219 -0
- package/token/repositories/token-repository-postgres.js +264 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/associations/index.d.ts +0 -17
- package/types/core/index.d.ts +12 -4
- package/types/database/index.d.ts +10 -2
- package/types/encrypt/index.d.ts +5 -3
- package/types/integrations/index.d.ts +3 -8
- package/types/module-plugin/index.d.ts +17 -69
- package/types/syncs/index.d.ts +0 -17
- package/user/repositories/user-repository-documentdb.js +441 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +201 -0
- package/user/repositories/user-repository-mongo.js +308 -0
- package/user/repositories/user-repository-postgres.js +360 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +125 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
- package/websocket/repositories/websocket-connection-repository.js +161 -0
- package/assertions/is-equal.js +0 -17
- package/associations/model.js +0 -54
- package/database/models/IndividualUser.js +0 -76
- package/database/models/OrganizationUser.js +0 -29
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/models/UserModel.js +0 -7
- package/database/models/WebsocketConnection.js +0 -49
- package/database/mongo.js +0 -45
- package/database/mongoose.js +0 -5
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/encrypt/test-encrypt.js +0 -107
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/entity.js +0 -46
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/api-key.js +0 -36
- package/module-plugin/requester/oauth-2.js +0 -219
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- package/syncs/model.js +0 -62
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Schema Parser for MongoDB Collections
|
|
3
|
+
*
|
|
4
|
+
* Dynamically parses the Prisma schema file to extract MongoDB collection names.
|
|
5
|
+
* This ensures collection names stay in sync with the schema without hardcoding.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - @@map() directives (custom collection names)
|
|
9
|
+
* - Models without @@map() (uses model name)
|
|
10
|
+
* - Comments and whitespace
|
|
11
|
+
* - Multiple schema file locations
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse Prisma schema file to extract collection names
|
|
19
|
+
*
|
|
20
|
+
* Reads the schema.prisma file and extracts all model definitions,
|
|
21
|
+
* returning the actual MongoDB collection names (from @@map directives).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} schemaPath - Path to schema.prisma file
|
|
24
|
+
* @returns {Promise<string[]>} Array of collection names
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```js
|
|
28
|
+
* const collections = await parseCollectionsFromSchema('./prisma/schema.prisma');
|
|
29
|
+
* // Returns: ['User', 'Token', 'Credential', ...]
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
async function parseCollectionsFromSchema(schemaPath) {
|
|
33
|
+
try {
|
|
34
|
+
const schemaContent = await fs.promises.readFile(schemaPath, 'utf-8');
|
|
35
|
+
return extractCollectionNames(schemaContent);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Failed to parse Prisma schema at ${schemaPath}: ${error.message}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Synchronous version of parseCollectionsFromSchema
|
|
45
|
+
*
|
|
46
|
+
* @param {string} schemaPath - Path to schema.prisma file
|
|
47
|
+
* @returns {string[]} Array of collection names
|
|
48
|
+
*/
|
|
49
|
+
function parseCollectionsFromSchemaSync(schemaPath) {
|
|
50
|
+
try {
|
|
51
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
52
|
+
return extractCollectionNames(schemaContent);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to parse Prisma schema at ${schemaPath}: ${error.message}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract collection names from Prisma schema content
|
|
62
|
+
*
|
|
63
|
+
* Parses the schema content to find:
|
|
64
|
+
* 1. All model definitions
|
|
65
|
+
* 2. Their @@map() directives (if present)
|
|
66
|
+
* 3. Falls back to model name if no @@map()
|
|
67
|
+
*
|
|
68
|
+
* @param {string} schemaContent - Content of schema.prisma file
|
|
69
|
+
* @returns {string[]} Array of collection names
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
function extractCollectionNames(schemaContent) {
|
|
73
|
+
const collections = [];
|
|
74
|
+
|
|
75
|
+
// Match model blocks: "model ModelName { ... }"
|
|
76
|
+
// Using non-greedy match to handle multiple models
|
|
77
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
78
|
+
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = modelRegex.exec(schemaContent)) !== null) {
|
|
81
|
+
const modelName = match[1];
|
|
82
|
+
const modelBody = match[2];
|
|
83
|
+
|
|
84
|
+
// Look for @@map("CollectionName") directive
|
|
85
|
+
const mapMatch = modelBody.match(/@@map\s*\(\s*["'](\w+)["']\s*\)/);
|
|
86
|
+
|
|
87
|
+
if (mapMatch) {
|
|
88
|
+
// Use mapped collection name
|
|
89
|
+
collections.push(mapMatch[1]);
|
|
90
|
+
} else {
|
|
91
|
+
// Use model name as collection name (Prisma default)
|
|
92
|
+
collections.push(modelName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return collections;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find Prisma MongoDB schema file
|
|
101
|
+
*
|
|
102
|
+
* Searches for the schema.prisma file in common locations:
|
|
103
|
+
* 1. prisma-mongodb/schema.prisma (Frigg convention)
|
|
104
|
+
* 2. prisma/schema.prisma (Prisma default)
|
|
105
|
+
* 3. schema.prisma (root)
|
|
106
|
+
*
|
|
107
|
+
* @param {string} startDir - Directory to start searching from
|
|
108
|
+
* @returns {string|null} Path to schema file, or null if not found
|
|
109
|
+
*/
|
|
110
|
+
function findMongoDBSchemaFile(startDir = __dirname) {
|
|
111
|
+
// Start from database directory and work up
|
|
112
|
+
const baseDir = path.resolve(startDir, '../..');
|
|
113
|
+
|
|
114
|
+
const searchPaths = [
|
|
115
|
+
path.join(baseDir, 'prisma-mongodb', 'schema.prisma'),
|
|
116
|
+
path.join(baseDir, 'prisma', 'schema.prisma'),
|
|
117
|
+
path.join(baseDir, 'schema.prisma'),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const schemaPath of searchPaths) {
|
|
121
|
+
if (fs.existsSync(schemaPath)) {
|
|
122
|
+
return schemaPath;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get MongoDB collection names from Prisma schema
|
|
131
|
+
*
|
|
132
|
+
* Convenience function that finds and parses the schema automatically.
|
|
133
|
+
*
|
|
134
|
+
* @returns {Promise<string[]>} Array of collection names
|
|
135
|
+
* @throws {Error} If schema file not found or parsing fails
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```js
|
|
139
|
+
* const collections = await getCollectionsFromSchema();
|
|
140
|
+
* await ensureCollectionsExist(collections);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
async function getCollectionsFromSchema() {
|
|
144
|
+
const schemaPath = findMongoDBSchemaFile();
|
|
145
|
+
|
|
146
|
+
if (!schemaPath) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'Could not find Prisma MongoDB schema file. ' +
|
|
149
|
+
'Searched: prisma-mongodb/schema.prisma, prisma/schema.prisma, schema.prisma'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return await parseCollectionsFromSchema(schemaPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Synchronous version of getCollectionsFromSchema
|
|
158
|
+
*
|
|
159
|
+
* @returns {string[]} Array of collection names
|
|
160
|
+
* @throws {Error} If schema file not found or parsing fails
|
|
161
|
+
*/
|
|
162
|
+
function getCollectionsFromSchemaSync() {
|
|
163
|
+
const schemaPath = findMongoDBSchemaFile();
|
|
164
|
+
|
|
165
|
+
if (!schemaPath) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'Could not find Prisma MongoDB schema file. ' +
|
|
168
|
+
'Searched: prisma-mongodb/schema.prisma, prisma/schema.prisma, schema.prisma'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return parseCollectionsFromSchemaSync(schemaPath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
parseCollectionsFromSchema,
|
|
177
|
+
parseCollectionsFromSchemaSync,
|
|
178
|
+
extractCollectionNames,
|
|
179
|
+
findMongoDBSchemaFile,
|
|
180
|
+
getCollectionsFromSchema,
|
|
181
|
+
getCollectionsFromSchemaSync,
|
|
182
|
+
};
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# Process Management FIFO Queue Specification
|
|
2
|
+
|
|
3
|
+
## Problem Statement
|
|
4
|
+
|
|
5
|
+
The current BaseCRMIntegration implementation has a **race condition** in process record updates:
|
|
6
|
+
|
|
7
|
+
1. Multiple queue workers process batches concurrently
|
|
8
|
+
2. Each worker calls `processManager.updateMetrics()`
|
|
9
|
+
3. Multiple workers read-modify-write the same process record simultaneously
|
|
10
|
+
4. **Result**: Lost updates, inconsistent metrics, potential data corruption
|
|
11
|
+
|
|
12
|
+
## Current Race Condition Example
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Time 1: Worker A reads process.results.aggregateData.totalSynced = 100
|
|
16
|
+
Time 2: Worker B reads process.results.aggregateData.totalSynced = 100
|
|
17
|
+
Time 3: Worker A adds 50 → writes totalSynced = 150
|
|
18
|
+
Time 4: Worker B adds 30 → writes totalSynced = 130 (overwrites Worker A's update!)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Solution: FIFO Queue for Process Updates
|
|
22
|
+
|
|
23
|
+
### Design Overview
|
|
24
|
+
|
|
25
|
+
Create a dedicated FIFO SQS queue in **Frigg Core** for all process management operations:
|
|
26
|
+
|
|
27
|
+
- **Queue Type**: FIFO (First-In-First-Out)
|
|
28
|
+
- **Message Group ID**: `process-{processId}` (ensures ordered processing per process)
|
|
29
|
+
- **Message Deduplication**: Enabled (prevents duplicate updates)
|
|
30
|
+
- **Dead Letter Queue**: Enabled (captures failed updates)
|
|
31
|
+
|
|
32
|
+
### Architecture
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ Current Flow (Race Condition) │
|
|
37
|
+
├─────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ Worker A ──┐ │
|
|
39
|
+
│ Worker B ──┼──→ ProcessManager.updateMetrics() │
|
|
40
|
+
│ Worker C ──┘ │
|
|
41
|
+
│ └──→ ProcessRepository.update() │
|
|
42
|
+
│ (Race condition!) │
|
|
43
|
+
└─────────────────────────────────────────────────────────────┘
|
|
44
|
+
|
|
45
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ Proposed Flow (FIFO Queue) │
|
|
47
|
+
├─────────────────────────────────────────────────────────────┤
|
|
48
|
+
│ Worker A ──┐ │
|
|
49
|
+
│ Worker B ──┼──→ QueueManager.queueProcessUpdate() │
|
|
50
|
+
│ Worker C ──┘ │
|
|
51
|
+
│ └──→ ProcessManagementFIFOQueue │
|
|
52
|
+
│ └──→ ProcessUpdateHandler │
|
|
53
|
+
│ └──→ ProcessRepository.update()│
|
|
54
|
+
│ (Ordered, no races!) │
|
|
55
|
+
└─────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Frigg Core Implementation
|
|
59
|
+
|
|
60
|
+
### 1. Process Management Queue Factory
|
|
61
|
+
|
|
62
|
+
**File**: `/packages/core/integrations/queues/process-management-queue-factory.js`
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const { SQS } = require('aws-sdk');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates FIFO queue for process management operations
|
|
69
|
+
* Ensures ordered processing per process ID
|
|
70
|
+
*/
|
|
71
|
+
class ProcessManagementQueueFactory {
|
|
72
|
+
constructor({ region = 'us-east-1' } = {}) {
|
|
73
|
+
this.sqs = new SQS({ region });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create FIFO queue for process updates
|
|
78
|
+
* @param {string} integrationName - Integration name (for queue naming)
|
|
79
|
+
* @returns {Promise<string>} Queue URL
|
|
80
|
+
*/
|
|
81
|
+
async createProcessManagementQueue(integrationName) {
|
|
82
|
+
const queueName = `${integrationName}-process-management.fifo`;
|
|
83
|
+
|
|
84
|
+
const params = {
|
|
85
|
+
QueueName: queueName,
|
|
86
|
+
Attributes: {
|
|
87
|
+
FifoQueue: 'true',
|
|
88
|
+
ContentBasedDeduplication: 'true',
|
|
89
|
+
MessageRetentionPeriod: '1209600', // 14 days
|
|
90
|
+
VisibilityTimeoutSeconds: '30',
|
|
91
|
+
DelaySeconds: '0',
|
|
92
|
+
ReceiveMessageWaitTimeSeconds: '20', // Long polling
|
|
93
|
+
DeadLetterTargetArn: `${queueName}-dlq.fifo`, // DLQ
|
|
94
|
+
MaxReceiveCount: '3', // Retry failed messages 3 times
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await this.sqs.createQueue(params).promise();
|
|
99
|
+
return result.QueueUrl;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send process update message to FIFO queue
|
|
104
|
+
* @param {string} queueUrl - FIFO queue URL
|
|
105
|
+
* @param {string} processId - Process ID (used as MessageGroupId)
|
|
106
|
+
* @param {string} operation - Operation type (UPDATE_STATE, UPDATE_METRICS, COMPLETE)
|
|
107
|
+
* @param {Object} data - Operation data
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async sendProcessUpdate(queueUrl, processId, operation, data) {
|
|
111
|
+
const params = {
|
|
112
|
+
QueueUrl: queueUrl,
|
|
113
|
+
MessageBody: JSON.stringify({
|
|
114
|
+
processId,
|
|
115
|
+
operation,
|
|
116
|
+
data,
|
|
117
|
+
timestamp: new Date().toISOString()
|
|
118
|
+
}),
|
|
119
|
+
MessageGroupId: `process-${processId}`,
|
|
120
|
+
MessageDeduplicationId: `${processId}-${operation}-${Date.now()}`,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await this.sqs.sendMessage(params).promise();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { ProcessManagementQueueFactory };
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2. Process Update Handler
|
|
131
|
+
|
|
132
|
+
**File**: `/packages/core/integrations/handlers/process-update-handler.js`
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const {
|
|
136
|
+
UpdateProcessState,
|
|
137
|
+
UpdateProcessMetrics,
|
|
138
|
+
GetProcess,
|
|
139
|
+
} = require('../use-cases');
|
|
140
|
+
const { createProcessRepository } = require('../repositories/process-repository-factory');
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handler for process management FIFO queue messages
|
|
144
|
+
* Processes updates in order per process ID
|
|
145
|
+
*/
|
|
146
|
+
class ProcessUpdateHandler {
|
|
147
|
+
constructor() {
|
|
148
|
+
const processRepository = createProcessRepository();
|
|
149
|
+
this.updateProcessStateUseCase = new UpdateProcessState({ processRepository });
|
|
150
|
+
this.updateProcessMetricsUseCase = new UpdateProcessMetrics({ processRepository });
|
|
151
|
+
this.getProcessUseCase = new GetProcess({ processRepository });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle process update message from FIFO queue
|
|
156
|
+
* @param {Object} message - SQS message
|
|
157
|
+
* @param {Object} message.body - Message body (JSON string)
|
|
158
|
+
* @returns {Promise<void>}
|
|
159
|
+
*/
|
|
160
|
+
async handle(message) {
|
|
161
|
+
try {
|
|
162
|
+
const { processId, operation, data } = JSON.parse(message.body);
|
|
163
|
+
|
|
164
|
+
switch (operation) {
|
|
165
|
+
case 'UPDATE_STATE':
|
|
166
|
+
await this.updateProcessStateUseCase.execute(
|
|
167
|
+
processId,
|
|
168
|
+
data.state,
|
|
169
|
+
data.contextUpdates
|
|
170
|
+
);
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'UPDATE_METRICS':
|
|
174
|
+
await this.updateProcessMetricsUseCase.execute(
|
|
175
|
+
processId,
|
|
176
|
+
data.metricsUpdate
|
|
177
|
+
);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case 'COMPLETE_PROCESS':
|
|
181
|
+
await this.updateProcessStateUseCase.execute(
|
|
182
|
+
processId,
|
|
183
|
+
'COMPLETED',
|
|
184
|
+
{ endTime: new Date().toISOString() }
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'HANDLE_ERROR':
|
|
189
|
+
await this.updateProcessStateUseCase.execute(
|
|
190
|
+
processId,
|
|
191
|
+
'ERROR',
|
|
192
|
+
{
|
|
193
|
+
error: data.error.message,
|
|
194
|
+
errorStack: data.error.stack,
|
|
195
|
+
errorTimestamp: new Date().toISOString()
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
throw new Error(`Unknown process operation: ${operation}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`Process update completed: ${operation} for process ${processId}`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Process update failed:', error);
|
|
207
|
+
throw error; // Will trigger SQS retry/DLQ
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { ProcessUpdateHandler };
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 3. QueueManager Enhancement
|
|
216
|
+
|
|
217
|
+
**File**: `/packages/core/integrations/queues/process-queue-manager.js`
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
const { ProcessManagementQueueFactory } = require('./process-management-queue-factory');
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Manages process update operations via FIFO queue
|
|
224
|
+
* Prevents race conditions in concurrent process updates
|
|
225
|
+
*/
|
|
226
|
+
class ProcessQueueManager {
|
|
227
|
+
constructor({ region = 'us-east-1' } = {}) {
|
|
228
|
+
this.factory = new ProcessManagementQueueFactory({ region });
|
|
229
|
+
this.queueUrls = new Map(); // Cache queue URLs per integration
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get or create FIFO queue for integration
|
|
234
|
+
* @param {string} integrationName - Integration name
|
|
235
|
+
* @returns {Promise<string>} Queue URL
|
|
236
|
+
*/
|
|
237
|
+
async getProcessQueueUrl(integrationName) {
|
|
238
|
+
if (!this.queueUrls.has(integrationName)) {
|
|
239
|
+
const queueUrl = await this.factory.createProcessManagementQueue(integrationName);
|
|
240
|
+
this.queueUrls.set(integrationName, queueUrl);
|
|
241
|
+
}
|
|
242
|
+
return this.queueUrls.get(integrationName);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Queue process state update
|
|
247
|
+
* @param {string} integrationName - Integration name
|
|
248
|
+
* @param {string} processId - Process ID
|
|
249
|
+
* @param {string} state - New state
|
|
250
|
+
* @param {Object} contextUpdates - Context updates
|
|
251
|
+
* @returns {Promise<void>}
|
|
252
|
+
*/
|
|
253
|
+
async queueStateUpdate(integrationName, processId, state, contextUpdates = {}) {
|
|
254
|
+
const queueUrl = await this.getProcessQueueUrl(integrationName);
|
|
255
|
+
await this.factory.sendProcessUpdate(queueUrl, processId, 'UPDATE_STATE', {
|
|
256
|
+
state,
|
|
257
|
+
contextUpdates
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Queue process metrics update
|
|
263
|
+
* @param {string} integrationName - Integration name
|
|
264
|
+
* @param {string} processId - Process ID
|
|
265
|
+
* @param {Object} metricsUpdate - Metrics to add
|
|
266
|
+
* @returns {Promise<void>}
|
|
267
|
+
*/
|
|
268
|
+
async queueMetricsUpdate(integrationName, processId, metricsUpdate) {
|
|
269
|
+
const queueUrl = await this.getProcessQueueUrl(integrationName);
|
|
270
|
+
await this.factory.sendProcessUpdate(queueUrl, processId, 'UPDATE_METRICS', {
|
|
271
|
+
metricsUpdate
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Queue process completion
|
|
277
|
+
* @param {string} integrationName - Integration name
|
|
278
|
+
* @param {string} processId - Process ID
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
async queueProcessCompletion(integrationName, processId) {
|
|
282
|
+
const queueUrl = await this.getProcessQueueUrl(integrationName);
|
|
283
|
+
await this.factory.sendProcessUpdate(queueUrl, processId, 'COMPLETE_PROCESS', {});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Queue process error handling
|
|
288
|
+
* @param {string} integrationName - Integration name
|
|
289
|
+
* @param {string} processId - Process ID
|
|
290
|
+
* @param {Error} error - Error object
|
|
291
|
+
* @returns {Promise<void>}
|
|
292
|
+
*/
|
|
293
|
+
async queueErrorHandling(integrationName, processId, error) {
|
|
294
|
+
const queueUrl = await this.getProcessQueueUrl(integrationName);
|
|
295
|
+
await this.factory.sendProcessUpdate(queueUrl, processId, 'HANDLE_ERROR', {
|
|
296
|
+
error: {
|
|
297
|
+
message: error.message,
|
|
298
|
+
stack: error.stack
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = { ProcessQueueManager };
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Integration with BaseCRMIntegration
|
|
308
|
+
|
|
309
|
+
### Updated ProcessManager
|
|
310
|
+
|
|
311
|
+
**File**: `/Users/sean/Documents/GitHub/quo--frigg/backend/src/base/services/ProcessManager.js`
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
const { ProcessQueueManager } = require('@friggframework/core/integrations/queues/process-queue-manager');
|
|
315
|
+
|
|
316
|
+
class ProcessManager {
|
|
317
|
+
constructor({
|
|
318
|
+
createProcessUseCase,
|
|
319
|
+
updateProcessStateUseCase,
|
|
320
|
+
updateProcessMetricsUseCase,
|
|
321
|
+
getProcessUseCase,
|
|
322
|
+
integrationName, // NEW: For FIFO queue
|
|
323
|
+
}) {
|
|
324
|
+
// ... existing constructor ...
|
|
325
|
+
this.processQueueManager = new ProcessQueueManager();
|
|
326
|
+
this.integrationName = integrationName;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Update process state via FIFO queue (prevents race conditions)
|
|
331
|
+
* @param {string} processId - Process ID to update
|
|
332
|
+
* @param {string} state - New state
|
|
333
|
+
* @param {Object} contextUpdates - Context updates
|
|
334
|
+
* @returns {Promise<void>} (async, no return value)
|
|
335
|
+
*/
|
|
336
|
+
async updateState(processId, state, contextUpdates = {}) {
|
|
337
|
+
await this.processQueueManager.queueStateUpdate(
|
|
338
|
+
this.integrationName,
|
|
339
|
+
processId,
|
|
340
|
+
state,
|
|
341
|
+
contextUpdates
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Update process metrics via FIFO queue (prevents race conditions)
|
|
347
|
+
* @param {string} processId - Process ID to update
|
|
348
|
+
* @param {Object} metricsUpdate - Metrics to add
|
|
349
|
+
* @returns {Promise<void>} (async, no return value)
|
|
350
|
+
*/
|
|
351
|
+
async updateMetrics(processId, metricsUpdate) {
|
|
352
|
+
await this.processQueueManager.queueMetricsUpdate(
|
|
353
|
+
this.integrationName,
|
|
354
|
+
processId,
|
|
355
|
+
metricsUpdate
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Complete process via FIFO queue
|
|
361
|
+
* @param {string} processId - Process ID to complete
|
|
362
|
+
* @returns {Promise<void>} (async, no return value)
|
|
363
|
+
*/
|
|
364
|
+
async completeProcess(processId) {
|
|
365
|
+
await this.processQueueManager.queueProcessCompletion(
|
|
366
|
+
this.integrationName,
|
|
367
|
+
processId
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Handle process error via FIFO queue
|
|
373
|
+
* @param {string} processId - Process ID to update
|
|
374
|
+
* @param {Error} error - Error object
|
|
375
|
+
* @returns {Promise<void>} (async, no return value)
|
|
376
|
+
*/
|
|
377
|
+
async handleError(processId, error) {
|
|
378
|
+
await this.processQueueManager.queueErrorHandling(
|
|
379
|
+
this.integrationName,
|
|
380
|
+
processId,
|
|
381
|
+
error
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Serverless Infrastructure
|
|
388
|
+
|
|
389
|
+
### FIFO Queue Creation
|
|
390
|
+
|
|
391
|
+
**File**: `/packages/devtools/infrastructure/serverless-template.js`
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
const attachProcessManagementQueues = (definition, AppDefinition) => {
|
|
395
|
+
for (const integration of AppDefinition.integrations) {
|
|
396
|
+
const integrationName = integration.Definition.name;
|
|
397
|
+
|
|
398
|
+
// Create FIFO queue for process management
|
|
399
|
+
const processQueueName = `${integrationName}ProcessManagementQueue`;
|
|
400
|
+
const processDLQName = `${integrationName}ProcessManagementDLQ`;
|
|
401
|
+
|
|
402
|
+
// FIFO Queue
|
|
403
|
+
definition.resources.Resources[processQueueName] = {
|
|
404
|
+
Type: 'AWS::SQS::Queue',
|
|
405
|
+
Properties: {
|
|
406
|
+
QueueName: `${integrationName}-process-management.fifo`,
|
|
407
|
+
FifoQueue: true,
|
|
408
|
+
ContentBasedDeduplication: true,
|
|
409
|
+
MessageRetentionPeriod: 1209600, // 14 days
|
|
410
|
+
VisibilityTimeoutSeconds: 30,
|
|
411
|
+
DelaySeconds: 0,
|
|
412
|
+
ReceiveMessageWaitTimeSeconds: 20, // Long polling
|
|
413
|
+
RedrivePolicy: {
|
|
414
|
+
deadLetterTargetArn: { 'Fn::GetAtt': [processDLQName, 'Arn'] },
|
|
415
|
+
maxReceiveCount: 3,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Dead Letter Queue
|
|
421
|
+
definition.resources.Resources[processDLQName] = {
|
|
422
|
+
Type: 'AWS::SQS::Queue',
|
|
423
|
+
Properties: {
|
|
424
|
+
QueueName: `${integrationName}-process-management-dlq.fifo`,
|
|
425
|
+
FifoQueue: true,
|
|
426
|
+
MessageRetentionPeriod: 1209600,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Process Update Handler Function
|
|
431
|
+
const processHandlerName = `${integrationName}ProcessUpdateHandler`;
|
|
432
|
+
definition.functions[processHandlerName] = {
|
|
433
|
+
handler: 'node_modules/@friggframework/core/handlers/process-update-handler.handler',
|
|
434
|
+
reservedConcurrency: 1, // Process updates sequentially per integration
|
|
435
|
+
events: [{
|
|
436
|
+
sqs: {
|
|
437
|
+
arn: { 'Fn::GetAtt': [processQueueName, 'Arn'] },
|
|
438
|
+
batchSize: 1, // Process one update at a time
|
|
439
|
+
maximumBatchingWindowInSeconds: 5,
|
|
440
|
+
},
|
|
441
|
+
}],
|
|
442
|
+
timeout: 30,
|
|
443
|
+
environment: {
|
|
444
|
+
INTEGRATION_NAME: integrationName,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Benefits
|
|
452
|
+
|
|
453
|
+
### ✅ Race Condition Prevention
|
|
454
|
+
- FIFO queue ensures ordered processing per process ID
|
|
455
|
+
- MessageGroupId = `process-{processId}` guarantees sequential updates
|
|
456
|
+
- No more lost updates or inconsistent metrics
|
|
457
|
+
|
|
458
|
+
### ✅ Cost Optimization
|
|
459
|
+
- Only one FIFO queue per integration (not per process)
|
|
460
|
+
- MessageGroupId provides ordering without expensive per-process queues
|
|
461
|
+
- Long polling reduces API calls
|
|
462
|
+
|
|
463
|
+
### ✅ Reliability
|
|
464
|
+
- Dead Letter Queue captures failed updates
|
|
465
|
+
- Retry mechanism with exponential backoff
|
|
466
|
+
- Content-based deduplication prevents duplicate processing
|
|
467
|
+
|
|
468
|
+
### ✅ Scalability
|
|
469
|
+
- Each integration has its own process management queue
|
|
470
|
+
- Process updates don't block data processing
|
|
471
|
+
- Can scale process update handlers independently
|
|
472
|
+
|
|
473
|
+
## Migration Strategy
|
|
474
|
+
|
|
475
|
+
### Phase 1: Current Implementation (Native Queue)
|
|
476
|
+
- Use existing integration queue for process updates
|
|
477
|
+
- Accept potential race conditions for now
|
|
478
|
+
- Focus on core functionality
|
|
479
|
+
|
|
480
|
+
### Phase 2: FIFO Queue Implementation
|
|
481
|
+
- Implement FIFO queue infrastructure in Frigg Core
|
|
482
|
+
- Update ProcessManager to use FIFO queue
|
|
483
|
+
- Deploy with feature flag
|
|
484
|
+
|
|
485
|
+
### Phase 3: Full Migration
|
|
486
|
+
- Switch all integrations to FIFO queue
|
|
487
|
+
- Remove native queue process update code
|
|
488
|
+
- Monitor for race condition elimination
|
|
489
|
+
|
|
490
|
+
## Cost Analysis
|
|
491
|
+
|
|
492
|
+
### FIFO Queue Costs (per integration)
|
|
493
|
+
- **Queue Creation**: Free
|
|
494
|
+
- **Message Storage**: $0.40 per million messages
|
|
495
|
+
- **Message Processing**: $0.40 per million requests
|
|
496
|
+
- **Example**: 10 integrations, 1000 process updates/day = ~$2.40/month
|
|
497
|
+
|
|
498
|
+
### Benefits vs Costs
|
|
499
|
+
- **Cost**: ~$2.40/month for 10 integrations
|
|
500
|
+
- **Benefit**: Eliminates race conditions, ensures data consistency
|
|
501
|
+
- **ROI**: High - prevents data corruption and debugging time
|
|
502
|
+
|
|
503
|
+
## Implementation Priority
|
|
504
|
+
|
|
505
|
+
**High Priority** - Race conditions in process updates can cause:
|
|
506
|
+
- Lost sync progress
|
|
507
|
+
- Inconsistent metrics
|
|
508
|
+
- Difficult debugging
|
|
509
|
+
- Data integrity issues
|
|
510
|
+
|
|
511
|
+
**Recommended Timeline**:
|
|
512
|
+
1. **Week 1**: Implement FIFO queue infrastructure in Frigg Core
|
|
513
|
+
2. **Week 2**: Update ProcessManager to use FIFO queue
|
|
514
|
+
3. **Week 3**: Deploy and test with one integration
|
|
515
|
+
4. **Week 4**: Roll out to all integrations
|
|
516
|
+
|
|
517
|
+
This solution provides a robust, scalable approach to process management while maintaining the performance benefits of concurrent data processing.
|