@friggframework/core 2.0.0-next.1 → 2.0.0-next.100
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 +57 -0
- package/handlers/backend-utils.js +316 -0
- 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 +15 -0
- 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 +117 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/reporting.js +9 -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 +30 -0
- package/index.js +111 -50
- 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 +619 -72
- package/integrations/integration-router.js +460 -218
- package/integrations/options.js +3 -4
- 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 +274 -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 +285 -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/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 -350
- 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
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
createIntegrationMappingRepository,
|
|
3
|
+
} = require('./repositories/integration-mapping-repository-factory');
|
|
2
4
|
const { Options } = require('./options');
|
|
5
|
+
const {
|
|
6
|
+
UpdateIntegrationStatus,
|
|
7
|
+
} = require('./use-cases/update-integration-status');
|
|
8
|
+
const {
|
|
9
|
+
createIntegrationRepository,
|
|
10
|
+
} = require('./repositories/integration-repository-factory');
|
|
11
|
+
const {
|
|
12
|
+
UpdateIntegrationMessages,
|
|
13
|
+
} = require('./use-cases/update-integration-messages');
|
|
14
|
+
const {
|
|
15
|
+
PatchIntegrationConfig,
|
|
16
|
+
} = require('./use-cases/patch-integration-config');
|
|
17
|
+
const {
|
|
18
|
+
UpdateIntegrationConfig,
|
|
19
|
+
} = require('./use-cases/update-integration-config');
|
|
20
|
+
const { validateExtensionBinding } = require('./extension');
|
|
21
|
+
|
|
3
22
|
const constantsToBeMigrated = {
|
|
4
23
|
defaultEvents: {
|
|
5
24
|
ON_CREATE: 'ON_CREATE',
|
|
@@ -10,6 +29,8 @@ const constantsToBeMigrated = {
|
|
|
10
29
|
GET_USER_ACTIONS: 'GET_USER_ACTIONS',
|
|
11
30
|
GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
|
|
12
31
|
REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
|
|
32
|
+
WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
|
|
33
|
+
ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
|
|
13
34
|
// etc...
|
|
14
35
|
},
|
|
15
36
|
types: {
|
|
@@ -19,6 +40,22 @@ const constantsToBeMigrated = {
|
|
|
19
40
|
};
|
|
20
41
|
|
|
21
42
|
class IntegrationBase {
|
|
43
|
+
// todo: maybe we can pass this as Dependency Injection in the sub-class constructor
|
|
44
|
+
integrationRepository = createIntegrationRepository();
|
|
45
|
+
integrationMappingRepository = createIntegrationMappingRepository();
|
|
46
|
+
updateIntegrationStatus = new UpdateIntegrationStatus({
|
|
47
|
+
integrationRepository: this.integrationRepository,
|
|
48
|
+
});
|
|
49
|
+
updateIntegrationMessages = new UpdateIntegrationMessages({
|
|
50
|
+
integrationRepository: this.integrationRepository,
|
|
51
|
+
});
|
|
52
|
+
patchIntegrationConfig = new PatchIntegrationConfig({
|
|
53
|
+
integrationRepository: this.integrationRepository,
|
|
54
|
+
});
|
|
55
|
+
updateIntegrationConfig = new UpdateIntegrationConfig({
|
|
56
|
+
integrationRepository: this.integrationRepository,
|
|
57
|
+
});
|
|
58
|
+
|
|
22
59
|
static getOptionDetails() {
|
|
23
60
|
const options = new Options({
|
|
24
61
|
module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
|
|
@@ -26,6 +63,7 @@ class IntegrationBase {
|
|
|
26
63
|
});
|
|
27
64
|
return options.get();
|
|
28
65
|
}
|
|
66
|
+
|
|
29
67
|
/**
|
|
30
68
|
* CHILDREN SHOULD SPECIFY A DEFINITION FOR THE INTEGRATION
|
|
31
69
|
*/
|
|
@@ -35,6 +73,9 @@ class IntegrationBase {
|
|
|
35
73
|
supportedVersions: [], // Eventually usable for deprecation and future test version purposes
|
|
36
74
|
|
|
37
75
|
modules: {},
|
|
76
|
+
// Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
|
|
77
|
+
// Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
|
|
78
|
+
extensions: {},
|
|
38
79
|
display: {
|
|
39
80
|
name: 'Integration Name',
|
|
40
81
|
logo: '',
|
|
@@ -50,29 +91,30 @@ class IntegrationBase {
|
|
|
50
91
|
static getCurrentVersion() {
|
|
51
92
|
return this.Definition.version;
|
|
52
93
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
|
|
95
|
+
// REMOVED: registerEventHandlers() - Event handling is now done by IntegrationEventDispatcher
|
|
96
|
+
|
|
97
|
+
constructor(params = {}) {
|
|
98
|
+
this.modules = {};
|
|
99
|
+
this.events = this.events || {};
|
|
100
|
+
this.messages = { errors: [], warnings: [] };
|
|
101
|
+
this._isHydrated = false;
|
|
102
|
+
|
|
103
|
+
if (params && Object.keys(params).length > 0) {
|
|
104
|
+
this.setIntegrationRecord({
|
|
105
|
+
record: {
|
|
106
|
+
id: params.id,
|
|
107
|
+
userId: params.userId,
|
|
108
|
+
entities: params.entities,
|
|
109
|
+
config: params.config,
|
|
110
|
+
status: params.status,
|
|
111
|
+
version: params.version,
|
|
112
|
+
messages: params.messages,
|
|
113
|
+
},
|
|
114
|
+
modules: params.modules || [],
|
|
115
|
+
});
|
|
66
116
|
}
|
|
67
|
-
}
|
|
68
|
-
registerEventHandlers() {
|
|
69
|
-
this.on = {
|
|
70
|
-
...this.defaultEvents,
|
|
71
|
-
...this.events,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
117
|
|
|
75
|
-
constructor(params) {
|
|
76
118
|
this.defaultEvents = {
|
|
77
119
|
[constantsToBeMigrated.defaultEvents.ON_CREATE]: {
|
|
78
120
|
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
@@ -106,83 +148,242 @@ class IntegrationBase {
|
|
|
106
148
|
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
107
149
|
handler: this.refreshActionOptions,
|
|
108
150
|
},
|
|
151
|
+
[constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
|
|
152
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
153
|
+
handler: this.onWebhookReceived,
|
|
154
|
+
},
|
|
155
|
+
[constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
|
|
156
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
157
|
+
handler: this.onWebhook,
|
|
158
|
+
},
|
|
109
159
|
};
|
|
110
|
-
this.loadModules();
|
|
111
160
|
}
|
|
112
161
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
// todo: debate wether we want to keep this pattern to set the record or not.
|
|
163
|
+
/**
|
|
164
|
+
* Persist the database record and module instances onto this integration instance.
|
|
165
|
+
* Accepts either a plain object containing the persisted fields or an object with
|
|
166
|
+
* a `record` property plus a `modules` collection.
|
|
167
|
+
* @param {Object} payload
|
|
168
|
+
* @param {Object} [payload.record]
|
|
169
|
+
* @param {Array} [payload.modules]
|
|
170
|
+
*/
|
|
171
|
+
setIntegrationRecord(payload = {}) {
|
|
172
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
173
|
+
throw new Error('setIntegrationRecord requires integration data');
|
|
118
174
|
}
|
|
119
|
-
|
|
175
|
+
|
|
176
|
+
const integrationRecord = payload.record;
|
|
177
|
+
const integrationModules = payload.modules ?? [];
|
|
178
|
+
|
|
179
|
+
if (!integrationRecord) {
|
|
180
|
+
throw new Error('Integration record not provided');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { id, userId, entities, config, status, version, messages } =
|
|
184
|
+
integrationRecord;
|
|
185
|
+
|
|
186
|
+
this.id = id;
|
|
187
|
+
this.userId = userId;
|
|
188
|
+
this.entities = entities;
|
|
189
|
+
this.config = config;
|
|
190
|
+
this.status = status;
|
|
191
|
+
this.version = version;
|
|
192
|
+
this.messages = messages || { errors: [], warnings: [] };
|
|
193
|
+
|
|
194
|
+
this.modules = this._appendModules(integrationModules);
|
|
195
|
+
|
|
196
|
+
this.record = {
|
|
197
|
+
id: this.id,
|
|
198
|
+
userId: this.userId,
|
|
199
|
+
entities: this.entities,
|
|
200
|
+
config: this.config,
|
|
201
|
+
status: this.status,
|
|
202
|
+
version: this.version,
|
|
203
|
+
messages: this.messages,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this._isHydrated = Boolean(this.id);
|
|
207
|
+
return this;
|
|
120
208
|
}
|
|
121
209
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
210
|
+
get isHydrated() {
|
|
211
|
+
return this._isHydrated;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
assertHydrated(message = 'Integration instance is not hydrated') {
|
|
215
|
+
if (!this.isHydrated) {
|
|
216
|
+
throw new Error(message);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns the modules as object with keys as module names.
|
|
222
|
+
* Uses the keys from Definition.modules to attach modules correctly.
|
|
223
|
+
*
|
|
224
|
+
* Example:
|
|
225
|
+
* Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
|
|
226
|
+
* Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
|
|
227
|
+
*
|
|
228
|
+
* @private
|
|
229
|
+
* @param {Array} integrationModules - Array of module instances
|
|
230
|
+
* @returns {Object} The modules object
|
|
231
|
+
*/
|
|
232
|
+
_appendModules(integrationModules) {
|
|
233
|
+
const modules = {};
|
|
234
|
+
|
|
235
|
+
// Build reverse mapping: definition.getName() → referenceKey
|
|
236
|
+
// e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
|
|
237
|
+
const moduleNameToKey = {};
|
|
238
|
+
if (this.constructor.Definition?.modules) {
|
|
239
|
+
for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
|
|
240
|
+
const definition = moduleConfig.definition;
|
|
241
|
+
if (definition) {
|
|
242
|
+
// Use getName() if available, fallback to moduleName
|
|
243
|
+
const definitionName = typeof definition.getName === 'function'
|
|
244
|
+
? definition.getName()
|
|
245
|
+
: definition.moduleName;
|
|
246
|
+
if (definitionName) {
|
|
247
|
+
moduleNameToKey[definitionName] = key;
|
|
248
|
+
}
|
|
141
249
|
}
|
|
142
250
|
}
|
|
143
251
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
252
|
+
|
|
253
|
+
for (const module of integrationModules) {
|
|
254
|
+
const moduleName =
|
|
255
|
+
typeof module.getName === 'function'
|
|
256
|
+
? module.getName()
|
|
257
|
+
: module.name;
|
|
258
|
+
|
|
259
|
+
// Use the reference key from Definition.modules if available,
|
|
260
|
+
// otherwise fall back to moduleName
|
|
261
|
+
const key = moduleNameToKey[moduleName] || moduleName;
|
|
262
|
+
|
|
263
|
+
if (key) {
|
|
264
|
+
modules[key] = module;
|
|
265
|
+
this[key] = module;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Wire the Delegate pattern so Module can notify this integration
|
|
269
|
+
// of events it cannot handle itself (e.g. credential invalidation
|
|
270
|
+
// needing an Integration.status flip). Without this, Module.notify
|
|
271
|
+
// silently no-ops and Integration.status never updates on auth
|
|
272
|
+
// failure.
|
|
273
|
+
if (module && typeof module === 'object') {
|
|
274
|
+
module.delegate = this;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return modules;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check the current config against the required fields declared by
|
|
283
|
+
* `getConfigOptions()`. Config options use the react-jsonschema-form shape,
|
|
284
|
+
* so the required top-level keys live in `jsonSchema.required`. Records a
|
|
285
|
+
* warning for each missing field. Does not change integration status —
|
|
286
|
+
* the caller decides the consequence (see `onCreate`/`onUpdate`), the same
|
|
287
|
+
* separation `testAuth`/`reconcileAuthStatus` use for the auth axis.
|
|
288
|
+
* @returns {Promise<boolean>} True when a required field is missing.
|
|
289
|
+
*/
|
|
290
|
+
async validateConfig() {
|
|
291
|
+
const { jsonSchema } = await this.getConfigOptions();
|
|
292
|
+
const currentConfig = this.getConfig() || {};
|
|
293
|
+
const requiredKeys = Array.isArray(jsonSchema?.required)
|
|
294
|
+
? jsonSchema.required
|
|
295
|
+
: [];
|
|
296
|
+
let needsConfig = false;
|
|
297
|
+
for (const key of requiredKeys) {
|
|
298
|
+
if (!Object.prototype.hasOwnProperty.call(currentConfig, key)) {
|
|
299
|
+
needsConfig = true;
|
|
300
|
+
const label = jsonSchema?.properties?.[key]?.title || key;
|
|
301
|
+
await this.updateIntegrationMessages.execute(
|
|
302
|
+
this.id,
|
|
303
|
+
'warnings',
|
|
304
|
+
'Config Validation Error',
|
|
305
|
+
`Missing required field of ${label}`,
|
|
306
|
+
Date.now()
|
|
307
|
+
);
|
|
308
|
+
}
|
|
147
309
|
}
|
|
310
|
+
return needsConfig;
|
|
148
311
|
}
|
|
149
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Verify every module's credentials. Records a diagnostic error message
|
|
315
|
+
* per failing module and returns whether all passed. Does not directly
|
|
316
|
+
* change integration status — the caller decides the consequence (see
|
|
317
|
+
* reconcileAuthStatus), so a passive check and an active reconnect can
|
|
318
|
+
* react differently. (A module can still fire a credential-invalidated
|
|
319
|
+
* delegate that flips status via receiveNotification, independent of this
|
|
320
|
+
* return value.)
|
|
321
|
+
* @returns {Promise<boolean>} True when every module authenticated.
|
|
322
|
+
*/
|
|
150
323
|
async testAuth() {
|
|
151
324
|
let didAuthPass = true;
|
|
152
325
|
|
|
153
|
-
for (const module of Object.keys(
|
|
326
|
+
for (const module of Object.keys(this.constructor.Definition.modules)) {
|
|
154
327
|
try {
|
|
155
|
-
await this[module].testAuth();
|
|
328
|
+
const authPassed = await this[module].testAuth();
|
|
329
|
+
if (!authPassed) {
|
|
330
|
+
throw new Error(`testAuth returned false for module ${module}`);
|
|
331
|
+
}
|
|
156
332
|
} catch {
|
|
157
333
|
didAuthPass = false;
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
334
|
+
await this.updateIntegrationMessages.execute(
|
|
335
|
+
this.id,
|
|
336
|
+
'errors',
|
|
337
|
+
'Authentication Error',
|
|
338
|
+
`There was an error with your ${this[
|
|
161
339
|
module
|
|
162
|
-
].
|
|
340
|
+
].getName()} Entity.
|
|
163
341
|
Please reconnect/re-authenticate, or reach out to Support for assistance.`,
|
|
164
|
-
|
|
165
|
-
|
|
342
|
+
Date.now()
|
|
343
|
+
);
|
|
166
344
|
}
|
|
167
345
|
}
|
|
168
346
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
347
|
+
return didAuthPass;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Reconcile the auth-health axis (ERROR ↔ ENABLED) from a testAuth result.
|
|
352
|
+
* On success it never clears DISABLED — a user pause is not an auth-health
|
|
353
|
+
* state, so it is only lifted by a deliberate reconnect. On failure the
|
|
354
|
+
* integration is marked ERROR regardless of its prior status.
|
|
355
|
+
* @param {boolean} authPassed - The result of testAuth().
|
|
356
|
+
*/
|
|
357
|
+
async reconcileAuthStatus(authPassed) {
|
|
358
|
+
if (!authPassed) {
|
|
359
|
+
console.log(
|
|
360
|
+
`[Frigg] Integration ${this.id} failed to authenticate`
|
|
361
|
+
);
|
|
362
|
+
await this.persistStatus('ERROR');
|
|
363
|
+
}
|
|
364
|
+
if (authPassed && this.status === 'ERROR') {
|
|
365
|
+
console.log(
|
|
366
|
+
`[Frigg] auth confirmed for integration ${this.id} — clearing ERROR → ENABLED`
|
|
367
|
+
);
|
|
368
|
+
await this.persistStatus('ENABLED');
|
|
173
369
|
}
|
|
174
370
|
}
|
|
175
371
|
|
|
176
372
|
async getMapping(sourceId) {
|
|
177
|
-
|
|
373
|
+
// todo: not sure we should call the repository directly from here
|
|
374
|
+
return this.integrationMappingRepository.findMappingBy(
|
|
375
|
+
this.id,
|
|
376
|
+
sourceId
|
|
377
|
+
);
|
|
178
378
|
}
|
|
179
379
|
|
|
180
380
|
async upsertMapping(sourceId, mapping) {
|
|
181
381
|
if (!sourceId) {
|
|
182
382
|
throw new Error(`sourceId must be set`);
|
|
183
383
|
}
|
|
184
|
-
|
|
185
|
-
|
|
384
|
+
// todo: not sure we should call the repository directly from here
|
|
385
|
+
return await this.integrationMappingRepository.upsertMapping(
|
|
386
|
+
this.id,
|
|
186
387
|
sourceId,
|
|
187
388
|
mapping
|
|
188
389
|
);
|
|
@@ -191,13 +392,40 @@ class IntegrationBase {
|
|
|
191
392
|
/**
|
|
192
393
|
* CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
|
|
193
394
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Default post-create lifecycle hook. The integration is born IN_CREATION;
|
|
397
|
+
* moved to NEEDS_CONFIG when validateConfig finds a required field
|
|
398
|
+
* missing, otherwise enabled. If this hook throws, the integration is
|
|
399
|
+
* left IN_CREATION — a visibly incomplete create rather than a healthy
|
|
400
|
+
* looking one. Children can override to run their own setup (and then own
|
|
401
|
+
* their status transition, calling `super.onCreate()` to keep this default).
|
|
402
|
+
*/
|
|
403
|
+
async onCreate() {
|
|
404
|
+
const needsConfig = await this.validateConfig();
|
|
405
|
+
await this.persistStatus(needsConfig ? 'NEEDS_CONFIG' : 'ENABLED');
|
|
198
406
|
}
|
|
199
407
|
|
|
200
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Default post-update lifecycle hook: merges any submitted config in as a
|
|
410
|
+
* patch, then re-validates. A NEEDS_CONFIG integration moves to ENABLED
|
|
411
|
+
* once nothing required is missing — other statuses (DISABLED, ERROR,
|
|
412
|
+
* IN_CREATION, IN_DELETION) are left alone; a config edit shouldn't
|
|
413
|
+
* silently un-pause or auto-heal those. Children can override to run
|
|
414
|
+
* their own update logic.
|
|
415
|
+
* @param {Object} [params]
|
|
416
|
+
* @param {Object} [params.config] - Keys to merge into the existing config.
|
|
417
|
+
*/
|
|
418
|
+
async onUpdate(params) {
|
|
419
|
+
if (params?.config) {
|
|
420
|
+
await this.patchConfig(params.config);
|
|
421
|
+
}
|
|
422
|
+
const needsConfig = await this.validateConfig();
|
|
423
|
+
if (needsConfig) {
|
|
424
|
+
await this.persistStatus('NEEDS_CONFIG');
|
|
425
|
+
} else if (this.status === 'NEEDS_CONFIG') {
|
|
426
|
+
await this.persistStatus('ENABLED');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
201
429
|
|
|
202
430
|
async onDelete(params) {}
|
|
203
431
|
|
|
@@ -224,7 +452,6 @@ class IntegrationBase {
|
|
|
224
452
|
return {};
|
|
225
453
|
}
|
|
226
454
|
async loadUserActions({ actionType } = {}) {
|
|
227
|
-
console.log('loadUserActions called with actionType:', actionType);
|
|
228
455
|
const userActions = {};
|
|
229
456
|
for (const [key, event] of Object.entries(this.events)) {
|
|
230
457
|
if (event.type === constantsToBeMigrated.types.USER_ACTION) {
|
|
@@ -259,6 +486,326 @@ class IntegrationBase {
|
|
|
259
486
|
};
|
|
260
487
|
return options;
|
|
261
488
|
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* WEBHOOK EVENT HANDLERS
|
|
492
|
+
*/
|
|
493
|
+
async onWebhookReceived({ req, res }) {
|
|
494
|
+
// Default: queue webhook for processing
|
|
495
|
+
const body = req.body;
|
|
496
|
+
const integrationId = req.params.integrationId || null;
|
|
497
|
+
|
|
498
|
+
await this.queueWebhook({
|
|
499
|
+
integrationId,
|
|
500
|
+
body,
|
|
501
|
+
headers: req.headers,
|
|
502
|
+
query: req.query,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
res.status(200).json({ received: true });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async onWebhook({ data }) {
|
|
509
|
+
// Default: no-op, integrations override this
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Queue a webhook for asynchronous worker dispatch.
|
|
514
|
+
*
|
|
515
|
+
* The dispatch event defaults to `ON_WEBHOOK` for backward compatibility
|
|
516
|
+
* with the `Definition.webhooks: true` path. Extensions (and any caller
|
|
517
|
+
* that needs the worker to invoke a specific bound handler) can override
|
|
518
|
+
* by passing `event` in the payload — it's stripped from the payload and
|
|
519
|
+
* used as the SQS message's dispatch event.
|
|
520
|
+
*
|
|
521
|
+
* @param {Object} data - Webhook payload. May include `event` to override
|
|
522
|
+
* the default `ON_WEBHOOK` dispatch event. All other fields are passed
|
|
523
|
+
* through to the worker as the `data` field of the SQS message.
|
|
524
|
+
*/
|
|
525
|
+
async queueWebhook(data) {
|
|
526
|
+
const { QueuerUtil } = require('../queues');
|
|
527
|
+
|
|
528
|
+
const queueName = `${this.constructor.Definition.name
|
|
529
|
+
.toUpperCase()
|
|
530
|
+
.replace(/-/g, '_')}_QUEUE_URL`;
|
|
531
|
+
const queueUrl = process.env[queueName];
|
|
532
|
+
|
|
533
|
+
if (!queueUrl) {
|
|
534
|
+
throw new Error(`Queue URL not found for ${queueName}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const { event: dispatchEvent, ...payload } = data || {};
|
|
538
|
+
|
|
539
|
+
return QueuerUtil.send(
|
|
540
|
+
{
|
|
541
|
+
event: dispatchEvent || 'ON_WEBHOOK',
|
|
542
|
+
data: payload,
|
|
543
|
+
},
|
|
544
|
+
queueUrl
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// === Domain Methods (moved from Integration.js) ===
|
|
549
|
+
|
|
550
|
+
getConfig() {
|
|
551
|
+
return this.config;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getModule(key) {
|
|
555
|
+
return this.modules[key];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setModule(key, module) {
|
|
559
|
+
this.modules[key] = module;
|
|
560
|
+
this[key] = module;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
addError(error) {
|
|
564
|
+
if (!this.messages.errors) {
|
|
565
|
+
this.messages.errors = [];
|
|
566
|
+
}
|
|
567
|
+
this.messages.errors.push(error);
|
|
568
|
+
this.status = 'ERROR';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
addWarning(warning) {
|
|
572
|
+
if (!this.messages.warnings) {
|
|
573
|
+
this.messages.warnings = [];
|
|
574
|
+
}
|
|
575
|
+
this.messages.warnings.push(warning);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Persist a status change and keep the in-memory field in sync. The
|
|
580
|
+
* single place that couples both writes, so no caller can update the
|
|
581
|
+
* database while leaving `this.status` stale.
|
|
582
|
+
* @param {string} status - The new integration status.
|
|
583
|
+
*/
|
|
584
|
+
async persistStatus(status) {
|
|
585
|
+
await this.updateIntegrationStatus.execute(this.id, status);
|
|
586
|
+
this.status = status;
|
|
587
|
+
console.log(`[Frigg] Integration ${this.id} status changed to ${status}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Merge a partial update into config and keep the in-memory field in
|
|
592
|
+
* sync with what was actually persisted — not a local `{...this.config,
|
|
593
|
+
* ...patch}` guess, which would silently drop keys a concurrent writer
|
|
594
|
+
* already landed. Throws if the merge fails.
|
|
595
|
+
* @param {Object} patch - Keys to merge into the existing config.
|
|
596
|
+
*/
|
|
597
|
+
async patchConfig(patch) {
|
|
598
|
+
const updated = await this.patchIntegrationConfig.execute(
|
|
599
|
+
this.id,
|
|
600
|
+
patch
|
|
601
|
+
);
|
|
602
|
+
this.config = updated.config;
|
|
603
|
+
return this.config;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Replace config entirely and keep the in-memory field in sync. Keys
|
|
608
|
+
* omitted from the new config are deleted. Throws if the write fails.
|
|
609
|
+
* @param {Object} config - The new configuration object.
|
|
610
|
+
*/
|
|
611
|
+
async updateConfig(config) {
|
|
612
|
+
const updated = await this.updateIntegrationConfig.execute(
|
|
613
|
+
this.id,
|
|
614
|
+
config
|
|
615
|
+
);
|
|
616
|
+
this.config = updated.config;
|
|
617
|
+
return this.config;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
isActive() {
|
|
621
|
+
return this.status === 'ENABLED' || this.status === 'ACTIVE';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
needsConfiguration() {
|
|
625
|
+
return this.status === 'NEEDS_CONFIG';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
hasErrors() {
|
|
629
|
+
return this.status === 'ERROR';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
belongsToUser(userId) {
|
|
633
|
+
return this.userId.toString() === userId.toString();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
registerEventHandlers() {
|
|
637
|
+
this.on = {
|
|
638
|
+
...this.defaultEvents,
|
|
639
|
+
...this.events,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Merge Tier 3 Integration Extension events into `this.events`.
|
|
645
|
+
*
|
|
646
|
+
* For each binding declared on `static Definition.extensions`, this method:
|
|
647
|
+
* 1. Validates the extension bundle shape and binding handlers
|
|
648
|
+
* 2. For each event the extension declares, resolves the handler in priority order:
|
|
649
|
+
* a. Subclass-defined `this.events[eventName]` (set in the constructor) — wins; if a
|
|
650
|
+
* binding tried to override that event with `handlers`, we log a warning so the
|
|
651
|
+
* author knows their override is shadowed.
|
|
652
|
+
* b. A method-name string in `binding.handlers[eventName]` → method on this instance
|
|
653
|
+
* c. The extension's own default `handler` function
|
|
654
|
+
* d. Otherwise throw — neither side provided a handler
|
|
655
|
+
* 3. Binds the resolved function to this instance and writes it to `this.events[eventName]`
|
|
656
|
+
*
|
|
657
|
+
* Two bindings declaring the same event throw a deterministic conflict error — silent
|
|
658
|
+
* "first/last writer wins" makes routing bugs nearly impossible to diagnose.
|
|
659
|
+
*
|
|
660
|
+
* @private
|
|
661
|
+
*/
|
|
662
|
+
_mergeExtensions() {
|
|
663
|
+
const extensions = this.constructor.Definition?.extensions || {};
|
|
664
|
+
const integrationName = this.constructor.Definition?.name;
|
|
665
|
+
// Tracks which event names have been claimed by an extension binding during
|
|
666
|
+
// this merge — distinct from subclass-defined events on `this.events`.
|
|
667
|
+
const mergedByExtension = new Map();
|
|
668
|
+
|
|
669
|
+
for (const [bindingName, binding] of Object.entries(extensions)) {
|
|
670
|
+
if (!binding || typeof binding !== 'object') {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Integration "${integrationName}" extension binding "${bindingName}" must be an object`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const { extension, handlers = {} } = binding;
|
|
676
|
+
validateExtensionBinding(
|
|
677
|
+
extension,
|
|
678
|
+
bindingName,
|
|
679
|
+
integrationName,
|
|
680
|
+
binding
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const extEvents = extension.events || {};
|
|
684
|
+
for (const [eventName, eventDef] of Object.entries(extEvents)) {
|
|
685
|
+
// Conflict detection: another extension binding already claimed this event name.
|
|
686
|
+
if (mergedByExtension.has(eventName)) {
|
|
687
|
+
const prev = mergedByExtension.get(eventName);
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Integration "${integrationName}" extension event conflict: ` +
|
|
690
|
+
`event "${eventName}" is declared by both binding "${prev}" and binding "${bindingName}" — ` +
|
|
691
|
+
`use distinct event names per binding or omit duplicates`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Subclass shadowing: if the subclass set this.events[eventName] before initialize(),
|
|
696
|
+
// it wins. Warn if the binding tried to wire an override that's now ignored.
|
|
697
|
+
if (this.events[eventName]) {
|
|
698
|
+
if (typeof handlers[eventName] === 'string') {
|
|
699
|
+
console.warn(
|
|
700
|
+
`[Frigg] Integration "${integrationName}" binding "${bindingName}": ` +
|
|
701
|
+
`handler "${handlers[eventName]}" for event "${eventName}" is ignored because ` +
|
|
702
|
+
`this.events["${eventName}"] was already set (subclass constructor or earlier merge)`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let fn;
|
|
709
|
+
const override = handlers[eventName];
|
|
710
|
+
if (typeof override === 'string') {
|
|
711
|
+
if (typeof this[override] !== 'function') {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Integration "${integrationName}" extension binding "${bindingName}": handler method "${override}" not found on instance`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
fn = this[override];
|
|
717
|
+
} else if (typeof eventDef.handler === 'function') {
|
|
718
|
+
fn = eventDef.handler;
|
|
719
|
+
} else {
|
|
720
|
+
throw new Error(
|
|
721
|
+
`Extension "${extension.name}" event "${eventName}" has no default handler and binding "${bindingName}" did not provide one`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
this.events[eventName] = {
|
|
726
|
+
type: eventDef.type,
|
|
727
|
+
handler: fn.bind(this),
|
|
728
|
+
};
|
|
729
|
+
mergedByExtension.set(eventName, bindingName);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async initialize() {
|
|
735
|
+
try {
|
|
736
|
+
const additionalUserActions = await this.loadDynamicUserActions();
|
|
737
|
+
this.events = { ...this.events, ...additionalUserActions };
|
|
738
|
+
} catch (e) {
|
|
739
|
+
this.addError(e);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
this._mergeExtensions();
|
|
743
|
+
this.registerEventHandlers();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async send(event, object) {
|
|
747
|
+
if (!this.on[event]) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Event ${event} is not defined in the Integration event object`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
return this.on[event].handler.call(this, object);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
getOptionDetails() {
|
|
756
|
+
const options = new Options({
|
|
757
|
+
module: Object.values(this.constructor.Definition.modules)[0],
|
|
758
|
+
...this.constructor.Definition,
|
|
759
|
+
});
|
|
760
|
+
return options.get();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Legacy method for backward compatibility
|
|
764
|
+
async loadModules() {
|
|
765
|
+
// This method was used in the old architecture for loading modules
|
|
766
|
+
// In the new architecture, modules are injected via constructor
|
|
767
|
+
// For backward compatibility, this is a no-op
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Receives notifications from modules (the Delegate pattern) when
|
|
773
|
+
* something integration-level needs attention. Today this catches the
|
|
774
|
+
* `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
|
|
775
|
+
* and flips this integration's status to ERROR so the queue worker
|
|
776
|
+
* stops processing further webhooks until the user re-authorizes.
|
|
777
|
+
*
|
|
778
|
+
* Modules are wired to this delegate in `_appendModules()`, which runs
|
|
779
|
+
* during `setIntegrationRecord()` — this covers every construction path
|
|
780
|
+
* (HTTP read, queue worker, create/update/delete flows, etc.).
|
|
781
|
+
*
|
|
782
|
+
* The delegate string below must match `Module.DLGT_CREDENTIAL_INVALIDATED`
|
|
783
|
+
* in `packages/core/modules/module.js`.
|
|
784
|
+
*
|
|
785
|
+
* @param {Object} notifier - The module that fired the event
|
|
786
|
+
* @param {string} delegateString - Event type string
|
|
787
|
+
* @param {Object} [object] - Optional event payload
|
|
788
|
+
* @returns {Promise<void>}
|
|
789
|
+
*/
|
|
790
|
+
async receiveNotification(notifier, delegateString, object = null) {
|
|
791
|
+
if (!this.id) return;
|
|
792
|
+
|
|
793
|
+
if (delegateString === 'CREDENTIAL_INVALIDATED') {
|
|
794
|
+
console.log(
|
|
795
|
+
`[Frigg] Module ${notifier?.name || '?'} reported invalid credentials for integration ${this.id} — marking ERROR`
|
|
796
|
+
);
|
|
797
|
+
await this.persistStatus('ERROR');
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (delegateString === 'CREDENTIAL_VALIDATED') {
|
|
802
|
+
if (this.status !== 'ERROR') return;
|
|
803
|
+
console.log(
|
|
804
|
+
`[Frigg] Module ${notifier?.name || '?'} reported valid credentials for integration ${this.id} — clearing ERROR → ENABLED`
|
|
805
|
+
);
|
|
806
|
+
await this.persistStatus('ENABLED');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
262
809
|
}
|
|
263
810
|
|
|
264
811
|
module.exports = { IntegrationBase };
|