@friggframework/core 2.0.0-next.40 → 2.0.0-next.42
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 +931 -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 +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- 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 +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- 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 +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -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/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -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/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -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 +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -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 +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -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 +14 -6
- package/prisma-mongodb/schema.prisma +321 -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/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- 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/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- 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 +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -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-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -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 +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/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/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}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
package/README.md
CHANGED
|
@@ -1,83 +1,964 @@
|
|
|
1
1
|
# Frigg Core
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
3
|
+
The `@friggframework/core` package is the foundational layer of the Frigg Framework, implementing a hexagonal architecture pattern for building scalable, maintainable enterprise integrations. It provides the essential building blocks, domain logic, and infrastructure components that power the entire Frigg ecosystem.
|
|
5
4
|
|
|
6
5
|
## Table of Contents
|
|
7
6
|
|
|
8
|
-
- [
|
|
9
|
-
- [Features](#features)
|
|
7
|
+
- [Architecture Overview](#architecture-overview)
|
|
10
8
|
- [Installation](#installation)
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Core Components](#core-components)
|
|
11
|
+
- [Hexagonal Architecture](#hexagonal-architecture)
|
|
12
|
+
- [Usage Examples](#usage-examples)
|
|
13
|
+
- [Testing](#testing)
|
|
14
|
+
- [Development](#development)
|
|
15
|
+
- [API Reference](#api-reference)
|
|
13
16
|
- [Contributing](#contributing)
|
|
14
|
-
- [License](#license)
|
|
15
|
-
|
|
16
|
-
## Introduction
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## Architecture Overview
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Frigg Core implements a **hexagonal architecture** (also known as ports and adapters) that separates business logic from external concerns:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Inbound Adapters │
|
|
25
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
26
|
+
│ │ Express │ │ Lambda │ │ WebSocket │ │
|
|
27
|
+
│ │ Routes │ │ Handlers │ │ Handlers │ │
|
|
28
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
29
|
+
└─────────────────────────────────────────────────────────────┘
|
|
30
|
+
│
|
|
31
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
32
|
+
│ Application Layer │
|
|
33
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
34
|
+
│ │ Use Cases │ │ Services │ │ Coordinators│ │
|
|
35
|
+
│ │ (Business │ │ │ │ │ │
|
|
36
|
+
│ │ Logic) │ │ │ │ │ │
|
|
37
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
38
|
+
└─────────────────────────────────────────────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
41
|
+
│ Domain Layer │
|
|
42
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
43
|
+
│ │ Integration │ │ Entities │ │ Value │ │
|
|
44
|
+
│ │ Aggregates │ │ │ │ Objects │ │
|
|
45
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
46
|
+
└─────────────────────────────────────────────────────────────┘
|
|
47
|
+
│
|
|
48
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
49
|
+
│ Outbound Adapters │
|
|
50
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
51
|
+
│ │ Database │ │ API Modules │ │ Event │ │
|
|
52
|
+
│ │ Repositories│ │ │ │ Publishers │ │
|
|
53
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
54
|
+
└─────────────────────────────────────────────────────────────┘
|
|
55
|
+
```
|
|
32
56
|
|
|
33
57
|
## Installation
|
|
34
58
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```sh
|
|
59
|
+
```bash
|
|
38
60
|
npm install @friggframework/core
|
|
39
61
|
# or
|
|
40
62
|
yarn add @friggframework/core
|
|
41
63
|
```
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
|
|
65
|
+
### Prerequisites
|
|
66
|
+
|
|
67
|
+
- Node.js 16+
|
|
68
|
+
- MongoDB 4.4+ (for data persistence)
|
|
69
|
+
- AWS credentials (for SQS, KMS, Lambda deployment)
|
|
70
|
+
|
|
71
|
+
### Environment Variables
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Database
|
|
75
|
+
MONGO_URI=mongodb://localhost:27017/frigg
|
|
76
|
+
FRIGG_ENCRYPTION_KEY=your-256-bit-encryption-key
|
|
77
|
+
|
|
78
|
+
# AWS (Optional - for production deployments)
|
|
79
|
+
AWS_REGION=us-east-1
|
|
80
|
+
AWS_ACCESS_KEY_ID=your-access-key
|
|
81
|
+
AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
82
|
+
|
|
83
|
+
# Logging
|
|
84
|
+
DEBUG=frigg:*
|
|
85
|
+
LOG_LEVEL=info
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Core Components
|
|
89
|
+
|
|
90
|
+
### 1. Integrations (`/integrations`)
|
|
91
|
+
|
|
92
|
+
The heart of the framework - manages integration lifecycle and business logic.
|
|
93
|
+
|
|
94
|
+
**Key Classes:**
|
|
95
|
+
- `IntegrationBase` - Base class for all integrations
|
|
96
|
+
- `Integration` - Domain aggregate using Proxy pattern
|
|
97
|
+
- Use cases: `CreateIntegration`, `UpdateIntegration`, `DeleteIntegration`
|
|
98
|
+
|
|
99
|
+
**Usage:**
|
|
44
100
|
```javascript
|
|
45
|
-
const {
|
|
46
|
-
const { logInfo } = require('@friggframework/core/logs');
|
|
101
|
+
const { IntegrationBase } = require('@friggframework/core');
|
|
47
102
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
103
|
+
class SlackHubSpotSync extends IntegrationBase {
|
|
104
|
+
static Definition = {
|
|
105
|
+
name: 'slack-hubspot-sync',
|
|
106
|
+
version: '2.1.0',
|
|
107
|
+
modules: {
|
|
108
|
+
slack: 'slack',
|
|
109
|
+
hubspot: 'hubspot'
|
|
110
|
+
}
|
|
111
|
+
};
|
|
51
112
|
|
|
52
|
-
|
|
53
|
-
|
|
113
|
+
async onCreate({ integrationId }) {
|
|
114
|
+
// Setup webhooks, initial sync, etc.
|
|
115
|
+
await this.slack.createWebhook(process.env.WEBHOOK_URL);
|
|
116
|
+
await this.hubspot.setupContactSync();
|
|
117
|
+
await super.onCreate({ integrationId });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
54
120
|
```
|
|
55
121
|
|
|
56
|
-
|
|
122
|
+
### 3. Database (`/database`)
|
|
123
|
+
|
|
124
|
+
MongoDB integration with Mongoose ODM.
|
|
57
125
|
|
|
58
|
-
|
|
126
|
+
**Key Components:**
|
|
127
|
+
- Connection management
|
|
128
|
+
- Pre-built models (User, Integration, Credential, etc.)
|
|
129
|
+
- Schema definitions
|
|
130
|
+
|
|
131
|
+
**Usage:**
|
|
132
|
+
```javascript
|
|
133
|
+
const {
|
|
134
|
+
connectToDatabase,
|
|
135
|
+
IntegrationModel,
|
|
136
|
+
UserModel
|
|
137
|
+
} = require('@friggframework/core');
|
|
138
|
+
|
|
139
|
+
await connectToDatabase();
|
|
140
|
+
|
|
141
|
+
// Query integrations
|
|
142
|
+
const userIntegrations = await IntegrationModel.find({
|
|
143
|
+
userId: 'user-123',
|
|
144
|
+
status: 'ENABLED'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Create user
|
|
148
|
+
const user = new UserModel({
|
|
149
|
+
email: 'user@example.com',
|
|
150
|
+
name: 'John Doe'
|
|
151
|
+
});
|
|
152
|
+
await user.save();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 4. Encryption (`/encrypt`)
|
|
156
|
+
|
|
157
|
+
AES-256-GCM encryption for sensitive data.
|
|
158
|
+
|
|
159
|
+
**Usage:**
|
|
160
|
+
```javascript
|
|
161
|
+
const { Encrypt, Cryptor } = require('@friggframework/core');
|
|
162
|
+
|
|
163
|
+
// Simple encryption
|
|
164
|
+
const encrypted = Encrypt.encrypt('sensitive-data');
|
|
165
|
+
const decrypted = Encrypt.decrypt(encrypted);
|
|
166
|
+
|
|
167
|
+
// Advanced encryption with custom key
|
|
168
|
+
const cryptor = new Cryptor(process.env.CUSTOM_KEY);
|
|
169
|
+
const secureData = cryptor.encrypt(JSON.stringify({
|
|
170
|
+
accessToken: 'oauth-token',
|
|
171
|
+
refreshToken: 'refresh-token'
|
|
172
|
+
}));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 5. Error Handling (`/errors`)
|
|
176
|
+
|
|
177
|
+
Standardized error types with proper HTTP status codes.
|
|
178
|
+
|
|
179
|
+
**Usage:**
|
|
180
|
+
```javascript
|
|
181
|
+
const {
|
|
182
|
+
BaseError,
|
|
183
|
+
RequiredPropertyError,
|
|
184
|
+
FetchError
|
|
185
|
+
} = require('@friggframework/core');
|
|
186
|
+
|
|
187
|
+
// Custom business logic error
|
|
188
|
+
throw new RequiredPropertyError('userId is required');
|
|
189
|
+
|
|
190
|
+
// API communication error
|
|
191
|
+
throw new FetchError('Failed to fetch data from external API', {
|
|
192
|
+
statusCode: 404,
|
|
193
|
+
response: errorResponse
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Base error with custom properties
|
|
197
|
+
throw new BaseError('Integration failed', {
|
|
198
|
+
integrationId: 'int-123',
|
|
199
|
+
errorCode: 'SYNC_FAILED'
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 6. Logging (`/logs`)
|
|
204
|
+
|
|
205
|
+
Structured logging with debug capabilities.
|
|
206
|
+
|
|
207
|
+
**Usage:**
|
|
208
|
+
```javascript
|
|
209
|
+
const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
|
|
210
|
+
|
|
211
|
+
// Initialize debug logging
|
|
212
|
+
initDebugLog('integration:slack');
|
|
213
|
+
|
|
214
|
+
// Log debug information
|
|
215
|
+
debug('Processing webhook payload', {
|
|
216
|
+
eventType: 'contact.created',
|
|
217
|
+
payload: webhookData
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Flush logs (useful in serverless environments)
|
|
221
|
+
await flushDebugLog();
|
|
222
|
+
```
|
|
59
223
|
|
|
60
|
-
|
|
61
|
-
- **Database**: @friggframework/core/database
|
|
62
|
-
- **Encryption**: @friggframework/core/encrypt
|
|
63
|
-
- **Errors**: @friggframework/core/errors
|
|
64
|
-
- **Integrations**: @friggframework/core/integrations
|
|
65
|
-
- **Lambda**: @friggframework/core/lambda
|
|
66
|
-
- **Logs**: @friggframework/core/logs
|
|
67
|
-
- **Module Plugin**: @friggframework/core/module-plugin
|
|
68
|
-
- **Syncs**: @friggframework/core/syncs
|
|
69
|
-
- **Infrastructure**: @friggframework/core/infrastructure
|
|
224
|
+
### 7. User Management (`/user`)
|
|
70
225
|
|
|
226
|
+
Comprehensive user authentication and authorization system supporting both individual and organizational users.
|
|
71
227
|
|
|
72
|
-
|
|
228
|
+
**Key Classes:**
|
|
229
|
+
- `User` - Domain aggregate for user entities
|
|
230
|
+
- `UserRepository` - Data access for user operations
|
|
231
|
+
- Use cases: `LoginUser`, `CreateIndividualUser`, `CreateOrganizationUser`, `GetUserFromBearerToken`
|
|
73
232
|
|
|
74
|
-
|
|
233
|
+
**User Types:**
|
|
234
|
+
- **Individual Users**: Personal accounts with email/username authentication
|
|
235
|
+
- **Organization Users**: Business accounts with organization-level access
|
|
236
|
+
- **Hybrid Mode**: Support for both user types simultaneously
|
|
237
|
+
|
|
238
|
+
**Authentication Methods:**
|
|
239
|
+
- **Password-based**: Traditional username/password authentication
|
|
240
|
+
- **Token-based**: Bearer token authentication with session management
|
|
241
|
+
- **App-based**: External app user ID authentication (passwordless)
|
|
242
|
+
|
|
243
|
+
**Usage:**
|
|
244
|
+
```javascript
|
|
245
|
+
const {
|
|
246
|
+
LoginUser,
|
|
247
|
+
CreateIndividualUser,
|
|
248
|
+
GetUserFromBearerToken,
|
|
249
|
+
UserRepository
|
|
250
|
+
} = require('@friggframework/core');
|
|
251
|
+
|
|
252
|
+
// Configure user behavior in app definition
|
|
253
|
+
const userConfig = {
|
|
254
|
+
usePassword: true,
|
|
255
|
+
primary: 'individual', // or 'organization'
|
|
256
|
+
individualUserRequired: true,
|
|
257
|
+
organizationUserRequired: false
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const userRepository = new UserRepository({ userConfig });
|
|
261
|
+
|
|
262
|
+
// Create individual user
|
|
263
|
+
const createUser = new CreateIndividualUser({ userRepository, userConfig });
|
|
264
|
+
const user = await createUser.execute({
|
|
265
|
+
email: 'user@example.com',
|
|
266
|
+
username: 'john_doe',
|
|
267
|
+
password: 'secure_password',
|
|
268
|
+
appUserId: 'external_user_123' // Optional external reference
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Login user
|
|
272
|
+
const loginUser = new LoginUser({ userRepository, userConfig });
|
|
273
|
+
const authenticatedUser = await loginUser.execute({
|
|
274
|
+
username: 'john_doe',
|
|
275
|
+
password: 'secure_password'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Token-based authentication
|
|
279
|
+
const getUserFromToken = new GetUserFromBearerToken({ userRepository, userConfig });
|
|
280
|
+
const user = await getUserFromToken.execute('Bearer eyJhbGciOiJIUzI1NiIs...');
|
|
281
|
+
|
|
282
|
+
// Access user properties
|
|
283
|
+
console.log('User ID:', user.getId());
|
|
284
|
+
console.log('Primary user:', user.getPrimaryUser());
|
|
285
|
+
console.log('Individual user:', user.getIndividualUser());
|
|
286
|
+
console.log('Organization user:', user.getOrganizationUser());
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 8. Lambda Utilities (`/lambda`)
|
|
290
|
+
|
|
291
|
+
AWS Lambda-specific utilities and helpers.
|
|
292
|
+
|
|
293
|
+
**Usage:**
|
|
294
|
+
```javascript
|
|
295
|
+
const { TimeoutCatcher } = require('@friggframework/core');
|
|
296
|
+
|
|
297
|
+
exports.handler = async (event, context) => {
|
|
298
|
+
const timeoutCatcher = new TimeoutCatcher(context);
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Long-running integration process
|
|
302
|
+
const result = await processIntegrationSync(event);
|
|
303
|
+
return { statusCode: 200, body: JSON.stringify(result) };
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (timeoutCatcher.isNearTimeout()) {
|
|
306
|
+
// Handle graceful shutdown
|
|
307
|
+
await saveProgressState(event);
|
|
308
|
+
return { statusCode: 202, body: 'Processing continues...' };
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## User Management & Behavior
|
|
316
|
+
|
|
317
|
+
Frigg Core provides a flexible user management system that supports various authentication patterns and user types. The system is designed around the concept of **Individual Users** (personal accounts) and **Organization Users** (business accounts), with configurable authentication methods.
|
|
318
|
+
|
|
319
|
+
### User Configuration
|
|
320
|
+
|
|
321
|
+
User behavior is configured in the app definition, allowing you to customize authentication requirements:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
// App Definition with User Configuration
|
|
325
|
+
const appDefinition = {
|
|
326
|
+
integrations: [HubSpotIntegration],
|
|
327
|
+
user: {
|
|
328
|
+
usePassword: true, // Enable password authentication
|
|
329
|
+
primary: 'individual', // Primary user type: 'individual' or 'organization'
|
|
330
|
+
organizationUserRequired: true, // Require organization user
|
|
331
|
+
individualUserRequired: true, // Require individual user
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### User Domain Model
|
|
337
|
+
|
|
338
|
+
The `User` class provides a rich domain model with behavior:
|
|
339
|
+
|
|
340
|
+
```javascript
|
|
341
|
+
const { User } = require('@friggframework/core');
|
|
342
|
+
|
|
343
|
+
// User instance methods
|
|
344
|
+
const user = new User(individualUser, organizationUser, usePassword, primary);
|
|
345
|
+
|
|
346
|
+
// Access methods
|
|
347
|
+
user.getId() // Get primary user ID
|
|
348
|
+
user.getPrimaryUser() // Get primary user based on config
|
|
349
|
+
user.getIndividualUser() // Get individual user
|
|
350
|
+
user.getOrganizationUser() // Get organization user
|
|
351
|
+
|
|
352
|
+
// Validation methods
|
|
353
|
+
user.isPasswordRequired() // Check if password is required
|
|
354
|
+
user.isPasswordValid(password) // Validate password
|
|
355
|
+
user.isIndividualUserRequired() // Check individual user requirement
|
|
356
|
+
user.isOrganizationUserRequired() // Check organization user requirement
|
|
357
|
+
|
|
358
|
+
// Configuration methods
|
|
359
|
+
user.setIndividualUser(individualUser)
|
|
360
|
+
user.setOrganizationUser(organizationUser)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Database Models
|
|
364
|
+
|
|
365
|
+
The user system uses MongoDB with Mongoose for data persistence:
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
// Individual User Schema
|
|
369
|
+
{
|
|
370
|
+
email: String,
|
|
371
|
+
username: { type: String, unique: true },
|
|
372
|
+
hashword: String, // Encrypted password
|
|
373
|
+
appUserId: String, // External app reference
|
|
374
|
+
organizationUser: ObjectId // Reference to organization
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Organization User Schema
|
|
378
|
+
{
|
|
379
|
+
name: String,
|
|
380
|
+
appOrgId: String, // External organization reference
|
|
381
|
+
domain: String,
|
|
382
|
+
settings: Object
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Session Token Schema
|
|
386
|
+
{
|
|
387
|
+
user: ObjectId, // Reference to user
|
|
388
|
+
token: String, // Encrypted token
|
|
389
|
+
expires: Date,
|
|
390
|
+
created: Date
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Security Features
|
|
395
|
+
|
|
396
|
+
- **Password Hashing**: Uses bcrypt with configurable salt rounds
|
|
397
|
+
- **Token Management**: Secure session tokens with expiration
|
|
398
|
+
- **Unique Constraints**: Enforced username and email uniqueness
|
|
399
|
+
- **External References**: Support for external app user/org IDs
|
|
400
|
+
- **Flexible Authentication**: Multiple authentication methods
|
|
401
|
+
|
|
402
|
+
## Hexagonal Architecture
|
|
403
|
+
|
|
404
|
+
### Use Case Pattern
|
|
405
|
+
|
|
406
|
+
Each business operation is encapsulated in a use case class:
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
class UpdateIntegrationStatus {
|
|
410
|
+
constructor({ integrationRepository }) {
|
|
411
|
+
this.integrationRepository = integrationRepository;
|
|
412
|
+
}
|
|
75
413
|
|
|
76
|
-
|
|
414
|
+
async execute(integrationId, newStatus) {
|
|
415
|
+
// Business logic validation
|
|
416
|
+
if (!['ENABLED', 'DISABLED', 'ERROR'].includes(newStatus)) {
|
|
417
|
+
throw new Error('Invalid status');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Domain operation
|
|
421
|
+
const integration = await this.integrationRepository.findById(integrationId);
|
|
422
|
+
if (!integration) {
|
|
423
|
+
throw new Error('Integration not found');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Update and persist
|
|
427
|
+
integration.status = newStatus;
|
|
428
|
+
integration.updatedAt = new Date();
|
|
429
|
+
|
|
430
|
+
return await this.integrationRepository.save(integration);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Repository Pattern
|
|
436
|
+
|
|
437
|
+
Data access is abstracted through repositories:
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
class IntegrationRepository {
|
|
441
|
+
async findById(id) {
|
|
442
|
+
return await IntegrationModel.findById(id);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async findByUserId(userId) {
|
|
446
|
+
return await IntegrationModel.find({ userId, deletedAt: null });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async save(integration) {
|
|
450
|
+
return await integration.save();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async createIntegration(entities, userId, config) {
|
|
454
|
+
const integration = new IntegrationModel({
|
|
455
|
+
entitiesIds: entities,
|
|
456
|
+
userId,
|
|
457
|
+
config,
|
|
458
|
+
status: 'NEW',
|
|
459
|
+
createdAt: new Date()
|
|
460
|
+
});
|
|
461
|
+
return await integration.save();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Domain Aggregates
|
|
467
|
+
|
|
468
|
+
Complex business objects with behavior:
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
const Integration = new Proxy(class {}, {
|
|
472
|
+
construct(target, args) {
|
|
473
|
+
const [params] = args;
|
|
474
|
+
const instance = new params.integrationClass(params);
|
|
475
|
+
|
|
476
|
+
// Attach domain properties
|
|
477
|
+
Object.assign(instance, {
|
|
478
|
+
id: params.id,
|
|
479
|
+
userId: params.userId,
|
|
480
|
+
entities: params.entities,
|
|
481
|
+
config: params.config,
|
|
482
|
+
status: params.status,
|
|
483
|
+
modules: params.modules
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return instance;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Usage Examples
|
|
492
|
+
|
|
493
|
+
### Real-World HubSpot Integration Example
|
|
494
|
+
|
|
495
|
+
Here's a complete, production-ready HubSpot integration that demonstrates advanced Frigg features:
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
const {
|
|
499
|
+
get,
|
|
500
|
+
IntegrationBase,
|
|
501
|
+
WebsocketConnection,
|
|
502
|
+
} = require('@friggframework/core');
|
|
503
|
+
const FriggConstants = require('../utils/constants');
|
|
504
|
+
const hubspot = require('@friggframework/api-module-hubspot');
|
|
505
|
+
const testRouter = require('../testRouter');
|
|
506
|
+
const extensions = require('../extensions');
|
|
507
|
+
|
|
508
|
+
class HubSpotIntegration extends IntegrationBase {
|
|
509
|
+
static Definition = {
|
|
510
|
+
name: 'hubspot',
|
|
511
|
+
version: '1.0.0',
|
|
512
|
+
supportedVersions: ['1.0.0'],
|
|
513
|
+
hasUserConfig: true,
|
|
514
|
+
|
|
515
|
+
display: {
|
|
516
|
+
label: 'HubSpot',
|
|
517
|
+
description: hubspot.Config.description,
|
|
518
|
+
category: 'Sales & CRM, Marketing',
|
|
519
|
+
detailsUrl: 'https://hubspot.com',
|
|
520
|
+
icon: hubspot.Config.logoUrl,
|
|
521
|
+
},
|
|
522
|
+
modules: {
|
|
523
|
+
hubspot: {
|
|
524
|
+
definition: hubspot.Definition,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
// Express routes for webhook endpoints and custom APIs
|
|
528
|
+
routes: [
|
|
529
|
+
{
|
|
530
|
+
path: '/hubspot/webhooks',
|
|
531
|
+
method: 'POST',
|
|
532
|
+
event: 'HUBSPOT_WEBHOOK',
|
|
533
|
+
},
|
|
534
|
+
testRouter,
|
|
535
|
+
],
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
constructor() {
|
|
539
|
+
super();
|
|
540
|
+
|
|
541
|
+
// Define event handlers for various integration actions
|
|
542
|
+
this.events = {
|
|
543
|
+
// Webhook handler with real-time WebSocket broadcasting
|
|
544
|
+
HUBSPOT_WEBHOOK: {
|
|
545
|
+
handler: async ({ data, context }) => {
|
|
546
|
+
console.log('Received HubSpot webhook:', data);
|
|
547
|
+
|
|
548
|
+
// Broadcast to all connected WebSocket clients
|
|
549
|
+
const activeConnections = await WebsocketConnection.getActiveConnections();
|
|
550
|
+
const message = JSON.stringify({
|
|
551
|
+
type: 'HUBSPOT_WEBHOOK',
|
|
552
|
+
data,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
activeConnections.forEach((connection) => {
|
|
556
|
+
connection.send(message);
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
// User action: Get sample data with formatted table output
|
|
562
|
+
[FriggConstants.defaultEvents.GET_SAMPLE_DATA]: {
|
|
563
|
+
type: FriggConstants.eventTypes.USER_ACTION,
|
|
564
|
+
handler: this.getSampleData,
|
|
565
|
+
title: 'Get Sample Data',
|
|
566
|
+
description: 'Get sample data from HubSpot and display in a formatted table',
|
|
567
|
+
userActionType: 'QUICK_ACTION',
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// User action: List available objects
|
|
571
|
+
GET_OBJECT_LIST: {
|
|
572
|
+
type: FriggConstants.eventTypes.USER_ACTION,
|
|
573
|
+
handler: this.getObjectList,
|
|
574
|
+
title: 'Get Object List',
|
|
575
|
+
description: 'Get list of available HubSpot objects',
|
|
576
|
+
userActionType: 'DATA',
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
// User action: Create records with dynamic forms
|
|
580
|
+
CREATE_RECORD: {
|
|
581
|
+
type: FriggConstants.eventTypes.USER_ACTION,
|
|
582
|
+
handler: this.createRecord,
|
|
583
|
+
title: 'Create Record',
|
|
584
|
+
description: 'Create a new record in HubSpot',
|
|
585
|
+
userActionType: 'DATA',
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Extension system for modular functionality
|
|
590
|
+
this.extensions = {
|
|
591
|
+
hubspotWebhooks: {
|
|
592
|
+
extension: extensions.hubspotWebhooks,
|
|
593
|
+
handlers: {
|
|
594
|
+
WEBHOOK_EVENT: this.handleWebhookEvent,
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Business logic: Fetch and format sample data
|
|
601
|
+
async getSampleData({ objectName }) {
|
|
602
|
+
let res;
|
|
603
|
+
switch (objectName) {
|
|
604
|
+
case 'deals':
|
|
605
|
+
res = await this.hubspot.api.searchDeals({
|
|
606
|
+
properties: ['dealname,amount,closedate'],
|
|
607
|
+
});
|
|
608
|
+
break;
|
|
609
|
+
case 'contacts':
|
|
610
|
+
res = await this.hubspot.api.listContacts({
|
|
611
|
+
after: 0,
|
|
612
|
+
properties: 'firstname,lastname,email',
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
case 'companies':
|
|
616
|
+
res = await this.hubspot.api.searchCompanies({
|
|
617
|
+
properties: ['name,website,email'],
|
|
618
|
+
limit: 100,
|
|
619
|
+
});
|
|
620
|
+
break;
|
|
621
|
+
default:
|
|
622
|
+
throw new Error(`Unsupported object type: ${objectName}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const portalId = this.hubspot.entity.externalId;
|
|
626
|
+
|
|
627
|
+
// Format data with HubSpot record links
|
|
628
|
+
const formatted = res.results.map((item) => {
|
|
629
|
+
const formattedItem = {
|
|
630
|
+
linkToRecord: `https://app.hubspot.com/contacts/${portalId}/${objectName}/${item.id}/`,
|
|
631
|
+
id: item.id,
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Clean and format properties
|
|
635
|
+
for (const [key, value] of Object.entries(item.properties)) {
|
|
636
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
637
|
+
formattedItem[key] = value;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
delete formattedItem.hs_object_id;
|
|
641
|
+
|
|
642
|
+
return formattedItem;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return { label: objectName, data: formatted };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Return available HubSpot object types
|
|
649
|
+
async getObjectList() {
|
|
650
|
+
return [
|
|
651
|
+
{ key: 'deals', label: 'Deals' },
|
|
652
|
+
{ key: 'contacts', label: 'Contacts' },
|
|
653
|
+
{ key: 'companies', label: 'Companies' },
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Create records based on object type
|
|
658
|
+
async createRecord(args) {
|
|
659
|
+
let res;
|
|
660
|
+
const objectType = args.objectType;
|
|
661
|
+
delete args.objectType;
|
|
662
|
+
|
|
663
|
+
switch (objectType.toLowerCase()) {
|
|
664
|
+
case 'deal':
|
|
665
|
+
res = await this.hubspot.api.createDeal({ ...args });
|
|
666
|
+
break;
|
|
667
|
+
case 'company':
|
|
668
|
+
res = await this.hubspot.api.createCompany({ ...args });
|
|
669
|
+
break;
|
|
670
|
+
case 'contact':
|
|
671
|
+
res = await this.hubspot.api.createContact({ ...args });
|
|
672
|
+
break;
|
|
673
|
+
default:
|
|
674
|
+
throw new Error(`Unsupported object type: ${objectType}`);
|
|
675
|
+
}
|
|
676
|
+
return { data: res };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Dynamic form generation based on action and context
|
|
680
|
+
async getActionOptions({ actionId, data }) {
|
|
681
|
+
switch (actionId) {
|
|
682
|
+
case 'CREATE_RECORD':
|
|
683
|
+
let jsonSchema = {
|
|
684
|
+
type: 'object',
|
|
685
|
+
properties: {
|
|
686
|
+
objectType: {
|
|
687
|
+
type: 'string',
|
|
688
|
+
title: 'Object Type',
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
required: [],
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
let uiSchema = {
|
|
695
|
+
type: 'HorizontalLayout',
|
|
696
|
+
elements: [
|
|
697
|
+
{
|
|
698
|
+
type: 'Control',
|
|
699
|
+
scope: '#/properties/objectType',
|
|
700
|
+
rule: { effect: 'HIDE', condition: {} },
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// Generate form fields based on object type
|
|
706
|
+
switch (data.name.toLowerCase()) {
|
|
707
|
+
case 'deal':
|
|
708
|
+
jsonSchema.properties = {
|
|
709
|
+
...jsonSchema.properties,
|
|
710
|
+
dealname: { type: 'string', title: 'Deal Name' },
|
|
711
|
+
amount: { type: 'number', title: 'Amount' },
|
|
712
|
+
};
|
|
713
|
+
jsonSchema.required = ['dealname', 'amount'];
|
|
714
|
+
uiSchema.elements.push(
|
|
715
|
+
{ type: 'Control', scope: '#/properties/dealname' },
|
|
716
|
+
{ type: 'Control', scope: '#/properties/amount' }
|
|
717
|
+
);
|
|
718
|
+
break;
|
|
719
|
+
|
|
720
|
+
case 'company':
|
|
721
|
+
jsonSchema.properties = {
|
|
722
|
+
...jsonSchema.properties,
|
|
723
|
+
name: { type: 'string', title: 'Company Name' },
|
|
724
|
+
website: { type: 'string', title: 'Website URL' },
|
|
725
|
+
};
|
|
726
|
+
jsonSchema.required = ['name', 'website'];
|
|
727
|
+
uiSchema.elements.push(
|
|
728
|
+
{ type: 'Control', scope: '#/properties/name' },
|
|
729
|
+
{ type: 'Control', scope: '#/properties/website' }
|
|
730
|
+
);
|
|
731
|
+
break;
|
|
732
|
+
|
|
733
|
+
case 'contact':
|
|
734
|
+
jsonSchema.properties = {
|
|
735
|
+
...jsonSchema.properties,
|
|
736
|
+
firstname: { type: 'string', title: 'First Name' },
|
|
737
|
+
lastname: { type: 'string', title: 'Last Name' },
|
|
738
|
+
email: { type: 'string', title: 'Email Address' },
|
|
739
|
+
};
|
|
740
|
+
jsonSchema.required = ['firstname', 'lastname', 'email'];
|
|
741
|
+
uiSchema.elements.push(
|
|
742
|
+
{ type: 'Control', scope: '#/properties/firstname' },
|
|
743
|
+
{ type: 'Control', scope: '#/properties/lastname' },
|
|
744
|
+
{ type: 'Control', scope: '#/properties/email' }
|
|
745
|
+
);
|
|
746
|
+
break;
|
|
747
|
+
|
|
748
|
+
default:
|
|
749
|
+
throw new Error(`Unsupported object type: ${data.name}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
jsonSchema,
|
|
754
|
+
uiSchema,
|
|
755
|
+
data: { objectType: data.name },
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async getConfigOptions() {
|
|
762
|
+
// Return configuration options for the integration
|
|
763
|
+
return {};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
module.exports = HubSpotIntegration;
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
index.js
|
|
771
|
+
```js
|
|
772
|
+
const HubSpotIntegration = require('./src/integrations/HubSpotIntegration');
|
|
773
|
+
|
|
774
|
+
const appDefinition = {
|
|
775
|
+
integrations: [
|
|
776
|
+
HubSpotIntegration,
|
|
777
|
+
],
|
|
778
|
+
user: {
|
|
779
|
+
usePassword: true,
|
|
780
|
+
primary: 'individual',
|
|
781
|
+
organizationUserRequired: true,
|
|
782
|
+
individualUserRequired: true,
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
module.exports = {
|
|
787
|
+
Definition: appDefinition,
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
### Key Features Demonstrated
|
|
794
|
+
|
|
795
|
+
This real-world example showcases:
|
|
796
|
+
|
|
797
|
+
**🔄 Webhook Integration**: Real-time event processing with WebSocket broadcasting
|
|
798
|
+
**📊 User Actions**: Interactive data operations with dynamic form generation
|
|
799
|
+
**🎯 API Module Integration**: Direct use of `@friggframework/api-module-hubspot`
|
|
800
|
+
**🛠 Extension System**: Modular functionality through extensions
|
|
801
|
+
**📝 Dynamic Forms**: JSON Schema-based form generation for different object types
|
|
802
|
+
**🔗 Deep Linking**: Direct links to HubSpot records in formatted data
|
|
803
|
+
**⚡ Real-time Updates**: WebSocket connections for live data streaming
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
## Testing
|
|
807
|
+
|
|
808
|
+
### Running Tests
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
# Run all tests
|
|
812
|
+
npm test
|
|
813
|
+
|
|
814
|
+
# Run specific test file
|
|
815
|
+
npm test -- --testPathPattern="integration.test.js"
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Test Structure
|
|
819
|
+
|
|
820
|
+
The core package uses a comprehensive testing approach:
|
|
821
|
+
|
|
822
|
+
```javascript
|
|
823
|
+
// Example test structure
|
|
824
|
+
describe('CreateIntegration Use-Case', () => {
|
|
825
|
+
let integrationRepository;
|
|
826
|
+
let moduleFactory;
|
|
827
|
+
let useCase;
|
|
828
|
+
|
|
829
|
+
beforeEach(() => {
|
|
830
|
+
integrationRepository = new TestIntegrationRepository();
|
|
831
|
+
moduleFactory = new TestModuleFactory();
|
|
832
|
+
useCase = new CreateIntegration({
|
|
833
|
+
integrationRepository,
|
|
834
|
+
integrationClasses: [TestIntegration],
|
|
835
|
+
moduleFactory
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe('happy path', () => {
|
|
840
|
+
it('creates an integration and returns DTO', async () => {
|
|
841
|
+
const result = await useCase.execute(['entity-1'], 'user-1', { type: 'test' });
|
|
842
|
+
expect(result.id).toBeDefined();
|
|
843
|
+
expect(result.status).toBe('NEW');
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
describe('error cases', () => {
|
|
848
|
+
it('throws error for unknown integration type', async () => {
|
|
849
|
+
await expect(useCase.execute(['entity-1'], 'user-1', { type: 'unknown' }))
|
|
850
|
+
.rejects.toThrow('No integration class found for type: unknown');
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Test Doubles
|
|
857
|
+
|
|
858
|
+
The framework provides test doubles for external dependencies:
|
|
859
|
+
|
|
860
|
+
```javascript
|
|
861
|
+
const { TestIntegrationRepository, TestModuleFactory } = require('@friggframework/core/test');
|
|
862
|
+
|
|
863
|
+
// Mock repository for testing
|
|
864
|
+
const testRepo = new TestIntegrationRepository();
|
|
865
|
+
testRepo.addMockIntegration({ id: 'test-123', userId: 'user-1' });
|
|
866
|
+
|
|
867
|
+
// Mock module factory
|
|
868
|
+
const testFactory = new TestModuleFactory();
|
|
869
|
+
testFactory.addMockModule('hubspot', mockHubSpotModule);
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
## Development
|
|
873
|
+
|
|
874
|
+
### Project Structure
|
|
875
|
+
|
|
876
|
+
```
|
|
877
|
+
packages/core/
|
|
878
|
+
├── integrations/ # Integration domain logic
|
|
879
|
+
│ ├── use-cases/ # Business use cases
|
|
880
|
+
│ ├── tests/ # Integration tests
|
|
881
|
+
│ └── integration-base.js # Base integration class
|
|
882
|
+
├── modules/ # API module system
|
|
883
|
+
│ ├── requester/ # HTTP clients
|
|
884
|
+
│ └── use-cases/ # Module management
|
|
885
|
+
├── database/ # Data persistence
|
|
886
|
+
├── encrypt/ # Encryption utilities
|
|
887
|
+
├── errors/ # Error definitions
|
|
888
|
+
├── logs/ # Logging system
|
|
889
|
+
└── lambda/ # Serverless utilities
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### Adding New Components
|
|
893
|
+
|
|
894
|
+
1. **Create the component**: Follow the established patterns
|
|
895
|
+
2. **Add tests**: Comprehensive test coverage required
|
|
896
|
+
3. **Export from index.js**: Make it available to consumers
|
|
897
|
+
4. **Update documentation**: Keep README current
|
|
898
|
+
|
|
899
|
+
### Code Style
|
|
900
|
+
|
|
901
|
+
```bash
|
|
902
|
+
# Format code
|
|
903
|
+
npm run lint:fix
|
|
904
|
+
|
|
905
|
+
# Check linting
|
|
906
|
+
npm run lint
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
## API Reference
|
|
910
|
+
|
|
911
|
+
### Core Exports
|
|
912
|
+
|
|
913
|
+
```javascript
|
|
914
|
+
const {
|
|
915
|
+
// Integrations
|
|
916
|
+
IntegrationBase,
|
|
917
|
+
IntegrationModel,
|
|
918
|
+
CreateIntegration,
|
|
919
|
+
UpdateIntegration,
|
|
920
|
+
DeleteIntegration,
|
|
921
|
+
|
|
922
|
+
// Modules
|
|
923
|
+
OAuth2Requester,
|
|
924
|
+
ApiKeyRequester,
|
|
925
|
+
Credential,
|
|
926
|
+
Entity,
|
|
927
|
+
// Database
|
|
928
|
+
connectToDatabase,
|
|
929
|
+
mongoose,
|
|
930
|
+
UserModel,
|
|
931
|
+
|
|
932
|
+
// Utilities
|
|
933
|
+
Encrypt,
|
|
934
|
+
Cryptor,
|
|
935
|
+
BaseError,
|
|
936
|
+
debug,
|
|
937
|
+
TimeoutCatcher
|
|
938
|
+
} = require('@friggframework/core');
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### Environment Configuration
|
|
942
|
+
|
|
943
|
+
| Variable | Required | Description |
|
|
944
|
+
|----------|----------|-------------|
|
|
945
|
+
| `MONGO_URI` | Yes | MongoDB connection string |
|
|
946
|
+
| `FRIGG_ENCRYPTION_KEY` | Yes | 256-bit encryption key |
|
|
947
|
+
| `AWS_REGION` | No | AWS region for services |
|
|
948
|
+
| `DEBUG` | No | Debug logging pattern |
|
|
949
|
+
| `LOG_LEVEL` | No | Logging level (debug, info, warn, error) |
|
|
77
950
|
|
|
78
951
|
## License
|
|
79
952
|
|
|
80
|
-
This project is licensed under the MIT License
|
|
953
|
+
This project is licensed under the MIT License - see the [LICENSE.md](../../LICENSE.md) file for details.
|
|
81
954
|
|
|
82
955
|
---
|
|
83
|
-
|
|
956
|
+
|
|
957
|
+
## Support
|
|
958
|
+
|
|
959
|
+
- 📖 [Documentation](https://docs.friggframework.org)
|
|
960
|
+
- 💬 [Community Slack](https://friggframework.slack.com)
|
|
961
|
+
- 🐛 [Issue Tracker](https://github.com/friggframework/frigg/issues)
|
|
962
|
+
- 📧 [Email Support](mailto:support@friggframework.org)
|
|
963
|
+
|
|
964
|
+
Built with ❤️ by the Frigg Framework team.
|