@friggframework/core 2.0.0--canary.502.c9c05da.0 → 2.0.0--canary.498.b74f21e.0
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/credential/repositories/credential-repository-postgres.js +7 -2
- package/database/encryption/README.md +20 -126
- package/database/encryption/encryption-schema-registry.js +2 -83
- package/database/index.js +39 -12
- package/database/utils/prisma-runner.js +71 -0
- package/handlers/backend-utils.js +16 -10
- package/handlers/routers/db-migration.js +72 -1
- package/integrations/integration-base.js +32 -3
- package/integrations/integration-router.js +4 -8
- package/modules/requester/api-key.js +8 -24
- package/modules/use-cases/get-entity-options-by-id.js +17 -5
- package/modules/use-cases/get-module.js +21 -2
- package/modules/use-cases/process-authorization-callback.js +12 -1
- package/modules/use-cases/refresh-entity-options.js +18 -5
- package/modules/use-cases/test-module-auth.js +19 -2
- package/package.json +5 -5
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +0 -44
- package/queues/queuer-util.js +0 -8
- package/user/repositories/user-repository-documentdb.js +20 -0
- package/user/repositories/user-repository-interface.js +14 -0
- package/user/repositories/user-repository-mongo.js +18 -0
- package/user/repositories/user-repository-postgres.js +22 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +47 -21
- package/user/user.js +32 -0
|
@@ -111,7 +111,8 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
111
111
|
if (!identifiers)
|
|
112
112
|
throw new Error('identifiers required to upsert credential');
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// Support both userId (preferred) and user (legacy) for backward compatibility
|
|
115
|
+
if (!identifiers.userId && !identifiers.user) {
|
|
115
116
|
throw new Error('userId required in identifiers');
|
|
116
117
|
}
|
|
117
118
|
if (!identifiers.externalId) {
|
|
@@ -154,7 +155,8 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
154
155
|
|
|
155
156
|
const created = await this.prisma.credential.create({
|
|
156
157
|
data: {
|
|
157
|
-
userId
|
|
158
|
+
// Use userId from where clause (supports both userId and user fields)
|
|
159
|
+
userId: where.userId,
|
|
158
160
|
externalId,
|
|
159
161
|
authIsValid: authIsValid,
|
|
160
162
|
data: oauthData,
|
|
@@ -257,8 +259,11 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
257
259
|
const where = {};
|
|
258
260
|
|
|
259
261
|
if (identifiers.id) where.id = this._convertId(identifiers.id);
|
|
262
|
+
// Support both userId (preferred) and user (legacy) for backward compatibility
|
|
260
263
|
if (identifiers.userId)
|
|
261
264
|
where.userId = this._convertId(identifiers.userId);
|
|
265
|
+
else if (identifiers.user)
|
|
266
|
+
where.userId = this._convertId(identifiers.user);
|
|
262
267
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
263
268
|
|
|
264
269
|
return where;
|
|
@@ -122,140 +122,34 @@ Or simply don't configure any encryption keys. In Production field level encrypt
|
|
|
122
122
|
|
|
123
123
|
## Encrypted Fields
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Fields are defined in `encryption-schema-registry.js`:
|
|
126
126
|
|
|
127
|
-
**Core fields include**:
|
|
128
|
-
- OAuth tokens: `access_token`, `refresh_token`, `id_token`
|
|
129
|
-
- API keys: `api_key`, `apiKey`, `API_KEY_VALUE`
|
|
130
|
-
- Basic auth: `password`
|
|
131
|
-
- OAuth client credentials: `client_secret`
|
|
132
|
-
|
|
133
|
-
**Note**: API modules should use `api_key` (snake_case) in their `apiPropertiesToPersist.credential` arrays for consistency with OAuth2Requester and BasicAuthRequester conventions.
|
|
134
|
-
|
|
135
|
-
### API Module Credential Naming Conventions
|
|
136
|
-
|
|
137
|
-
When creating API module definitions, use **snake_case** for credential property names to ensure automatic encryption:
|
|
138
|
-
|
|
139
|
-
**✅ Recommended (automatically encrypted):**
|
|
140
|
-
```javascript
|
|
141
|
-
// API Module Definition
|
|
142
|
-
const Definition = {
|
|
143
|
-
requiredAuthMethods: {
|
|
144
|
-
apiPropertiesToPersist: {
|
|
145
|
-
// For API key authentication
|
|
146
|
-
credential: ['api_key'], // ✅ Automatically encrypted
|
|
147
|
-
// or for OAuth authentication
|
|
148
|
-
credential: ['access_token', 'refresh_token'], // ✅ OAuth - encrypted
|
|
149
|
-
// or for Basic authentication
|
|
150
|
-
credential: ['username', 'password'], // ✅ Basic auth - encrypted
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// API class (extends ApiKeyRequester)
|
|
156
|
-
class MyApi extends ApiKeyRequester {
|
|
157
|
-
constructor(params) {
|
|
158
|
-
super(params);
|
|
159
|
-
this.api_key = params.api_key; // ✅ snake_case convention
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**❌ Avoid (requires manual encryption schema):**
|
|
165
127
|
```javascript
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
### Extending Encryption Schema
|
|
174
|
-
|
|
175
|
-
#### Option 1: Module-Level Encryption (API Module Developers)
|
|
176
|
-
|
|
177
|
-
**NEW**: API modules can now declare their encryption requirements directly in the module definition:
|
|
178
|
-
|
|
179
|
-
```javascript
|
|
180
|
-
// api-module-library/my-service/definition.js
|
|
181
|
-
const Definition = {
|
|
182
|
-
moduleName: 'myService',
|
|
183
|
-
API: MyServiceApi,
|
|
184
|
-
|
|
185
|
-
// Declare which credential fields need encryption
|
|
186
|
-
encryption: {
|
|
187
|
-
credentialFields: ['api_key', 'webhook_secret']
|
|
128
|
+
const ENCRYPTION_SCHEMA = {
|
|
129
|
+
Credential: {
|
|
130
|
+
fields: [
|
|
131
|
+
'data.access_token', // OAuth access token
|
|
132
|
+
'data.refresh_token', // OAuth refresh token
|
|
133
|
+
'data.id_token', // OpenID Connect ID token
|
|
134
|
+
],
|
|
188
135
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**How it works**:
|
|
201
|
-
1. Module declares `encryption.credentialFields` array
|
|
202
|
-
2. Framework automatically adds `data.` prefix: `['api_key']` → `['data.api_key']`
|
|
203
|
-
3. Fields are merged with core encryption schema on app startup
|
|
204
|
-
4. All modules across all integrations are scanned and combined
|
|
205
|
-
|
|
206
|
-
**Benefits**:
|
|
207
|
-
- ✅ Module authors control their own security requirements
|
|
208
|
-
- ✅ No need to modify core framework or app configuration
|
|
209
|
-
- ✅ Automatic encryption for API key-based integrations
|
|
210
|
-
- ✅ Works seamlessly with `apiPropertiesToPersist`
|
|
211
|
-
|
|
212
|
-
**Example - API Key Module**:
|
|
213
|
-
```javascript
|
|
214
|
-
// API Module Definition
|
|
215
|
-
const Definition = {
|
|
216
|
-
moduleName: 'axiscare',
|
|
217
|
-
API: AxisCareApi,
|
|
218
|
-
encryption: {
|
|
219
|
-
credentialFields: ['api_key'] // Auto-encrypted as 'data.api_key'
|
|
136
|
+
IntegrationMapping: {
|
|
137
|
+
fields: ['mapping'], // Complete mapping object
|
|
138
|
+
},
|
|
139
|
+
User: {
|
|
140
|
+
fields: ['hashword'], // Password hash
|
|
141
|
+
},
|
|
142
|
+
Token: {
|
|
143
|
+
fields: ['token'], // Authentication token
|
|
220
144
|
},
|
|
221
|
-
requiredAuthMethods: {
|
|
222
|
-
apiPropertiesToPersist: {
|
|
223
|
-
credential: ['api_key'] // Will be encrypted automatically
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// API Class (extends ApiKeyRequester)
|
|
229
|
-
class AxisCareApi extends ApiKeyRequester {
|
|
230
|
-
constructor(params) {
|
|
231
|
-
super(params);
|
|
232
|
-
this.api_key = params.api_key; // snake_case convention
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Example - Custom Authentication**:
|
|
238
|
-
```javascript
|
|
239
|
-
const Definition = {
|
|
240
|
-
moduleName: 'customService',
|
|
241
|
-
encryption: {
|
|
242
|
-
credentialFields: [
|
|
243
|
-
'signing_key',
|
|
244
|
-
'webhook_secret',
|
|
245
|
-
'data.custom_nested_field' // Can specify data. prefix explicitly
|
|
246
|
-
]
|
|
247
|
-
}
|
|
248
145
|
};
|
|
249
146
|
```
|
|
250
147
|
|
|
251
|
-
|
|
252
|
-
- Only supports Credential model fields (stored in `credential.data`)
|
|
253
|
-
- Cannot encrypt entity fields or custom models (use app-level schema for those)
|
|
254
|
-
- Applied globally once - module schemas loaded at app startup
|
|
148
|
+
### Extending Encryption Schema
|
|
255
149
|
|
|
256
|
-
####
|
|
150
|
+
#### Recommended: Custom Schema via appDefinition (Integration Developers)
|
|
257
151
|
|
|
258
|
-
Integration developers can extend encryption without modifying core framework files
|
|
152
|
+
Integration developers can extend encryption without modifying core framework files:
|
|
259
153
|
|
|
260
154
|
**In `backend/index.js`:**
|
|
261
155
|
|
|
@@ -340,7 +234,7 @@ await prisma.asanaTaskMapping.create({
|
|
|
340
234
|
FRIGG_DEBUG=1 npm run frigg:start
|
|
341
235
|
```
|
|
342
236
|
|
|
343
|
-
####
|
|
237
|
+
#### Advanced: Modifying Core Schema (Framework Developers)
|
|
344
238
|
|
|
345
239
|
Framework developers maintaining core models can modify `encryption-schema-registry.js`:
|
|
346
240
|
|
|
@@ -19,11 +19,6 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
19
19
|
'data.access_token',
|
|
20
20
|
'data.refresh_token',
|
|
21
21
|
'data.id_token',
|
|
22
|
-
'data.api_key',
|
|
23
|
-
'data.apiKey',
|
|
24
|
-
'data.API_KEY_VALUE',
|
|
25
|
-
'data.password',
|
|
26
|
-
'data.client_secret',
|
|
27
22
|
],
|
|
28
23
|
},
|
|
29
24
|
|
|
@@ -114,74 +109,6 @@ function registerCustomSchema(schema) {
|
|
|
114
109
|
);
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
/**
|
|
118
|
-
* Extracts credential field paths from module definitions
|
|
119
|
-
* @param {Array} moduleDefinitions - Array of module definition objects
|
|
120
|
-
* @returns {Array<string>} Array of field paths with data. prefix
|
|
121
|
-
*/
|
|
122
|
-
function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
123
|
-
const fields = [];
|
|
124
|
-
|
|
125
|
-
for (const moduleDef of moduleDefinitions) {
|
|
126
|
-
if (!moduleDef?.encryption?.credentialFields) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const credentialFields = moduleDef.encryption.credentialFields;
|
|
131
|
-
if (!Array.isArray(credentialFields) || credentialFields.length === 0) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
for (const field of credentialFields) {
|
|
136
|
-
const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
|
|
137
|
-
fields.push(prefixedField);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return [...new Set(fields)];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Loads and registers encryption schemas from API module definitions.
|
|
146
|
-
* Each module can declare credentialFields to encrypt in its encryption config.
|
|
147
|
-
*
|
|
148
|
-
* @param {Array} integrations - Array of integration classes with modules
|
|
149
|
-
*/
|
|
150
|
-
function loadModuleEncryptionSchemas(integrations) {
|
|
151
|
-
if (!integrations) {
|
|
152
|
-
throw new Error('integrations parameter is required');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (!Array.isArray(integrations)) {
|
|
156
|
-
throw new Error('integrations must be an array');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (integrations.length === 0) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
164
|
-
|
|
165
|
-
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
166
|
-
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
167
|
-
|
|
168
|
-
if (credentialFields.length === 0) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const moduleSchema = {
|
|
173
|
-
Credential: {
|
|
174
|
-
fields: credentialFields
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
logger.info(
|
|
179
|
-
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
registerCustomSchema(moduleSchema);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
112
|
/**
|
|
186
113
|
* Loads and registers custom encryption schema from appDefinition.
|
|
187
114
|
* Gracefully handles cases where appDefinition is not available.
|
|
@@ -212,17 +139,11 @@ function loadCustomEncryptionSchema() {
|
|
|
212
139
|
return; // No app definition found
|
|
213
140
|
}
|
|
214
141
|
|
|
215
|
-
// Load app-level custom schema
|
|
216
142
|
const customSchema = appDefinition.encryption?.schema;
|
|
143
|
+
|
|
217
144
|
if (customSchema && Object.keys(customSchema).length > 0) {
|
|
218
145
|
registerCustomSchema(customSchema);
|
|
219
146
|
}
|
|
220
|
-
|
|
221
|
-
// Load module-level encryption schemas from integrations
|
|
222
|
-
const integrations = appDefinition.integrations;
|
|
223
|
-
if (integrations && Array.isArray(integrations)) {
|
|
224
|
-
loadModuleEncryptionSchemas(integrations);
|
|
225
|
-
}
|
|
226
147
|
} catch (error) {
|
|
227
148
|
// Silently ignore errors - custom schema is optional
|
|
228
149
|
// This handles cases like:
|
|
@@ -261,8 +182,6 @@ module.exports = {
|
|
|
261
182
|
getEncryptedModels,
|
|
262
183
|
registerCustomSchema,
|
|
263
184
|
loadCustomEncryptionSchema,
|
|
264
|
-
loadModuleEncryptionSchemas,
|
|
265
|
-
extractCredentialFieldsFromModules,
|
|
266
185
|
validateCustomSchema,
|
|
267
|
-
resetCustomSchema,
|
|
186
|
+
resetCustomSchema, // For testing only
|
|
268
187
|
};
|
package/database/index.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* etc.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// Lazy-load mongoose to avoid importing mongodb when using PostgreSQL only
|
|
16
|
+
let _mongoose = null;
|
|
17
|
+
let _IndividualUser = null;
|
|
18
|
+
let _OrganizationUser = null;
|
|
19
|
+
let _UserModel = null;
|
|
20
|
+
let _WebsocketConnection = null;
|
|
20
21
|
|
|
21
|
-
// Prisma exports
|
|
22
|
+
// Prisma exports (always available)
|
|
22
23
|
const { prisma } = require('./prisma');
|
|
23
24
|
const { TokenRepository } = require('../token/repositories/token-repository');
|
|
24
25
|
const {
|
|
@@ -26,12 +27,38 @@ const {
|
|
|
26
27
|
} = require('../websocket/repositories/websocket-connection-repository');
|
|
27
28
|
|
|
28
29
|
module.exports = {
|
|
29
|
-
mongoose
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Lazy-loaded mongoose exports (only load when accessed)
|
|
31
|
+
get mongoose() {
|
|
32
|
+
if (!_mongoose) {
|
|
33
|
+
_mongoose = require('./mongoose').mongoose;
|
|
34
|
+
}
|
|
35
|
+
return _mongoose;
|
|
36
|
+
},
|
|
37
|
+
get IndividualUser() {
|
|
38
|
+
if (!_IndividualUser) {
|
|
39
|
+
_IndividualUser = require('./models/IndividualUser').IndividualUser;
|
|
40
|
+
}
|
|
41
|
+
return _IndividualUser;
|
|
42
|
+
},
|
|
43
|
+
get OrganizationUser() {
|
|
44
|
+
if (!_OrganizationUser) {
|
|
45
|
+
_OrganizationUser = require('./models/OrganizationUser').OrganizationUser;
|
|
46
|
+
}
|
|
47
|
+
return _OrganizationUser;
|
|
48
|
+
},
|
|
49
|
+
get UserModel() {
|
|
50
|
+
if (!_UserModel) {
|
|
51
|
+
_UserModel = require('./models/UserModel').UserModel;
|
|
52
|
+
}
|
|
53
|
+
return _UserModel;
|
|
54
|
+
},
|
|
55
|
+
get WebsocketConnection() {
|
|
56
|
+
if (!_WebsocketConnection) {
|
|
57
|
+
_WebsocketConnection = require('./models/WebsocketConnection').WebsocketConnection;
|
|
58
|
+
}
|
|
59
|
+
return _WebsocketConnection;
|
|
60
|
+
},
|
|
61
|
+
// Prisma (always available)
|
|
35
62
|
prisma,
|
|
36
63
|
TokenRepository,
|
|
37
64
|
WebsocketConnectionRepository,
|
|
@@ -373,6 +373,76 @@ async function runPrismaDbPush(verbose = false, nonInteractive = false) {
|
|
|
373
373
|
});
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Runs Prisma migrate resolve to mark a migration as applied or rolled back
|
|
378
|
+
* @param {string} migrationName - Name of the migration to resolve (e.g., '20251112195422_update_user_unique_constraints')
|
|
379
|
+
* @param {'applied'|'rolled-back'} action - Whether to mark as applied or rolled back
|
|
380
|
+
* @param {boolean} verbose - Enable verbose output
|
|
381
|
+
* @returns {Promise<Object>} { success: boolean, output?: string, error?: string }
|
|
382
|
+
*/
|
|
383
|
+
async function runPrismaMigrateResolve(migrationName, action = 'applied', verbose = false) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
try {
|
|
386
|
+
const schemaPath = getPrismaSchemaPath('postgresql');
|
|
387
|
+
|
|
388
|
+
// Get Prisma binary path (checks multiple locations)
|
|
389
|
+
const prismaBin = getPrismaBinaryPath();
|
|
390
|
+
|
|
391
|
+
// Determine args based on whether we're using direct binary or npx
|
|
392
|
+
const isDirectBinary = prismaBin !== 'npx prisma';
|
|
393
|
+
const args = isDirectBinary
|
|
394
|
+
? ['migrate', 'resolve', `--${action}`, migrationName, '--schema', schemaPath]
|
|
395
|
+
: ['prisma', 'migrate', 'resolve', `--${action}`, migrationName, '--schema', schemaPath];
|
|
396
|
+
|
|
397
|
+
if (verbose) {
|
|
398
|
+
const displayCmd = isDirectBinary
|
|
399
|
+
? `${prismaBin} ${args.join(' ')}`
|
|
400
|
+
: `npx ${args.join(' ')}`;
|
|
401
|
+
console.log(chalk.gray(`Running: ${displayCmd}`));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Execute the command (prismaBin might be 'node /path/to/index.js' or 'npx prisma')
|
|
405
|
+
const [executable, ...executableArgs] = prismaBin.split(' ');
|
|
406
|
+
const fullArgs = [...executableArgs, ...args];
|
|
407
|
+
|
|
408
|
+
const proc = spawn(executable, fullArgs, {
|
|
409
|
+
stdio: 'inherit',
|
|
410
|
+
env: {
|
|
411
|
+
...process.env,
|
|
412
|
+
PRISMA_HIDE_UPDATE_MESSAGE: '1'
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
proc.on('error', (error) => {
|
|
417
|
+
resolve({
|
|
418
|
+
success: false,
|
|
419
|
+
error: error.message
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
proc.on('close', (code) => {
|
|
424
|
+
if (code === 0) {
|
|
425
|
+
resolve({
|
|
426
|
+
success: true,
|
|
427
|
+
output: `Migration ${migrationName} marked as ${action}`
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
resolve({
|
|
431
|
+
success: false,
|
|
432
|
+
error: `Resolve process exited with code ${code}`
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
} catch (error) {
|
|
438
|
+
resolve({
|
|
439
|
+
success: false,
|
|
440
|
+
error: error.message
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
376
446
|
/**
|
|
377
447
|
* Determines migration command based on STAGE environment variable
|
|
378
448
|
* @param {string} stage - Stage from CLI option or environment
|
|
@@ -401,6 +471,7 @@ module.exports = {
|
|
|
401
471
|
runPrismaGenerate,
|
|
402
472
|
checkDatabaseState,
|
|
403
473
|
runPrismaMigrate,
|
|
474
|
+
runPrismaMigrateResolve,
|
|
404
475
|
runPrismaDbPush,
|
|
405
476
|
getMigrationCommand
|
|
406
477
|
};
|
|
@@ -92,13 +92,16 @@ const loadIntegrationForWebhook = async (integrationId) => {
|
|
|
92
92
|
integrationId
|
|
93
93
|
);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
const instance = await getIntegrationInstance.execute(
|
|
96
96
|
integrationId,
|
|
97
97
|
integrationRecord.userId
|
|
98
98
|
);
|
|
99
|
+
|
|
100
|
+
return instance;
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
104
|
+
|
|
102
105
|
const { processRepository, integrationRepository, moduleRepository } =
|
|
103
106
|
initializeRepositories();
|
|
104
107
|
|
|
@@ -122,10 +125,12 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
|
122
125
|
throw new Error(`Process not found: ${processId}`);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
const instance = await getIntegrationInstance.execute(
|
|
126
129
|
process.integrationId,
|
|
127
130
|
process.userId
|
|
128
131
|
);
|
|
132
|
+
|
|
133
|
+
return instance;
|
|
129
134
|
};
|
|
130
135
|
|
|
131
136
|
const createQueueWorker = (integrationClass) => {
|
|
@@ -133,18 +138,19 @@ const createQueueWorker = (integrationClass) => {
|
|
|
133
138
|
async _run(params, context) {
|
|
134
139
|
try {
|
|
135
140
|
let integrationInstance;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
params.data.integrationId
|
|
142
|
-
);
|
|
143
|
-
} else if (params.data?.processId) {
|
|
141
|
+
|
|
142
|
+
// Prioritize processId first (for sync handler compatibility),
|
|
143
|
+
// then integrationId (for ANY event type that needs hydration),
|
|
144
|
+
// fallback to unhydrated instance
|
|
145
|
+
if (params.data?.processId) {
|
|
144
146
|
integrationInstance = await loadIntegrationForProcess(
|
|
145
147
|
params.data.processId,
|
|
146
148
|
integrationClass
|
|
147
149
|
);
|
|
150
|
+
} else if (params.data?.integrationId) {
|
|
151
|
+
integrationInstance = await loadIntegrationForWebhook(
|
|
152
|
+
params.data.integrationId
|
|
153
|
+
);
|
|
148
154
|
} else {
|
|
149
155
|
// Instantiates a DRY integration class without database records.
|
|
150
156
|
// There will be cases where we need to use helpers that the api modules can export.
|
|
@@ -235,6 +235,77 @@ router.get(
|
|
|
235
235
|
})
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* POST /db-migrate/resolve
|
|
240
|
+
*
|
|
241
|
+
* Resolve a failed migration by marking it as applied or rolled back
|
|
242
|
+
*
|
|
243
|
+
* Request body:
|
|
244
|
+
* {
|
|
245
|
+
* migrationName: string (e.g., '20251112195422_update_user_unique_constraints'),
|
|
246
|
+
* action: 'applied' | 'rolled-back',
|
|
247
|
+
* stage: string (optional, defaults to STAGE env var or 'production')
|
|
248
|
+
* }
|
|
249
|
+
*
|
|
250
|
+
* Response (200 OK):
|
|
251
|
+
* {
|
|
252
|
+
* success: true,
|
|
253
|
+
* message: string,
|
|
254
|
+
* migrationName: string,
|
|
255
|
+
* action: string
|
|
256
|
+
* }
|
|
257
|
+
*/
|
|
258
|
+
router.post(
|
|
259
|
+
'/db-migrate/resolve',
|
|
260
|
+
catchAsyncError(async (req, res) => {
|
|
261
|
+
const { migrationName, action = 'applied' } = req.body;
|
|
262
|
+
|
|
263
|
+
console.log(`Migration resolve request: migration=${migrationName}, action=${action}`);
|
|
264
|
+
|
|
265
|
+
// Validation
|
|
266
|
+
if (!migrationName) {
|
|
267
|
+
return res.status(400).json({
|
|
268
|
+
success: false,
|
|
269
|
+
error: 'migrationName is required'
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!['applied', 'rolled-back'].includes(action)) {
|
|
274
|
+
return res.status(400).json({
|
|
275
|
+
success: false,
|
|
276
|
+
error: 'action must be either "applied" or "rolled-back"'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Import prismaRunner here to avoid circular dependencies
|
|
282
|
+
const prismaRunner = require('../../database/utils/prisma-runner');
|
|
283
|
+
|
|
284
|
+
const result = await prismaRunner.runPrismaMigrateResolve(migrationName, action, true);
|
|
285
|
+
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
return res.status(500).json({
|
|
288
|
+
success: false,
|
|
289
|
+
error: `Failed to resolve migration: ${result.error}`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
res.status(200).json({
|
|
294
|
+
success: true,
|
|
295
|
+
message: `Migration ${migrationName} marked as ${action}`,
|
|
296
|
+
migrationName,
|
|
297
|
+
action
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Migration resolve failed:', error);
|
|
301
|
+
return res.status(500).json({
|
|
302
|
+
success: false,
|
|
303
|
+
error: error.message
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
238
309
|
// Minimal Lambda handler (avoids app-handler-helpers which loads core/index.js → user/**)
|
|
239
310
|
const serverlessHttp = require('serverless-http');
|
|
240
311
|
const express = require('express');
|
|
@@ -244,7 +315,7 @@ const app = express();
|
|
|
244
315
|
app.use(cors());
|
|
245
316
|
app.use(express.json());
|
|
246
317
|
app.use(router);
|
|
247
|
-
app.use((err,
|
|
318
|
+
app.use((err, _req, res, _next) => {
|
|
248
319
|
console.error('Migration Router Error:', err);
|
|
249
320
|
res.status(500).json({ message: 'Internal Server Error' });
|
|
250
321
|
});
|
|
@@ -203,22 +203,53 @@ class IntegrationBase {
|
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
205
|
* Returns the modules as object with keys as module names.
|
|
206
|
+
* Uses the keys from Definition.modules to attach modules correctly.
|
|
207
|
+
*
|
|
208
|
+
* Example:
|
|
209
|
+
* Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
|
|
210
|
+
* Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
|
|
211
|
+
*
|
|
206
212
|
* @private
|
|
207
213
|
* @param {Array} integrationModules - Array of module instances
|
|
208
214
|
* @returns {Object} The modules object
|
|
209
215
|
*/
|
|
210
216
|
_appendModules(integrationModules) {
|
|
211
217
|
const modules = {};
|
|
218
|
+
|
|
219
|
+
// Build reverse mapping: definition.getName() → referenceKey
|
|
220
|
+
// e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
|
|
221
|
+
const moduleNameToKey = {};
|
|
222
|
+
if (this.constructor.Definition?.modules) {
|
|
223
|
+
for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
|
|
224
|
+
const definition = moduleConfig.definition;
|
|
225
|
+
if (definition) {
|
|
226
|
+
// Use getName() if available, fallback to moduleName
|
|
227
|
+
const definitionName = typeof definition.getName === 'function'
|
|
228
|
+
? definition.getName()
|
|
229
|
+
: definition.moduleName;
|
|
230
|
+
if (definitionName) {
|
|
231
|
+
moduleNameToKey[definitionName] = key;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
212
237
|
for (const module of integrationModules) {
|
|
213
|
-
const
|
|
238
|
+
const moduleName =
|
|
214
239
|
typeof module.getName === 'function'
|
|
215
240
|
? module.getName()
|
|
216
241
|
: module.name;
|
|
242
|
+
|
|
243
|
+
// Use the reference key from Definition.modules if available,
|
|
244
|
+
// otherwise fall back to moduleName
|
|
245
|
+
const key = moduleNameToKey[moduleName] || moduleName;
|
|
246
|
+
|
|
217
247
|
if (key) {
|
|
218
248
|
modules[key] = module;
|
|
219
249
|
this[key] = module;
|
|
220
250
|
}
|
|
221
251
|
}
|
|
252
|
+
|
|
222
253
|
return modules;
|
|
223
254
|
}
|
|
224
255
|
|
|
@@ -333,7 +364,6 @@ class IntegrationBase {
|
|
|
333
364
|
return {};
|
|
334
365
|
}
|
|
335
366
|
async loadUserActions({ actionType } = {}) {
|
|
336
|
-
console.log('loadUserActions called with actionType:', actionType);
|
|
337
367
|
const userActions = {};
|
|
338
368
|
for (const [key, event] of Object.entries(this.events)) {
|
|
339
369
|
if (event.type === constantsToBeMigrated.types.USER_ACTION) {
|
|
@@ -389,7 +419,6 @@ class IntegrationBase {
|
|
|
389
419
|
|
|
390
420
|
async onWebhook({ data }) {
|
|
391
421
|
// Default: no-op, integrations override this
|
|
392
|
-
console.log('Webhook received:', data);
|
|
393
422
|
}
|
|
394
423
|
|
|
395
424
|
async queueWebhook(data) {
|