@friggframework/core 2.0.0-next.10 → 2.0.0-next.101
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 +702 -0
- package/README.md +959 -50
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-script-execution-repository-interface.js +166 -0
- package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +228 -0
- package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +268 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/README.md +451 -0
- package/application/commands/admin-script-commands.js +358 -0
- package/application/commands/credential-commands.js +245 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +343 -0
- package/application/commands/process-commands.js +135 -0
- package/application/commands/scheduler-commands.js +263 -0
- package/application/commands/user-commands.js +312 -0
- package/application/index.js +82 -0
- package/assertions/index.js +0 -3
- package/core/CLAUDE.md +690 -0
- package/core/Worker.js +60 -24
- package/core/create-handler.js +84 -6
- 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 +401 -0
- package/database/encryption/field-encryption-service.js +254 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +230 -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 +5 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +5 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +374 -0
- package/generated/prisma-mongodb/index-browser.js +356 -0
- package/generated/prisma-mongodb/index.d.ts +25667 -0
- package/generated/prisma-mongodb/index.js +399 -0
- package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
- package/generated/prisma-mongodb/runtime/edge.js +35 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
- package/generated/prisma-mongodb/runtime/library.js +147 -0
- package/generated/prisma-mongodb/runtime/react-native.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-mongodb/schema.prisma +432 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +381 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +5 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +5 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +395 -0
- package/generated/prisma-postgresql/index-browser.js +377 -0
- package/generated/prisma-postgresql/index.d.ts +28455 -0
- package/generated/prisma-postgresql/index.js +420 -0
- package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/package.json +183 -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/edge-esm.js +35 -0
- package/generated/prisma-postgresql/runtime/edge.js +35 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
- package/generated/prisma-postgresql/runtime/library.js +147 -0
- package/generated/prisma-postgresql/runtime/react-native.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-postgresql/schema.prisma +414 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +402 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +43 -0
- package/handlers/app-handler-helpers.js +3 -3
- package/handlers/backend-utils.js +271 -42
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/middleware/admin-auth.js +73 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +4 -15
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +311 -0
- package/handlers/routers/health.js +518 -0
- package/handlers/routers/integration-defined-routers.js +85 -10
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/reporting.js +9 -0
- package/handlers/routers/user.js +27 -5
- package/handlers/routers/websocket.js +5 -3
- 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 +13 -7
- package/index.js +91 -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/EXTENSIONS.md +240 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/extension.js +254 -0
- package/integrations/index.js +20 -10
- package/integrations/integration-base.js +633 -72
- package/integrations/integration-router.js +398 -180
- package/integrations/options.js +1 -1
- package/integrations/repositories/config-patch-shared.js +43 -0
- 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 +284 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +173 -0
- package/integrations/repositories/integration-repository-mongo.js +414 -0
- package/integrations/repositories/integration-repository-postgres.js +452 -0
- package/integrations/repositories/process-repository-documentdb.js +311 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +136 -0
- package/integrations/repositories/process-repository-mongo.js +262 -0
- package/integrations/repositories/process-repository-postgres.js +380 -0
- package/integrations/repositories/process-update-ops-shared.js +112 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +113 -0
- package/integrations/tests/doubles/test-integration-repository.js +146 -0
- package/integrations/use-cases/create-integration.js +215 -0
- package/integrations/use-cases/create-process.js +130 -0
- package/integrations/use-cases/delete-integration-for-user.js +119 -0
- package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -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 +89 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/patch-integration-config.js +39 -0
- package/integrations/use-cases/process-errors.js +28 -0
- package/integrations/use-cases/update-integration-config.js +32 -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 +216 -0
- package/integrations/use-cases/update-process-state.js +163 -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 +287 -0
- package/modules/repositories/module-repository-documentdb.js +350 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +145 -0
- package/modules/repositories/module-repository-mongo.js +436 -0
- package/modules/repositories/module-repository-postgres.js +481 -0
- package/modules/repositories/module-repository.js +369 -0
- package/modules/requester/api-key.js +52 -0
- package/modules/requester/oauth-2.js +396 -0
- package/modules/requester/requester.js +334 -0
- 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 +34 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +243 -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 +432 -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/20260422120000_add_entity_data_column/migration.sql +10 -0
- package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
- package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
- package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
- package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
- package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +414 -0
- package/queues/queuer-util.js +103 -21
- package/reporting/README.md +86 -0
- package/reporting/index.js +17 -0
- package/reporting/reporting-router.js +77 -0
- package/reporting/repositories/reporting-repository-documentdb.js +127 -0
- package/reporting/repositories/reporting-repository-factory.js +35 -0
- package/reporting/repositories/reporting-repository-interface.js +16 -0
- package/reporting/repositories/reporting-repository-mongo.js +54 -0
- package/reporting/repositories/reporting-repository-postgres.js +70 -0
- package/reporting/use-cases/index.js +6 -0
- package/reporting/use-cases/list-integrations-report.js +142 -0
- 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 +458 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +214 -0
- package/user/repositories/user-repository-mongo.js +323 -0
- package/user/repositories/user-repository-postgres.js +377 -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/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- 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.js +0 -165
- 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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for ProcessRepository.applyProcessUpdate() validation.
|
|
3
|
+
*
|
|
4
|
+
* These utilities are backend-agnostic: they enforce invariants on the
|
|
5
|
+
* `ProcessUpdateOps` shape BEFORE each adapter emits any SQL or database
|
|
6
|
+
* command. Keeping validation here means any bug we fix (e.g. tighter
|
|
7
|
+
* path regex, size cap) fixes all three adapters in one place.
|
|
8
|
+
*
|
|
9
|
+
* Imported by the Postgres, MongoDB, and DocumentDB adapters.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Allowed dot-path shape. Root must be `context` or `results`, and each
|
|
14
|
+
* segment after the first must be a JS-identifier-style token. Numeric
|
|
15
|
+
* segments (array indices) and bracket syntax are intentionally
|
|
16
|
+
* disallowed — array element mutation is exclusively handled via
|
|
17
|
+
* `pushSlice`, which targets a whole array at a path.
|
|
18
|
+
*/
|
|
19
|
+
const PATH_REGEX = /^(context|results)(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalizes and validates a `ProcessUpdateOps` object. Returns a frozen
|
|
23
|
+
* copy with defaults applied and every key pre-validated. Throws synchronously
|
|
24
|
+
* on any shape error so adapters can fail fast before touching the DB.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} ops
|
|
27
|
+
* @returns {{
|
|
28
|
+
* increment: Record<string, number>,
|
|
29
|
+
* set: Record<string, unknown>,
|
|
30
|
+
* pushSlice: Record<string, { values: unknown[]; keepLast: number }>,
|
|
31
|
+
* newState: string|null,
|
|
32
|
+
* }}
|
|
33
|
+
*/
|
|
34
|
+
function validateOps(ops) {
|
|
35
|
+
if (!ops || typeof ops !== 'object' || Array.isArray(ops)) {
|
|
36
|
+
throw new Error('applyProcessUpdate: ops must be an object');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const increment = ops.increment || {};
|
|
40
|
+
const set = ops.set || {};
|
|
41
|
+
const pushSlice = ops.pushSlice || {};
|
|
42
|
+
const newState = ops.newState ?? null;
|
|
43
|
+
|
|
44
|
+
for (const [path, delta] of Object.entries(increment)) {
|
|
45
|
+
assertPath(path, 'increment');
|
|
46
|
+
if (typeof delta !== 'number' || !Number.isFinite(delta)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`applyProcessUpdate: increment['${path}'] must be a finite number, got ${typeof delta}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const path of Object.keys(set)) {
|
|
54
|
+
assertPath(path, 'set');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [path, spec] of Object.entries(pushSlice)) {
|
|
58
|
+
assertPath(path, 'pushSlice');
|
|
59
|
+
if (
|
|
60
|
+
!spec ||
|
|
61
|
+
typeof spec !== 'object' ||
|
|
62
|
+
!Array.isArray(spec.values) ||
|
|
63
|
+
typeof spec.keepLast !== 'number' ||
|
|
64
|
+
!Number.isInteger(spec.keepLast) ||
|
|
65
|
+
spec.keepLast <= 0
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`applyProcessUpdate: pushSlice['${path}'] must be { values: [], keepLast: positive integer }`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (newState !== null && typeof newState !== 'string') {
|
|
74
|
+
throw new Error('applyProcessUpdate: newState must be a string');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hasAnyOp =
|
|
78
|
+
Object.keys(increment).length > 0 ||
|
|
79
|
+
Object.keys(set).length > 0 ||
|
|
80
|
+
Object.keys(pushSlice).length > 0 ||
|
|
81
|
+
newState !== null;
|
|
82
|
+
if (!hasAnyOp) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'applyProcessUpdate: at least one of increment/set/pushSlice/newState must be provided'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Object.freeze({ increment, set, pushSlice, newState });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertPath(path, opName) {
|
|
92
|
+
if (!PATH_REGEX.test(path)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`applyProcessUpdate: invalid path '${path}' in ${opName} (must match ${PATH_REGEX})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Splits a validated path into `{ column, segments }`.
|
|
101
|
+
* `'context.pagination.pageCount'` → `{ column: 'context', segments: ['pagination', 'pageCount'] }`.
|
|
102
|
+
*/
|
|
103
|
+
function splitPath(path) {
|
|
104
|
+
const [column, ...segments] = path.split('.');
|
|
105
|
+
return { column, segments };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
PATH_REGEX,
|
|
110
|
+
validateOps,
|
|
111
|
+
splitPath,
|
|
112
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { IntegrationBase } = require('../../integration-base');
|
|
2
|
+
|
|
3
|
+
class ConfigCapturingModule {
|
|
4
|
+
static definition = {
|
|
5
|
+
getName: () => 'config-capturing-module'
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ConfigCapturingIntegration extends IntegrationBase {
|
|
10
|
+
static Definition = {
|
|
11
|
+
name: 'config-capturing',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
modules: {
|
|
14
|
+
primary: ConfigCapturingModule
|
|
15
|
+
},
|
|
16
|
+
display: {
|
|
17
|
+
label: 'Config Capturing Integration',
|
|
18
|
+
description: 'Test double for capturing config state during updates',
|
|
19
|
+
detailsUrl: 'https://example.com',
|
|
20
|
+
icon: 'test-icon'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static _capturedOnUpdateState = null;
|
|
25
|
+
|
|
26
|
+
static resetCaptures() {
|
|
27
|
+
this._capturedOnUpdateState = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static getCapturedOnUpdateState() {
|
|
31
|
+
return this._capturedOnUpdateState;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(params) {
|
|
35
|
+
super(params);
|
|
36
|
+
this.integrationRepository = {
|
|
37
|
+
updateIntegrationById: jest.fn().mockResolvedValue({}),
|
|
38
|
+
findIntegrationById: jest.fn().mockResolvedValue({}),
|
|
39
|
+
};
|
|
40
|
+
this.updateIntegrationStatus = {
|
|
41
|
+
execute: jest.fn().mockResolvedValue({})
|
|
42
|
+
};
|
|
43
|
+
this.updateIntegrationMessages = {
|
|
44
|
+
execute: jest.fn().mockResolvedValue({})
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async initialize() {
|
|
49
|
+
this.registerEventHandlers();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async onUpdate(params) {
|
|
53
|
+
ConfigCapturingIntegration._capturedOnUpdateState = {
|
|
54
|
+
thisConfig: JSON.parse(JSON.stringify(this.config)),
|
|
55
|
+
paramsConfig: params.config
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.config = this._deepMerge(this.config, params.config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_deepMerge(target, source) {
|
|
62
|
+
const result = { ...target };
|
|
63
|
+
for (const key of Object.keys(source)) {
|
|
64
|
+
if (
|
|
65
|
+
source[key] !== null &&
|
|
66
|
+
typeof source[key] === 'object' &&
|
|
67
|
+
!Array.isArray(source[key]) &&
|
|
68
|
+
target[key] !== null &&
|
|
69
|
+
typeof target[key] === 'object' &&
|
|
70
|
+
!Array.isArray(target[key])
|
|
71
|
+
) {
|
|
72
|
+
result[key] = this._deepMerge(target[key], source[key]);
|
|
73
|
+
} else {
|
|
74
|
+
result[key] = source[key];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { ConfigCapturingIntegration };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const { IntegrationBase } = require('../../integration-base');
|
|
2
|
+
|
|
3
|
+
class DummyModule {
|
|
4
|
+
static definition = {
|
|
5
|
+
getName: () => 'dummy'
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class DummyIntegration extends IntegrationBase {
|
|
10
|
+
static Definition = {
|
|
11
|
+
name: 'dummy',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
modules: {
|
|
14
|
+
dummy: DummyModule
|
|
15
|
+
},
|
|
16
|
+
display: {
|
|
17
|
+
label: 'Dummy Integration',
|
|
18
|
+
description: 'A dummy integration for testing',
|
|
19
|
+
detailsUrl: 'https://example.com',
|
|
20
|
+
icon: 'dummy-icon'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static getOptionDetails() {
|
|
25
|
+
return {
|
|
26
|
+
name: this.Definition.name,
|
|
27
|
+
version: this.Definition.version,
|
|
28
|
+
display: this.Definition.display
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(params) {
|
|
33
|
+
super(params);
|
|
34
|
+
this.sendSpy = jest.fn();
|
|
35
|
+
this.testAuthSpy = jest.fn();
|
|
36
|
+
this.eventCallHistory = [];
|
|
37
|
+
this.events = {};
|
|
38
|
+
|
|
39
|
+
this.integrationRepository = {
|
|
40
|
+
updateIntegrationById: jest.fn().mockResolvedValue({}),
|
|
41
|
+
findIntegrationById: jest.fn().mockResolvedValue({}),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.updateIntegrationStatus = {
|
|
45
|
+
execute: jest.fn().mockResolvedValue({})
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.updateIntegrationMessages = {
|
|
49
|
+
execute: jest.fn().mockResolvedValue({})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async loadDynamicUserActions() {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async send(event, data) {
|
|
58
|
+
this.sendSpy(event, data);
|
|
59
|
+
this.eventCallHistory.push({ event, data, timestamp: Date.now() });
|
|
60
|
+
if (event === 'ON_UPDATE') {
|
|
61
|
+
await this.onUpdate(data);
|
|
62
|
+
}
|
|
63
|
+
if (event === 'ON_DELETE') {
|
|
64
|
+
await this.onDelete(data);
|
|
65
|
+
}
|
|
66
|
+
return { event, data };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async initialize() {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async testAuth() {
|
|
74
|
+
this.testAuthSpy();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async onCreate({ integrationId }) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async onUpdate(params) {
|
|
82
|
+
this.config = this._deepMerge(this.config, params.config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_deepMerge(target, source) {
|
|
86
|
+
const result = { ...target };
|
|
87
|
+
for (const key of Object.keys(source)) {
|
|
88
|
+
if (
|
|
89
|
+
source[key] !== null &&
|
|
90
|
+
typeof source[key] === 'object' &&
|
|
91
|
+
!Array.isArray(source[key]) &&
|
|
92
|
+
target[key] !== null &&
|
|
93
|
+
typeof target[key] === 'object' &&
|
|
94
|
+
!Array.isArray(target[key])
|
|
95
|
+
) {
|
|
96
|
+
result[key] = this._deepMerge(target[key], source[key]);
|
|
97
|
+
} else {
|
|
98
|
+
result[key] = source[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async onDelete(params) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getConfig() {
|
|
109
|
+
return this.config || {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { DummyIntegration };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const { v4: uuid } = require('uuid');
|
|
2
|
+
const { validateConfigPatch } = require('../../repositories/config-patch-shared');
|
|
3
|
+
|
|
4
|
+
class TestIntegrationRepository {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.store = new Map();
|
|
7
|
+
this.operationHistory = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async createIntegration(entities, userId, config) {
|
|
11
|
+
const id = uuid();
|
|
12
|
+
const record = {
|
|
13
|
+
id,
|
|
14
|
+
_id: id,
|
|
15
|
+
entitiesIds: entities,
|
|
16
|
+
userId: userId,
|
|
17
|
+
config,
|
|
18
|
+
version: '0.0.0',
|
|
19
|
+
status: 'IN_CREATION',
|
|
20
|
+
messages: {},
|
|
21
|
+
createdAt: new Date(),
|
|
22
|
+
};
|
|
23
|
+
this.store.set(id, record);
|
|
24
|
+
this.operationHistory.push({ operation: 'create', id, userId, config });
|
|
25
|
+
return record;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async findIntegrationById(id) {
|
|
29
|
+
const rec = this.store.get(id);
|
|
30
|
+
this.operationHistory.push({ operation: 'findById', id, found: !!rec });
|
|
31
|
+
if (!rec) return null;
|
|
32
|
+
return rec;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async findIntegrationsByUserId(userId) {
|
|
36
|
+
const results = Array.from(this.store.values()).filter(r => r.userId === userId);
|
|
37
|
+
this.operationHistory.push({ operation: 'findByUserId', userId, count: results.length });
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findIntegrations(filter = {}) {
|
|
42
|
+
const { type, status } = filter;
|
|
43
|
+
const results = Array.from(this.store.values()).filter((r) => {
|
|
44
|
+
if (type && r.config?.type !== type) return false;
|
|
45
|
+
if (status && r.status !== status) return false;
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
this.operationHistory.push({ operation: 'findAll', count: results.length });
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async findIntegrationByUserId(userId) {
|
|
53
|
+
const record = Array.from(this.store.values()).find((r) => r.userId === userId);
|
|
54
|
+
this.operationHistory.push({
|
|
55
|
+
operation: 'findSingleByUserId',
|
|
56
|
+
userId,
|
|
57
|
+
found: !!record,
|
|
58
|
+
});
|
|
59
|
+
return record || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async findIntegrationsByEntityId(entityId) {
|
|
63
|
+
const target = String(entityId);
|
|
64
|
+
const results = Array.from(this.store.values()).filter((r) =>
|
|
65
|
+
(r.entitiesIds || []).map(String).includes(target)
|
|
66
|
+
);
|
|
67
|
+
this.operationHistory.push({
|
|
68
|
+
operation: 'findByEntityId',
|
|
69
|
+
entityId: target,
|
|
70
|
+
count: results.length,
|
|
71
|
+
});
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async updateIntegrationMessages(id, type, title, body, timestamp) {
|
|
76
|
+
const rec = this.store.get(id);
|
|
77
|
+
if (!rec) {
|
|
78
|
+
this.operationHistory.push({ operation: 'updateMessages', id, success: false });
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!rec.messages[type]) rec.messages[type] = [];
|
|
82
|
+
rec.messages[type].push({ title, message: body, timestamp });
|
|
83
|
+
this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async updateIntegrationConfig(id, config) {
|
|
88
|
+
if (config === null || config === undefined) {
|
|
89
|
+
throw new Error('Config parameter is required');
|
|
90
|
+
}
|
|
91
|
+
const rec = this.store.get(id);
|
|
92
|
+
if (!rec) {
|
|
93
|
+
this.operationHistory.push({ operation: 'updateConfig', id, success: false });
|
|
94
|
+
throw new Error(`Integration with id ${id} not found`);
|
|
95
|
+
}
|
|
96
|
+
rec.config = config;
|
|
97
|
+
this.operationHistory.push({ operation: 'updateConfig', id, success: true });
|
|
98
|
+
return rec;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async patchIntegrationConfig(id, patch) {
|
|
102
|
+
validateConfigPatch(patch);
|
|
103
|
+
if (!this.store.has(id)) {
|
|
104
|
+
this.operationHistory.push({ operation: 'patchConfig', id, success: false });
|
|
105
|
+
throw new Error(`Integration with id ${id} not found`);
|
|
106
|
+
}
|
|
107
|
+
// Simulates the DB round trip real adapters make: the merge reads
|
|
108
|
+
// the record fresh at write time rather than off a snapshot taken
|
|
109
|
+
// before the await, so concurrent patches to the same id can't lose
|
|
110
|
+
// each other's keys — same guarantee as `config || $1::jsonb` in
|
|
111
|
+
// postgres or a mongo/documentdb findAndModify.
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
const rec = this.store.get(id);
|
|
114
|
+
rec.config = { ...rec.config, ...patch };
|
|
115
|
+
this.operationHistory.push({ operation: 'patchConfig', id, success: true });
|
|
116
|
+
return rec;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async deleteIntegrationById(id) {
|
|
120
|
+
const existed = this.store.has(id);
|
|
121
|
+
const result = this.store.delete(id);
|
|
122
|
+
this.operationHistory.push({ operation: 'delete', id, existed, success: result });
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async updateIntegrationStatus(id, status) {
|
|
127
|
+
const rec = this.store.get(id);
|
|
128
|
+
if (rec) {
|
|
129
|
+
rec.status = status;
|
|
130
|
+
this.operationHistory.push({ operation: 'updateStatus', id, status, success: true });
|
|
131
|
+
} else {
|
|
132
|
+
this.operationHistory.push({ operation: 'updateStatus', id, status, success: false });
|
|
133
|
+
}
|
|
134
|
+
return !!rec;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getOperationHistory() {
|
|
138
|
+
return [...this.operationHistory];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clearHistory() {
|
|
142
|
+
this.operationHistory = [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { TestIntegrationRepository };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Removed Integration wrapper - using IntegrationBase directly
|
|
2
|
+
const {
|
|
3
|
+
mapIntegrationClassToIntegrationDTO,
|
|
4
|
+
} = require('../utils/map-integration-dto');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Use case for creating a new integration instance.
|
|
8
|
+
* @class CreateIntegration
|
|
9
|
+
*/
|
|
10
|
+
class CreateIntegration {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new CreateIntegration instance.
|
|
13
|
+
* @param {Object} params - Configuration parameters.
|
|
14
|
+
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
|
|
15
|
+
* @param {import('../integration-classes').IntegrationClasses} params.integrationClasses - Array of available integration classes.
|
|
16
|
+
* @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
|
|
17
|
+
*/
|
|
18
|
+
constructor({ integrationRepository, integrationClasses, moduleFactory }) {
|
|
19
|
+
this.integrationRepository = integrationRepository;
|
|
20
|
+
this.integrationClasses = integrationClasses;
|
|
21
|
+
this.moduleFactory = moduleFactory;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Executes the integration creation process. Reuses an existing
|
|
26
|
+
* integration when one already exists for the same userId, config.type,
|
|
27
|
+
* and entity set, instead of creating a duplicate.
|
|
28
|
+
* @async
|
|
29
|
+
* @param {string[]} entities - Array of entity IDs to associate with the integration.
|
|
30
|
+
* @param {string} userId - ID of the user creating the integration.
|
|
31
|
+
* @param {Object} config - Configuration object for the integration.
|
|
32
|
+
* @param {string} config.type - Type of integration to create.
|
|
33
|
+
* @returns {Promise<Object>} The created or reused integration DTO.
|
|
34
|
+
* @throws {Error} When integration class is not found for the specified type.
|
|
35
|
+
*/
|
|
36
|
+
async execute(entities, userId, config) {
|
|
37
|
+
console.log(
|
|
38
|
+
`[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const existing = await this._findDuplicate(
|
|
42
|
+
userId,
|
|
43
|
+
config?.type,
|
|
44
|
+
entities
|
|
45
|
+
);
|
|
46
|
+
if (existing) {
|
|
47
|
+
console.log(
|
|
48
|
+
`[Frigg] Found existing integration ${existing.id} for the same user, type, and entities — reusing it`
|
|
49
|
+
);
|
|
50
|
+
return this._reuseExisting(existing, config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const integrationRecord =
|
|
54
|
+
await this.integrationRepository.createIntegration(
|
|
55
|
+
entities,
|
|
56
|
+
userId,
|
|
57
|
+
config
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
`[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// A concurrent request may have created a matching row between the
|
|
64
|
+
// lookup above and this insert; re-check before any side effects run.
|
|
65
|
+
const survivor = await this._findDuplicate(
|
|
66
|
+
userId,
|
|
67
|
+
config?.type,
|
|
68
|
+
entities
|
|
69
|
+
);
|
|
70
|
+
if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
|
|
71
|
+
console.log(
|
|
72
|
+
`[Frigg] Concurrent create detected — deleting duplicate ${integrationRecord.id} and reusing ${survivor.id}`
|
|
73
|
+
);
|
|
74
|
+
await this.integrationRepository.deleteIntegrationById(
|
|
75
|
+
integrationRecord.id
|
|
76
|
+
);
|
|
77
|
+
return this._reuseExisting(survivor, config);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const integrationInstance = await this._buildInstance(
|
|
81
|
+
integrationRecord
|
|
82
|
+
);
|
|
83
|
+
console.log(
|
|
84
|
+
`[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
|
|
85
|
+
);
|
|
86
|
+
await integrationInstance.send('ON_CREATE', {
|
|
87
|
+
integrationId: integrationRecord.id,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _reuseExisting(integrationRecord, config) {
|
|
94
|
+
// The caller may be reconnecting with changed settings. Persist the
|
|
95
|
+
// requested config onto the reused row as a shallow patch, so new
|
|
96
|
+
// values take effect while keys added during the original create
|
|
97
|
+
// (webhook ids, secrets) survive. `type` is re-written to the same
|
|
98
|
+
// value it dedupe-matched on, which is a harmless no-op.
|
|
99
|
+
if (config && Object.keys(config).length > 0) {
|
|
100
|
+
integrationRecord =
|
|
101
|
+
await this.integrationRepository.patchIntegrationConfig(
|
|
102
|
+
integrationRecord.id,
|
|
103
|
+
config
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const integrationInstance = await this._buildInstance(
|
|
108
|
+
integrationRecord
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (integrationInstance.status === 'IN_CREATION') {
|
|
112
|
+
// ON_CREATE never finished for this row (crashed mid-setup or is
|
|
113
|
+
// still running concurrently). Re-firing ON_CREATE here would
|
|
114
|
+
// risk re-registering a webhook a partially-completed attempt
|
|
115
|
+
// already created — unsafe without idempotent setup, which is a
|
|
116
|
+
// separate, larger change. Surfaced loudly rather than silently
|
|
117
|
+
// reused as if healthy, so it's diagnosable; testAuth below still
|
|
118
|
+
// runs and can at least surface bad credentials as ERROR.
|
|
119
|
+
console.warn(
|
|
120
|
+
`[Frigg] Integration ${integrationInstance.id} is still IN_CREATION on reuse — setup never completed`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// User is actively trying to reconnect; here if the integration is
|
|
125
|
+
// disabled, we can enable it again.
|
|
126
|
+
const authPassed = await integrationInstance.testAuth();
|
|
127
|
+
await integrationInstance.reconcileAuthStatus(authPassed);
|
|
128
|
+
if (integrationInstance.status === 'DISABLED') {
|
|
129
|
+
console.log(
|
|
130
|
+
`[Frigg] Integration ${integrationInstance.id} changed status from DISABLED to ENABLED`
|
|
131
|
+
);
|
|
132
|
+
await integrationInstance.persistStatus('ENABLED');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _findDuplicate(userId, type, entityIds) {
|
|
139
|
+
const candidates =
|
|
140
|
+
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
141
|
+
const target = [...(entityIds ?? [])].map(String).sort();
|
|
142
|
+
|
|
143
|
+
const matches = candidates.filter((integration) => {
|
|
144
|
+
// An IN_DELETION row is either mid-teardown or a zombie left
|
|
145
|
+
// behind by a failed deleteIntegrationById call — never a live
|
|
146
|
+
// integration. Treating it as a duplicate would hand a reinstall
|
|
147
|
+
// attempt a row nothing will ever move out of IN_DELETION,
|
|
148
|
+
// silently discarded by the queue worker forever. Let a fresh
|
|
149
|
+
// create proceed instead; the zombie is cleaned up out-of-band,
|
|
150
|
+
// matching how teardown failures are already handled here.
|
|
151
|
+
if (integration.status === 'IN_DELETION') return false;
|
|
152
|
+
if (integration.config?.type !== type) return false;
|
|
153
|
+
const current = [...(integration.entitiesIds ?? [])]
|
|
154
|
+
.map(String)
|
|
155
|
+
.sort();
|
|
156
|
+
return (
|
|
157
|
+
current.length === target.length &&
|
|
158
|
+
current.every((id, index) => id === target[index])
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return matches.length ? this._pickOldest(matches) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_pickOldest(records) {
|
|
166
|
+
return [...records].sort((a, b) => {
|
|
167
|
+
const diff =
|
|
168
|
+
new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0);
|
|
169
|
+
if (diff !== 0) return diff;
|
|
170
|
+
return String(a.id).localeCompare(String(b.id), 'en', {
|
|
171
|
+
numeric: true,
|
|
172
|
+
});
|
|
173
|
+
})[0];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async _buildInstance(integrationRecord) {
|
|
177
|
+
const integrationClass = this.integrationClasses.find(
|
|
178
|
+
(integrationClass) =>
|
|
179
|
+
integrationClass.Definition.name ===
|
|
180
|
+
integrationRecord.config.type
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (!integrationClass) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`No integration class found for type: ${integrationRecord.config.type}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const modules = [];
|
|
190
|
+
for (const entityId of integrationRecord.entitiesIds) {
|
|
191
|
+
const moduleInstance = await this.moduleFactory.getModuleInstance(
|
|
192
|
+
entityId,
|
|
193
|
+
integrationRecord.userId
|
|
194
|
+
);
|
|
195
|
+
modules.push(moduleInstance);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const integrationInstance = new integrationClass({
|
|
199
|
+
id: integrationRecord.id,
|
|
200
|
+
userId: integrationRecord.userId,
|
|
201
|
+
entities: integrationRecord.entitiesIds,
|
|
202
|
+
config: integrationRecord.config,
|
|
203
|
+
status: integrationRecord.status,
|
|
204
|
+
version: integrationRecord.version,
|
|
205
|
+
messages: integrationRecord.messages,
|
|
206
|
+
modules,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await integrationInstance.initialize();
|
|
210
|
+
|
|
211
|
+
return integrationInstance;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { CreateIntegration };
|