@friggframework/core 2.0.0-next.5 → 2.0.0-next.50
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 +693 -0
- package/README.md +959 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +179 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/Worker.js +8 -21
- package/core/create-handler.js +2 -7
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +307 -0
- package/credential/repositories/credential-repository-postgres.js +313 -0
- package/credential/repositories/credential-repository.js +302 -0
- package/credential/use-cases/get-credential-for-user.js +21 -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/encryption/README.md +684 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/index.js +25 -12
- package/database/models/WebsocketConnection.js +16 -10
- package/database/models/readme.md +1 -0
- package/database/prisma.js +222 -0
- package/database/repositories/health-check-repository-factory.js +43 -0
- package/database/repositories/health-check-repository-interface.js +87 -0
- package/database/repositories/health-check-repository-mongodb.js +91 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/health-check-repository.js +108 -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 +137 -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 +91 -0
- package/database/utils/mongodb-schema-init.js +106 -0
- package/database/utils/prisma-runner.js +400 -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/encrypt/test-encrypt.js +0 -2
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +334 -0
- package/generated/prisma-mongodb/index-browser.js +316 -0
- package/generated/prisma-mongodb/index.d.ts +22898 -0
- package/generated/prisma-mongodb/index.js +359 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +362 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +341 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +356 -0
- package/generated/prisma-postgresql/index-browser.js +338 -0
- package/generated/prisma-postgresql/index.d.ts +25072 -0
- package/generated/prisma-postgresql/index.js +381 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +345 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +363 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +56 -0
- package/handlers/backend-utils.js +180 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +15 -0
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +256 -0
- package/handlers/routers/health.js +519 -0
- package/handlers/routers/integration-defined-routers.js +45 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/user.js +63 -0
- package/handlers/routers/websocket.js +57 -0
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +352 -0
- package/handlers/workers/integration-defined-workers.js +27 -0
- package/index.js +77 -22
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +296 -54
- package/integrations/integration-router.js +381 -182
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -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-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +127 -0
- package/integrations/repositories/integration-repository-mongo.js +303 -0
- package/integrations/repositories/integration-repository-postgres.js +352 -0
- package/integrations/repositories/process-repository-factory.js +46 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/dummy-integration-class.js +83 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/delete-integration-for-user.js +101 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/utils/map-integration-dto.js +36 -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}/entity.js +1 -1
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +377 -0
- package/modules/repositories/module-repository-postgres.js +426 -0
- package/modules/repositories/module-repository.js +316 -0
- package/{module-plugin → modules}/requester/requester.js +1 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- 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 +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +55 -0
- package/modules/use-cases/process-authorization-callback.js +122 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +82 -50
- package/prisma-mongodb/schema.prisma +362 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +345 -0
- package/queues/queuer-util.js +28 -15
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -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-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/core/index.d.ts +2 -2
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -59
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +291 -0
- package/user/repositories/user-repository-postgres.js +350 -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 +106 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +93 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -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/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -45
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Frigg Commands - Application Service Layer
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Frigg Commands provide a clean, stable application service layer for all database operations in the Frigg Integration Framework. They abstract away the underlying ORM (currently Mongoose) and provide a consistent API for managing users, credentials, entities, and integrations.
|
|
6
|
+
|
|
7
|
+
## Why Use Commands?
|
|
8
|
+
|
|
9
|
+
### 1. **ORM Independence**
|
|
10
|
+
Commands isolate your integration code from the underlying database implementation. This allows Frigg to migrate between ORMs (e.g., Mongoose to Prisma) without breaking your integration code.
|
|
11
|
+
|
|
12
|
+
### 2. **Hexagonal Architecture**
|
|
13
|
+
Commands act as the **application service layer** in hexagonal architecture:
|
|
14
|
+
- **Domain Layer**: Your use cases and business logic
|
|
15
|
+
- **Application Layer**: Frigg Commands (this layer)
|
|
16
|
+
- **Infrastructure Layer**: Repositories and database models (hidden from you)
|
|
17
|
+
|
|
18
|
+
### 3. **Single Source of Truth**
|
|
19
|
+
All database operations flow through commands, making it easier to:
|
|
20
|
+
- Add caching, logging, or monitoring
|
|
21
|
+
- Enforce data validation rules
|
|
22
|
+
- Maintain consistent error handling
|
|
23
|
+
- Track data access patterns
|
|
24
|
+
|
|
25
|
+
### 4. **Future-Proof**
|
|
26
|
+
When Frigg upgrades its internals, commands maintain backward compatibility. Your integration code continues working without changes.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Commands are available through the `@friggframework/core` package:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Basic Usage
|
|
37
|
+
|
|
38
|
+
### Initialize Commands
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
42
|
+
const MyIntegration = require('./MyIntegration');
|
|
43
|
+
|
|
44
|
+
// Create command set with your integration class
|
|
45
|
+
const commands = createFriggCommands({
|
|
46
|
+
integrationClass: MyIntegration
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Use Commands in Your Integration
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
class MyIntegration extends IntegrationBase {
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this.commands = createFriggCommands({
|
|
57
|
+
integrationClass: MyIntegration
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async hydrateFromExternalUser(externalUserId) {
|
|
62
|
+
// Find integration context by external entity ID
|
|
63
|
+
const result = await this.commands.findIntegrationContextByExternalEntityId(
|
|
64
|
+
externalUserId
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (result.error) {
|
|
68
|
+
return { error: result.error };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Hydrate integration with retrieved context
|
|
72
|
+
this.setIntegrationRecord(result.context);
|
|
73
|
+
return { record: this.record };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Use Commands in Use Cases
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
82
|
+
|
|
83
|
+
class AuthenticateUserUseCase {
|
|
84
|
+
constructor({ commands } = {}) {
|
|
85
|
+
// Accept injected commands for testing, or create default
|
|
86
|
+
this.commands = commands || createFriggCommands({
|
|
87
|
+
integrationClass: MyIntegration
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async execute({ appUserId, username, email }) {
|
|
92
|
+
// Find or create user
|
|
93
|
+
let user = await this.commands.findUserByAppUserId(appUserId);
|
|
94
|
+
|
|
95
|
+
if (!user) {
|
|
96
|
+
user = await this.commands.createUser({
|
|
97
|
+
appUserId,
|
|
98
|
+
username,
|
|
99
|
+
email
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return user;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Available Commands
|
|
109
|
+
|
|
110
|
+
### User Commands
|
|
111
|
+
|
|
112
|
+
Manage Frigg users (individuals or organizations using your integration).
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
// Create a new user
|
|
116
|
+
const user = await commands.createUser({
|
|
117
|
+
username: 'john@example.com',
|
|
118
|
+
email: 'john@example.com',
|
|
119
|
+
appUserId: 'external-user-123',
|
|
120
|
+
password: 'optional-password' // For password-based auth
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Find user by app-specific user ID
|
|
124
|
+
const user = await commands.findUserByAppUserId('external-user-123');
|
|
125
|
+
|
|
126
|
+
// Find user by username
|
|
127
|
+
const user = await commands.findUserByUsername('john@example.com');
|
|
128
|
+
|
|
129
|
+
// Find user by Frigg internal ID
|
|
130
|
+
const user = await commands.findUserById('frigg-user-id');
|
|
131
|
+
|
|
132
|
+
// Update user
|
|
133
|
+
const updatedUser = await commands.updateUser('frigg-user-id', {
|
|
134
|
+
email: 'newemail@example.com'
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Credential Commands
|
|
139
|
+
|
|
140
|
+
Manage OAuth tokens and API credentials.
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
// Create credential
|
|
144
|
+
const credential = await commands.createCredential({
|
|
145
|
+
userId: 'frigg-user-id',
|
|
146
|
+
externalId: 'oauth-user-id',
|
|
147
|
+
access_token: 'access_token_value',
|
|
148
|
+
refresh_token: 'refresh_token_value',
|
|
149
|
+
expires_at: new Date('2024-12-31'),
|
|
150
|
+
moduleName: 'asana',
|
|
151
|
+
authIsValid: true
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Find credential
|
|
155
|
+
const credential = await commands.findCredential({
|
|
156
|
+
userId: 'frigg-user-id',
|
|
157
|
+
moduleName: 'asana'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Update credential (e.g., after token refresh)
|
|
161
|
+
const updated = await commands.updateCredential('credential-id', {
|
|
162
|
+
access_token: 'new_access_token',
|
|
163
|
+
expires_at: new Date('2025-01-31')
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Delete credential
|
|
167
|
+
await commands.deleteCredential('credential-id');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Entity Commands
|
|
171
|
+
|
|
172
|
+
Manage module entities (connections to external services).
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
// Create entity
|
|
176
|
+
const entity = await commands.createEntity({
|
|
177
|
+
userId: 'frigg-user-id',
|
|
178
|
+
externalId: 'asana-workspace-123',
|
|
179
|
+
name: 'My Workspace',
|
|
180
|
+
moduleName: 'asana',
|
|
181
|
+
credentialId: 'credential-id'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Find single entity
|
|
185
|
+
const entity = await commands.findEntity({
|
|
186
|
+
userId: 'frigg-user-id',
|
|
187
|
+
externalId: 'asana-workspace-123',
|
|
188
|
+
moduleName: 'asana'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Find entity by ID
|
|
192
|
+
const entity = await commands.findEntityById('entity-id');
|
|
193
|
+
|
|
194
|
+
// Find all entities for user
|
|
195
|
+
const entities = await commands.findEntitiesByUserId('frigg-user-id');
|
|
196
|
+
|
|
197
|
+
// Find entities by module
|
|
198
|
+
const asanaEntities = await commands.findEntitiesByUserIdAndModuleName(
|
|
199
|
+
'frigg-user-id',
|
|
200
|
+
'asana'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Find multiple entities by IDs
|
|
204
|
+
const entities = await commands.findEntitiesByIds(['entity-id-1', 'entity-id-2']);
|
|
205
|
+
|
|
206
|
+
// Update entity
|
|
207
|
+
const updated = await commands.updateEntity('entity-id', {
|
|
208
|
+
name: 'Updated Workspace Name'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Delete entity
|
|
212
|
+
await commands.deleteEntity('entity-id');
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Integration Commands
|
|
216
|
+
|
|
217
|
+
Manage integration records and load full integration contexts.
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// Find integration context by external entity ID
|
|
221
|
+
// Returns { context, error } where context includes record + hydrated modules
|
|
222
|
+
const result = await commands.findIntegrationContextByExternalEntityId(
|
|
223
|
+
'external-user-or-workspace-id'
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (!result.error) {
|
|
227
|
+
integration.setIntegrationRecord(result.context);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Load integration context by integration ID
|
|
231
|
+
const result = await commands.loadIntegrationContextById('integration-id');
|
|
232
|
+
|
|
233
|
+
if (!result.error) {
|
|
234
|
+
integration.setIntegrationRecord(result.context);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Architecture Principles
|
|
239
|
+
|
|
240
|
+
### Dependency Injection for Testing
|
|
241
|
+
|
|
242
|
+
Commands support dependency injection for testing:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// Production code - uses real repositories
|
|
246
|
+
const commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
247
|
+
|
|
248
|
+
// Test code - inject mocks
|
|
249
|
+
const mockCommands = {
|
|
250
|
+
createUser: jest.fn().mockResolvedValue({ id: 'user-123' }),
|
|
251
|
+
findUserByAppUserId: jest.fn().mockResolvedValue(null)
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const useCase = new MyUseCase({ commands: mockCommands });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Integration vs Unit Testing
|
|
258
|
+
|
|
259
|
+
**Commands are designed for integration testing** - they use real repositories by default:
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
// ❌ Don't do this - commands always use real repositories
|
|
263
|
+
const commands = createFriggCommands({
|
|
264
|
+
userRepository: mockUserRepo // This parameter doesn't exist
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ✅ Do this - inject mocked commands into your use cases
|
|
268
|
+
const useCase = new MyUseCase({
|
|
269
|
+
commands: mockCommands
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Error Handling
|
|
274
|
+
|
|
275
|
+
Commands return domain objects directly. Handle errors at the use case level:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
try {
|
|
279
|
+
const user = await commands.createUser({ username, email });
|
|
280
|
+
return { success: true, user };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// Handle database errors
|
|
283
|
+
return { success: false, error: error.message };
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
For integration context operations, errors are returned in the result:
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
const result = await commands.findIntegrationContextByExternalEntityId(userId);
|
|
291
|
+
|
|
292
|
+
if (result.error) {
|
|
293
|
+
return { error: result.error };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Use result.context
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Migration Guide
|
|
300
|
+
|
|
301
|
+
### From Direct Model Access
|
|
302
|
+
|
|
303
|
+
**Before (❌ Don't do this):**
|
|
304
|
+
```javascript
|
|
305
|
+
const { User } = require('@friggframework/core');
|
|
306
|
+
|
|
307
|
+
const user = await User.findOne({ appUserId: '123' });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**After (✅ Do this):**
|
|
311
|
+
```javascript
|
|
312
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
313
|
+
|
|
314
|
+
const commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
315
|
+
const user = await commands.findUserByAppUserId('123');
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### From IntegrationRepository (Backend Pattern)
|
|
319
|
+
|
|
320
|
+
**Before (❌ Old pattern):**
|
|
321
|
+
```javascript
|
|
322
|
+
const { IntegrationRepository } = require('./repositories/IntegrationRepository');
|
|
323
|
+
|
|
324
|
+
this.integrationRepository = new IntegrationRepository(MyIntegration);
|
|
325
|
+
const result = await this.integrationRepository.loadIntegrationRecordByAsanaUser(userId);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**After (✅ New pattern):**
|
|
329
|
+
```javascript
|
|
330
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
331
|
+
|
|
332
|
+
this.commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
333
|
+
const result = await this.commands.findIntegrationContextByExternalEntityId(userId);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Best Practices
|
|
337
|
+
|
|
338
|
+
### 1. Create Commands Once
|
|
339
|
+
Initialize commands in your constructor:
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
class MyIntegration extends IntegrationBase {
|
|
343
|
+
constructor() {
|
|
344
|
+
super();
|
|
345
|
+
this.commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 2. Pass Commands to Use Cases
|
|
351
|
+
Use dependency injection for testability:
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
class MyUseCase {
|
|
355
|
+
constructor({ commands } = {}) {
|
|
356
|
+
this.commands = commands || createFriggCommands({
|
|
357
|
+
integrationClass: MyIntegration
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 3. Use Specific Finders
|
|
364
|
+
Use the most specific finder method:
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
// ✅ Good - specific finder
|
|
368
|
+
const user = await commands.findUserByAppUserId('123');
|
|
369
|
+
|
|
370
|
+
// ❌ Less efficient - generic finder
|
|
371
|
+
const user = await commands.findUser({ appUserId: '123' });
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 4. Handle Null Returns
|
|
375
|
+
Most finders return `null` if not found:
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
const user = await commands.findUserByAppUserId('123');
|
|
379
|
+
|
|
380
|
+
if (!user) {
|
|
381
|
+
// Handle user not found
|
|
382
|
+
user = await commands.createUser({ ... });
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Command Reference
|
|
387
|
+
|
|
388
|
+
| Category | Command | Description |
|
|
389
|
+
|----------|---------|-------------|
|
|
390
|
+
| **User** | `createUser(data)` | Create new Frigg user |
|
|
391
|
+
| | `findUserByAppUserId(appUserId)` | Find by external app user ID |
|
|
392
|
+
| | `findUserByUsername(username)` | Find by username |
|
|
393
|
+
| | `findUserById(id)` | Find by Frigg user ID |
|
|
394
|
+
| | `updateUser(id, updates)` | Update user properties |
|
|
395
|
+
| **Credential** | `createCredential(data)` | Create OAuth credential |
|
|
396
|
+
| | `findCredential(filter)` | Find credential by filter |
|
|
397
|
+
| | `updateCredential(id, updates)` | Update credential (token refresh) |
|
|
398
|
+
| | `deleteCredential(id)` | Delete credential |
|
|
399
|
+
| **Entity** | `createEntity(data)` | Create module entity |
|
|
400
|
+
| | `findEntity(filter)` | Find entity by filter |
|
|
401
|
+
| | `findEntityById(id)` | Find by entity ID |
|
|
402
|
+
| | `findEntitiesByUserId(userId)` | Find all user entities |
|
|
403
|
+
| | `findEntitiesByUserIdAndModuleName(userId, moduleName)` | Find user entities for module |
|
|
404
|
+
| | `findEntitiesByIds(ids)` | Find multiple by IDs |
|
|
405
|
+
| | `updateEntity(id, updates)` | Update entity properties |
|
|
406
|
+
| | `deleteEntity(id)` | Delete entity |
|
|
407
|
+
| **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
|
|
408
|
+
| | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
|
|
409
|
+
|
|
410
|
+
## Support
|
|
411
|
+
|
|
412
|
+
For questions or issues with commands:
|
|
413
|
+
1. Check this README
|
|
414
|
+
2. Review the main Frigg documentation
|
|
415
|
+
3. Open an issue on the Frigg Framework repository
|
|
416
|
+
|
|
417
|
+
## Related Documentation
|
|
418
|
+
|
|
419
|
+
- [Frigg Framework Overview](../../README.md)
|
|
420
|
+
- [Integration Development Guide](../../docs/integration-guide.md)
|
|
421
|
+
- [Hexagonal Architecture](../../docs/architecture.md)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createCredentialRepository,
|
|
3
|
+
} = require('../../credential/repositories/credential-repository-factory');
|
|
4
|
+
|
|
5
|
+
const ERROR_CODE_MAP = {
|
|
6
|
+
CREDENTIAL_NOT_FOUND: 404,
|
|
7
|
+
INVALID_CREDENTIAL_DATA: 400,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function mapErrorToResponse(error) {
|
|
11
|
+
const status = ERROR_CODE_MAP[error?.code] || 500;
|
|
12
|
+
return {
|
|
13
|
+
error: status,
|
|
14
|
+
reason: error?.message,
|
|
15
|
+
code: error?.code,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create credential command factory
|
|
21
|
+
*
|
|
22
|
+
* NOTE: This is an internal API. Integration developers should use createFriggCommands() instead.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Object} Credential command object with CRUD operations
|
|
25
|
+
*/
|
|
26
|
+
function createCredentialCommands() {
|
|
27
|
+
const credRepo = createCredentialRepository();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
/**
|
|
31
|
+
* Create a new credential
|
|
32
|
+
* @param {Object} params
|
|
33
|
+
* @param {string} params.userId - User ID who owns this credential
|
|
34
|
+
* @param {string} params.externalId - External identifier from the API module
|
|
35
|
+
* @param {string} params.access_token - OAuth access token
|
|
36
|
+
* @param {string} [params.refresh_token] - OAuth refresh token
|
|
37
|
+
* @param {string} [params.domain] - Domain for the credential
|
|
38
|
+
* @param {boolean} [params.authIsValid=true] - Whether authentication is valid
|
|
39
|
+
* @returns {Promise<Object>} Created credential object
|
|
40
|
+
*/
|
|
41
|
+
async createCredential({
|
|
42
|
+
userId,
|
|
43
|
+
externalId,
|
|
44
|
+
access_token,
|
|
45
|
+
refresh_token,
|
|
46
|
+
domain,
|
|
47
|
+
authIsValid = true,
|
|
48
|
+
} = {}) {
|
|
49
|
+
try {
|
|
50
|
+
if (!userId || !externalId || !access_token) {
|
|
51
|
+
const error = new Error(
|
|
52
|
+
'userId, externalId, and access_token are required'
|
|
53
|
+
);
|
|
54
|
+
error.code = 'INVALID_CREDENTIAL_DATA';
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const credentialData = {
|
|
59
|
+
identifiers: { user: userId, externalId },
|
|
60
|
+
details: {
|
|
61
|
+
access_token,
|
|
62
|
+
authIsValid,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (refresh_token) {
|
|
67
|
+
credentialData.details.refresh_token = refresh_token;
|
|
68
|
+
}
|
|
69
|
+
if (domain) {
|
|
70
|
+
credentialData.details.domain = domain;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const credential = await credRepo.upsertCredential(
|
|
74
|
+
credentialData
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: credential.id,
|
|
79
|
+
userId: credential.userId,
|
|
80
|
+
externalId: credential.externalId,
|
|
81
|
+
access_token: credential.access_token,
|
|
82
|
+
refresh_token: credential.refresh_token,
|
|
83
|
+
authIsValid: credential.authIsValid,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return mapErrorToResponse(error);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find a credential by filter criteria
|
|
92
|
+
* @param {Object} filter
|
|
93
|
+
* @param {string} [filter.userId] - User ID to search for
|
|
94
|
+
* @param {string} [filter.externalId] - External ID to search for
|
|
95
|
+
* @param {string} [filter.credentialId] - Credential ID to search for
|
|
96
|
+
* @returns {Promise<Object|null>} Credential object or null if not found
|
|
97
|
+
*/
|
|
98
|
+
async findCredential(filter = {}) {
|
|
99
|
+
try {
|
|
100
|
+
if (
|
|
101
|
+
!filter.userId &&
|
|
102
|
+
!filter.externalId &&
|
|
103
|
+
!filter.credentialId
|
|
104
|
+
) {
|
|
105
|
+
const error = new Error(
|
|
106
|
+
'At least one filter criterion is required'
|
|
107
|
+
);
|
|
108
|
+
error.code = 'INVALID_CREDENTIAL_DATA';
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const credential = await credRepo.findCredential(filter);
|
|
113
|
+
|
|
114
|
+
if (!credential) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: credential.id,
|
|
120
|
+
userId: credential.userId,
|
|
121
|
+
externalId: credential.externalId,
|
|
122
|
+
access_token: credential.access_token,
|
|
123
|
+
refresh_token: credential.refresh_token,
|
|
124
|
+
authIsValid: credential.authIsValid,
|
|
125
|
+
domain: credential.domain,
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return mapErrorToResponse(error);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Update a credential by ID
|
|
134
|
+
* @param {string} credentialId - Credential ID to update
|
|
135
|
+
* @param {Object} updates - Fields to update
|
|
136
|
+
* @returns {Promise<Object>} Updated credential object
|
|
137
|
+
*/
|
|
138
|
+
async updateCredential(credentialId, updates) {
|
|
139
|
+
try {
|
|
140
|
+
if (!credentialId) {
|
|
141
|
+
const error = new Error('credentialId is required');
|
|
142
|
+
error.code = 'INVALID_CREDENTIAL_DATA';
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const credential = await credRepo.updateCredential(
|
|
147
|
+
credentialId,
|
|
148
|
+
updates
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!credential) {
|
|
152
|
+
const error = new Error(
|
|
153
|
+
`Credential ${credentialId} not found`
|
|
154
|
+
);
|
|
155
|
+
error.code = 'CREDENTIAL_NOT_FOUND';
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: credential.id,
|
|
161
|
+
userId: credential.userId,
|
|
162
|
+
externalId: credential.externalId,
|
|
163
|
+
access_token: credential.access_token,
|
|
164
|
+
refresh_token: credential.refresh_token,
|
|
165
|
+
authIsValid: credential.authIsValid,
|
|
166
|
+
domain: credential.domain,
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return mapErrorToResponse(error);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update authentication status for a credential
|
|
175
|
+
* @param {string} credentialId - Credential ID to update
|
|
176
|
+
* @param {boolean} isValid - Whether authentication is valid
|
|
177
|
+
* @returns {Promise<Object>} Result object with success flag
|
|
178
|
+
*/
|
|
179
|
+
async updateAuthenticationStatus(credentialId, isValid) {
|
|
180
|
+
try {
|
|
181
|
+
if (!credentialId) {
|
|
182
|
+
const error = new Error('credentialId is required');
|
|
183
|
+
error.code = 'INVALID_CREDENTIAL_DATA';
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await credRepo.updateAuthenticationStatus(
|
|
188
|
+
credentialId,
|
|
189
|
+
isValid
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return { success: true };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return mapErrorToResponse(error);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Delete a credential by ID
|
|
200
|
+
* @param {string} credentialId - Credential ID to delete
|
|
201
|
+
* @returns {Promise<Object>} Result object with success flag
|
|
202
|
+
*/
|
|
203
|
+
async deleteCredential(credentialId) {
|
|
204
|
+
try {
|
|
205
|
+
if (!credentialId) {
|
|
206
|
+
const error = new Error('credentialId is required');
|
|
207
|
+
error.code = 'INVALID_CREDENTIAL_DATA';
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await credRepo.deleteCredentialById(credentialId);
|
|
212
|
+
|
|
213
|
+
return { success: true };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return mapErrorToResponse(error);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
createCredentialCommands,
|
|
223
|
+
ERROR_CODE_MAP,
|
|
224
|
+
};
|