@friggframework/core 2.0.0-next.45 → 2.0.0-next.47
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/README.md +28 -0
- package/application/commands/integration-commands.js +19 -0
- package/core/Worker.js +8 -21
- package/credential/repositories/credential-repository-mongo.js +14 -8
- package/credential/repositories/credential-repository-postgres.js +14 -8
- package/credential/repositories/credential-repository.js +3 -8
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +11 -2
- package/database/models/WebsocketConnection.js +11 -10
- package/database/prisma.js +63 -3
- package/database/repositories/health-check-repository-mongodb.js +3 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +3 -2
- 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/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/encrypt/Cryptor.js +14 -16
- 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 +22897 -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 +3977 -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 +25071 -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 +3977 -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/database-migration-handler.js +227 -0
- package/handlers/routers/auth.js +1 -1
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +256 -0
- package/handlers/routers/health.js +41 -6
- package/handlers/routers/integration-webhook-routers.js +2 -2
- package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
- package/handlers/workers/db-migration.js +352 -0
- package/index.js +12 -0
- package/integrations/integration-router.js +60 -70
- package/integrations/repositories/integration-repository-interface.js +12 -0
- package/integrations/repositories/integration-repository-mongo.js +32 -0
- package/integrations/repositories/integration-repository-postgres.js +33 -0
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/test-integration-repository.js +2 -2
- package/logs/logger.js +0 -4
- package/modules/entity.js +0 -1
- package/modules/repositories/module-repository-mongo.js +3 -12
- package/modules/repositories/module-repository-postgres.js +0 -11
- package/modules/repositories/module-repository.js +1 -12
- package/modules/use-cases/get-entity-options-by-id.js +1 -1
- package/modules/use-cases/get-module.js +1 -2
- package/modules/use-cases/refresh-entity-options.js +1 -1
- package/modules/use-cases/test-module-auth.js +1 -1
- package/package.json +82 -66
- package/prisma-mongodb/schema.prisma +21 -21
- package/prisma-postgresql/schema.prisma +15 -15
- package/queues/queuer-util.js +24 -21
- package/types/core/index.d.ts +2 -2
- package/types/module-plugin/index.d.ts +0 -2
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
- package/user/user.js +16 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
- package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
- package/websocket/repositories/websocket-connection-repository.js +11 -10
- package/application/commands/integration-commands.test.js +0 -123
- package/database/encryption/encryption-integration.test.js +0 -553
- package/database/encryption/encryption-schema-registry.test.js +0 -392
- package/database/encryption/field-encryption-service.test.js +0 -525
- package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
- package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
- package/database/encryption/postgres-relation-decryption.test.js +0 -245
- package/database/encryption/prisma-encryption-extension.test.js +0 -439
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/auth-flow.integration.test.js +0 -147
- package/handlers/integration-event-dispatcher.test.js +0 -209
- package/handlers/routers/health.test.js +0 -210
- package/handlers/routers/integration-webhook-routers.test.js +0 -126
- package/handlers/webhook-flow.integration.test.js +0 -356
- package/handlers/workers/integration-defined-workers.test.js +0 -184
- package/integrations/tests/use-cases/create-integration.test.js +0 -131
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
- package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
- package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
- package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
- package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
- package/integrations/tests/use-cases/update-integration.test.js +0 -141
- package/integrations/use-cases/create-process.test.js +0 -178
- package/integrations/use-cases/get-process.test.js +0 -190
- package/integrations/use-cases/load-integration-context-full.test.js +0 -329
- package/integrations/use-cases/load-integration-context.test.js +0 -114
- package/integrations/use-cases/update-process-metrics.test.js +0 -308
- package/integrations/use-cases/update-process-state.test.js +0 -256
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/modules/module-hydration.test.js +0 -205
- package/modules/requester/requester.test.js +0 -28
- package/user/tests/use-cases/create-individual-user.test.js +0 -24
- package/user/tests/use-cases/create-organization-user.test.js +0 -28
- package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
- package/user/tests/use-cases/login-user.test.js +0 -220
- package/user/tests/user-password-encryption-isolation.test.js +0 -237
- package/user/tests/user-password-hashing.test.js +0 -235
package/README.md
CHANGED
|
@@ -62,6 +62,34 @@ npm install @friggframework/core
|
|
|
62
62
|
yarn add @friggframework/core
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### Prisma Support (Optional)
|
|
66
|
+
|
|
67
|
+
`@friggframework/core` supports both MongoDB and PostgreSQL via Prisma ORM. **Prisma is an optional peer dependency** - you only need to install it if you're using database features that require migrations or schema generation.
|
|
68
|
+
|
|
69
|
+
**When you need Prisma:**
|
|
70
|
+
- Running database migrations (`prisma migrate`, `prisma db push`)
|
|
71
|
+
- Generating Prisma clients for your application
|
|
72
|
+
- Using the migration Lambda function (`dbMigrate`)
|
|
73
|
+
|
|
74
|
+
**Installation:**
|
|
75
|
+
```bash
|
|
76
|
+
# Install Prisma CLI and Client as dev dependencies
|
|
77
|
+
npm install --save-dev prisma @prisma/client
|
|
78
|
+
|
|
79
|
+
# Or with yarn
|
|
80
|
+
yarn add -D prisma @prisma/client
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Generate Prisma Clients:**
|
|
84
|
+
```bash
|
|
85
|
+
# From @friggframework/core directory
|
|
86
|
+
npm run prisma:generate:mongo # MongoDB only
|
|
87
|
+
npm run prisma:generate:postgres # PostgreSQL only
|
|
88
|
+
npm run prisma:generate # Both databases
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Note:** The published npm package includes pre-generated Prisma clients, so you don't need to install Prisma just to use `@friggframework/core` in production. Prisma is only required if you're actively developing migrations or running the migration Lambda function.
|
|
92
|
+
|
|
65
93
|
### Prerequisites
|
|
66
94
|
|
|
67
95
|
- Node.js 16+
|
|
@@ -142,6 +142,25 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
142
142
|
return mapErrorToResponse(error);
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update integration configuration
|
|
148
|
+
* @param {Object} params
|
|
149
|
+
* @param {string} params.integrationId - Integration ID
|
|
150
|
+
* @param {Object} params.config - Updated config object
|
|
151
|
+
* @returns {Promise<Object>} Updated integration
|
|
152
|
+
*/
|
|
153
|
+
async updateIntegrationConfig({ integrationId, config }) {
|
|
154
|
+
try {
|
|
155
|
+
const integration = await integrationRepository.updateIntegrationConfig(
|
|
156
|
+
integrationId,
|
|
157
|
+
config
|
|
158
|
+
);
|
|
159
|
+
return integration;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return mapErrorToResponse(error);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
145
164
|
};
|
|
146
165
|
}
|
|
147
166
|
|
package/core/Worker.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
const { RequiredPropertyError } = require('../errors');
|
|
4
4
|
const { get } = require('../assertions');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
|
|
6
|
+
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
8
7
|
|
|
9
8
|
class Worker {
|
|
10
9
|
async getQueueURL(params) {
|
|
@@ -12,15 +11,9 @@ class Worker {
|
|
|
12
11
|
// let params = {
|
|
13
12
|
// QueueName: process.env.QueueName
|
|
14
13
|
// };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
reject(err);
|
|
19
|
-
} else {
|
|
20
|
-
resolve(data.QueueUrl);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
14
|
+
const command = new GetQueueUrlCommand(params);
|
|
15
|
+
const data = await sqs.send(command);
|
|
16
|
+
return data.QueueUrl;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
async run(params, context = {}) {
|
|
@@ -54,15 +47,9 @@ class Worker {
|
|
|
54
47
|
}
|
|
55
48
|
|
|
56
49
|
async sendAsyncSQSMessage(params) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
reject(err);
|
|
61
|
-
} else {
|
|
62
|
-
resolve(data.MessageId);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
});
|
|
50
|
+
const command = new SendMessageCommand(params);
|
|
51
|
+
const data = await sqs.send(command);
|
|
52
|
+
return data.MessageId;
|
|
66
53
|
}
|
|
67
54
|
|
|
68
55
|
// Throw an exception if the params do not validate
|
|
@@ -44,7 +44,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
44
44
|
userId: credential.userId,
|
|
45
45
|
externalId: credential.externalId,
|
|
46
46
|
authIsValid: credential.authIsValid,
|
|
47
|
-
subType: credential.subType,
|
|
48
47
|
...data, // Spread OAuth tokens from JSON field
|
|
49
48
|
};
|
|
50
49
|
}
|
|
@@ -100,6 +99,17 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
100
99
|
if (!identifiers)
|
|
101
100
|
throw new Error('identifiers required to upsert credential');
|
|
102
101
|
|
|
102
|
+
if (!identifiers.user && !identifiers.userId) {
|
|
103
|
+
throw new Error('user or userId required in identifiers');
|
|
104
|
+
}
|
|
105
|
+
if (!identifiers.externalId) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'externalId required in identifiers to prevent credential collision. ' +
|
|
108
|
+
'When multiple credentials exist for the same user, both userId and externalId ' +
|
|
109
|
+
'are needed to uniquely identify which credential to update.'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// Build where clause from identifiers
|
|
104
114
|
const where = this._convertIdentifiersToWhere(identifiers);
|
|
105
115
|
|
|
@@ -109,7 +119,7 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
109
119
|
userId,
|
|
110
120
|
externalId,
|
|
111
121
|
authIsValid,
|
|
112
|
-
|
|
122
|
+
|
|
113
123
|
...oauthData
|
|
114
124
|
} = details;
|
|
115
125
|
|
|
@@ -132,7 +142,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
132
142
|
authIsValid !== undefined
|
|
133
143
|
? authIsValid
|
|
134
144
|
: existing.authIsValid,
|
|
135
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
136
145
|
data: mergedData,
|
|
137
146
|
},
|
|
138
147
|
});
|
|
@@ -152,7 +161,7 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
152
161
|
userId: userId || user,
|
|
153
162
|
externalId,
|
|
154
163
|
authIsValid: authIsValid,
|
|
155
|
-
|
|
164
|
+
|
|
156
165
|
data: oauthData,
|
|
157
166
|
},
|
|
158
167
|
});
|
|
@@ -225,7 +234,7 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
225
234
|
userId,
|
|
226
235
|
externalId,
|
|
227
236
|
authIsValid,
|
|
228
|
-
|
|
237
|
+
|
|
229
238
|
...oauthData
|
|
230
239
|
} = updates;
|
|
231
240
|
|
|
@@ -240,7 +249,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
240
249
|
externalId !== undefined ? externalId : existing.externalId,
|
|
241
250
|
authIsValid:
|
|
242
251
|
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
243
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
244
252
|
data: mergedData,
|
|
245
253
|
},
|
|
246
254
|
});
|
|
@@ -273,7 +281,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
273
281
|
if (identifiers.user) where.userId = identifiers.user;
|
|
274
282
|
if (identifiers.userId) where.userId = identifiers.userId;
|
|
275
283
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
276
|
-
if (identifiers.subType) where.subType = identifiers.subType;
|
|
277
284
|
|
|
278
285
|
return where;
|
|
279
286
|
}
|
|
@@ -292,7 +299,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
292
299
|
if (filter.user) where.userId = filter.user;
|
|
293
300
|
if (filter.userId) where.userId = filter.userId;
|
|
294
301
|
if (filter.externalId) where.externalId = filter.externalId;
|
|
295
|
-
if (filter.subType) where.subType = filter.subType;
|
|
296
302
|
|
|
297
303
|
return where;
|
|
298
304
|
}
|
|
@@ -61,7 +61,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
61
61
|
userId: credential.userId?.toString(),
|
|
62
62
|
externalId: credential.externalId,
|
|
63
63
|
authIsValid: credential.authIsValid,
|
|
64
|
-
subType: credential.subType,
|
|
65
64
|
...data, // Spread OAuth tokens from JSON field
|
|
66
65
|
};
|
|
67
66
|
}
|
|
@@ -119,12 +118,23 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
119
118
|
if (!identifiers)
|
|
120
119
|
throw new Error('identifiers required to upsert credential');
|
|
121
120
|
|
|
121
|
+
if (!identifiers.user && !identifiers.userId) {
|
|
122
|
+
throw new Error('user or userId required in identifiers');
|
|
123
|
+
}
|
|
124
|
+
if (!identifiers.externalId) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
'externalId required in identifiers to prevent credential collision. ' +
|
|
127
|
+
'When multiple credentials exist for the same user, both userId and externalId ' +
|
|
128
|
+
'are needed to uniquely identify which credential to update.'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
const where = this._convertIdentifiersToWhere(identifiers);
|
|
123
133
|
|
|
124
134
|
const { user, externalId } = identifiers;
|
|
125
135
|
|
|
126
136
|
// Separate schema fields from dynamic OAuth data
|
|
127
|
-
const { authIsValid,
|
|
137
|
+
const { authIsValid, ...oauthData } = details;
|
|
128
138
|
|
|
129
139
|
const existing = await this.prisma.credential.findFirst({ where });
|
|
130
140
|
|
|
@@ -143,7 +153,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
143
153
|
authIsValid !== undefined
|
|
144
154
|
? authIsValid
|
|
145
155
|
: existing.authIsValid,
|
|
146
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
147
156
|
data: mergedData,
|
|
148
157
|
},
|
|
149
158
|
});
|
|
@@ -162,7 +171,7 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
162
171
|
userId: this._convertId(user),
|
|
163
172
|
externalId,
|
|
164
173
|
authIsValid: authIsValid,
|
|
165
|
-
|
|
174
|
+
|
|
166
175
|
data: oauthData,
|
|
167
176
|
},
|
|
168
177
|
});
|
|
@@ -231,7 +240,7 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
231
240
|
}
|
|
232
241
|
|
|
233
242
|
// Separate schema fields from OAuth data
|
|
234
|
-
const { user, authIsValid,
|
|
243
|
+
const { user, authIsValid, ...oauthData } =
|
|
235
244
|
updates;
|
|
236
245
|
|
|
237
246
|
// Merge OAuth data with existing
|
|
@@ -245,7 +254,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
245
254
|
externalId !== undefined ? externalId : existing.externalId,
|
|
246
255
|
authIsValid:
|
|
247
256
|
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
248
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
249
257
|
data: mergedData,
|
|
250
258
|
},
|
|
251
259
|
});
|
|
@@ -278,7 +286,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
278
286
|
if (identifiers.userId)
|
|
279
287
|
where.userId = this._convertId(identifiers.userId);
|
|
280
288
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
281
|
-
if (identifiers.subType) where.subType = identifiers.subType;
|
|
282
289
|
|
|
283
290
|
return where;
|
|
284
291
|
}
|
|
@@ -298,7 +305,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
298
305
|
if (filter.user) where.userId = this._convertId(filter.user);
|
|
299
306
|
if (filter.userId) where.userId = this._convertId(filter.userId);
|
|
300
307
|
if (filter.externalId) where.externalId = filter.externalId;
|
|
301
|
-
if (filter.subType) where.subType = filter.subType;
|
|
302
308
|
|
|
303
309
|
return where;
|
|
304
310
|
}
|
|
@@ -50,7 +50,6 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
50
50
|
userId: credential.userId,
|
|
51
51
|
externalId: credential.externalId,
|
|
52
52
|
authIsValid: credential.authIsValid,
|
|
53
|
-
subType: credential.subType,
|
|
54
53
|
...data, // Spread OAuth tokens from JSON field
|
|
55
54
|
};
|
|
56
55
|
}
|
|
@@ -115,7 +114,7 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
115
114
|
userId,
|
|
116
115
|
externalId,
|
|
117
116
|
authIsValid,
|
|
118
|
-
|
|
117
|
+
|
|
119
118
|
...oauthData
|
|
120
119
|
} = details;
|
|
121
120
|
|
|
@@ -138,7 +137,6 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
138
137
|
authIsValid !== undefined
|
|
139
138
|
? authIsValid
|
|
140
139
|
: existing.authIsValid,
|
|
141
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
142
140
|
data: mergedData,
|
|
143
141
|
},
|
|
144
142
|
});
|
|
@@ -158,7 +156,7 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
158
156
|
userId: userId || user,
|
|
159
157
|
externalId,
|
|
160
158
|
authIsValid: authIsValid,
|
|
161
|
-
|
|
159
|
+
|
|
162
160
|
data: oauthData,
|
|
163
161
|
},
|
|
164
162
|
});
|
|
@@ -231,7 +229,7 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
231
229
|
userId,
|
|
232
230
|
externalId,
|
|
233
231
|
authIsValid,
|
|
234
|
-
|
|
232
|
+
|
|
235
233
|
...oauthData
|
|
236
234
|
} = updates;
|
|
237
235
|
|
|
@@ -246,7 +244,6 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
246
244
|
externalId !== undefined ? externalId : existing.externalId,
|
|
247
245
|
authIsValid:
|
|
248
246
|
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
249
|
-
subType: subType !== undefined ? subType : existing.subType,
|
|
250
247
|
data: mergedData,
|
|
251
248
|
},
|
|
252
249
|
});
|
|
@@ -279,7 +276,6 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
279
276
|
if (identifiers.user) where.userId = identifiers.user;
|
|
280
277
|
if (identifiers.userId) where.userId = identifiers.userId;
|
|
281
278
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
282
|
-
if (identifiers.subType) where.subType = identifiers.subType;
|
|
283
279
|
|
|
284
280
|
return where;
|
|
285
281
|
}
|
|
@@ -298,7 +294,6 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
298
294
|
if (filter.user) where.userId = filter.user;
|
|
299
295
|
if (filter.userId) where.userId = filter.userId;
|
|
300
296
|
if (filter.externalId) where.externalId = filter.externalId;
|
|
301
|
-
if (filter.subType) where.subType = filter.subType;
|
|
302
297
|
|
|
303
298
|
return where;
|
|
304
299
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# MongoDB Transaction Namespace Fix
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
The encryption health check was failing with the following error:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Cannot create namespace frigg.Credential in multi-document transaction.
|
|
9
|
+
Error code: 263
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Root Cause
|
|
13
|
+
|
|
14
|
+
MongoDB does not allow creating collections (namespaces) inside multi-document transactions. When Prisma tries to create a document in a collection that doesn't exist yet, MongoDB needs to implicitly create the collection. If this happens inside a transaction context, MongoDB throws error code 263.
|
|
15
|
+
|
|
16
|
+
### Technical Details
|
|
17
|
+
|
|
18
|
+
- **MongoDB Constraint**: Collections must exist before being used in multi-document transactions
|
|
19
|
+
- **Prisma Behavior**: Prisma may implicitly use transactions for certain operations
|
|
20
|
+
- **Impact**: Health checks fail on fresh databases or when collections haven't been created yet
|
|
21
|
+
|
|
22
|
+
## Solution
|
|
23
|
+
|
|
24
|
+
**Implemented a comprehensive schema initialization system that ensures all collections exist at application startup.**
|
|
25
|
+
|
|
26
|
+
### Architectural Approach
|
|
27
|
+
|
|
28
|
+
Rather than checking before each individual database operation, we take a **systematic, fail-fast approach**:
|
|
29
|
+
|
|
30
|
+
1. **Parse Prisma Schema**: Extract all collection names from the Prisma schema definition
|
|
31
|
+
2. **Initialize at Startup**: Create all collections when the database connection is established
|
|
32
|
+
3. **Fail Fast**: If there are database issues, the application fails immediately at startup rather than during runtime operations
|
|
33
|
+
4. **Idempotent**: Safe to run multiple times - only creates collections that don't exist
|
|
34
|
+
|
|
35
|
+
This follows the **"fail fast"** principle and ensures consistent state across all application instances.
|
|
36
|
+
|
|
37
|
+
### Changes Made
|
|
38
|
+
|
|
39
|
+
1. **Created MongoDB Schema Initialization** (`packages/core/database/utils/mongodb-schema-init.js`)
|
|
40
|
+
- `initializeMongoDBSchema()` - Ensures all Prisma collections exist at startup
|
|
41
|
+
- `getPrismaCollections()` - Returns list of all Prisma collection names
|
|
42
|
+
- `PRISMA_COLLECTIONS` - Constant array of all 13 Prisma collections
|
|
43
|
+
- Only runs for MongoDB (skips PostgreSQL)
|
|
44
|
+
- Fails fast if database not connected
|
|
45
|
+
|
|
46
|
+
2. **Created MongoDB Collection Utilities** (`packages/core/database/utils/mongodb-collection-utils.js`)
|
|
47
|
+
- `ensureCollectionExists(collectionName)` - Ensures a single collection exists
|
|
48
|
+
- `ensureCollectionsExist(collectionNames)` - Batch creates multiple collections
|
|
49
|
+
- `collectionExists(collectionName)` - Checks if a collection exists
|
|
50
|
+
- Handles race conditions gracefully (NamespaceExists errors)
|
|
51
|
+
|
|
52
|
+
3. **Integrated into Database Connection** (`packages/core/database/prisma.js`)
|
|
53
|
+
- Modified `connectPrisma()` to call `initializeMongoDBSchema()` after connection
|
|
54
|
+
- Ensures all collections exist before application handles requests
|
|
55
|
+
|
|
56
|
+
4. **Updated Health Check Repository** (`packages/core/database/repositories/health-check-repository-mongodb.js`)
|
|
57
|
+
- Removed per-operation collection existence checks
|
|
58
|
+
- Added documentation noting schema is initialized at startup
|
|
59
|
+
|
|
60
|
+
5. **Added Comprehensive Tests**
|
|
61
|
+
- `mongodb-schema-init.test.js` - Tests schema initialization system
|
|
62
|
+
- `mongodb-collection-utils.test.js` - Tests collection utility functions
|
|
63
|
+
- Tests error handling, race conditions, and edge cases
|
|
64
|
+
|
|
65
|
+
### Implementation Flow
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// 1. Application startup - connect to database
|
|
69
|
+
await connectPrisma();
|
|
70
|
+
└─> await initializeMongoDBSchema();
|
|
71
|
+
└─> await ensureCollectionsExist([
|
|
72
|
+
'User', 'Token', 'Credential', 'Entity',
|
|
73
|
+
'Integration', 'IntegrationMapping', 'Process',
|
|
74
|
+
'Sync', 'DataIdentifier', 'Association',
|
|
75
|
+
'AssociationObject', 'State', 'WebsocketConnection'
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// 2. Now all collections exist - safe to handle requests
|
|
79
|
+
// No per-operation checks needed!
|
|
80
|
+
await prisma.credential.create({ data: {...} }); // Works without namespace error
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Best Practices Followed
|
|
84
|
+
|
|
85
|
+
1. **Domain-Driven Design**: Created reusable utility module for MongoDB-specific concerns
|
|
86
|
+
2. **Hexagonal Architecture**: Infrastructure concerns (schema initialization) handled in infrastructure layer
|
|
87
|
+
3. **Test-Driven Development**: Added comprehensive tests for all utility functions
|
|
88
|
+
4. **Fail Fast Principle**: Database issues discovered at startup, not during runtime
|
|
89
|
+
5. **Idempotency**: Safe to run multiple times across multiple instances
|
|
90
|
+
6. **Error Handling**: Graceful degradation on race conditions and errors
|
|
91
|
+
7. **Documentation**: Inline comments, JSDoc, and comprehensive documentation
|
|
92
|
+
|
|
93
|
+
## Benefits
|
|
94
|
+
|
|
95
|
+
### Immediate Benefits
|
|
96
|
+
- ✅ Fixes encryption health check failures on fresh databases
|
|
97
|
+
- ✅ Prevents transaction namespace errors across **all** Prisma operations
|
|
98
|
+
- ✅ No per-operation overhead - collections created once at startup
|
|
99
|
+
- ✅ Fail fast - database issues discovered immediately at startup
|
|
100
|
+
- ✅ Idempotent - safe to run multiple times and across multiple instances
|
|
101
|
+
|
|
102
|
+
### Architectural Benefits
|
|
103
|
+
- ✅ **Clean separation of concerns**: Schema initialization is infrastructure concern, handled at startup
|
|
104
|
+
- ✅ **Follows DDD/Hexagonal Architecture**: Infrastructure layer handles database setup, repositories focus on business operations
|
|
105
|
+
- ✅ **Consistent across all environments**: Dev, test, staging, production all follow same pattern
|
|
106
|
+
- ✅ **No repository-level checks needed**: All repositories benefit automatically
|
|
107
|
+
- ✅ **Well-tested and documented**: Comprehensive test coverage and documentation
|
|
108
|
+
|
|
109
|
+
### Operational Benefits
|
|
110
|
+
- ✅ **Predictable startup**: Clear logging of schema initialization
|
|
111
|
+
- ✅ **Zero runtime overhead**: Collections created once, not on every operation
|
|
112
|
+
- ✅ **Production-ready**: Handles race conditions, errors, and edge cases gracefully
|
|
113
|
+
|
|
114
|
+
## Design Decisions
|
|
115
|
+
|
|
116
|
+
### Why Initialize at Startup?
|
|
117
|
+
|
|
118
|
+
We considered two approaches:
|
|
119
|
+
|
|
120
|
+
**❌ Per-Operation Checks (Initial approach)**
|
|
121
|
+
```javascript
|
|
122
|
+
async createCredential(data) {
|
|
123
|
+
await ensureCollectionExists('Credential'); // Check every time
|
|
124
|
+
return await prisma.credential.create({ data });
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
- Pros: Guarantees collection exists before each operation
|
|
128
|
+
- Cons: Runtime overhead, repeated checks, scattered logic
|
|
129
|
+
|
|
130
|
+
**✅ Startup Initialization (Final approach)**
|
|
131
|
+
```javascript
|
|
132
|
+
// Once at startup
|
|
133
|
+
await connectPrisma(); // Initializes all collections
|
|
134
|
+
|
|
135
|
+
// All operations just work
|
|
136
|
+
async createCredential(data) {
|
|
137
|
+
return await prisma.credential.create({ data }); // No checks needed
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
- Pros: Zero runtime overhead, centralized logic, fail fast, consistent
|
|
141
|
+
- Cons: Requires database connection at startup (already required)
|
|
142
|
+
|
|
143
|
+
### Benefits of Startup Approach
|
|
144
|
+
|
|
145
|
+
1. **Performance**: Collections created once vs. checking before every operation
|
|
146
|
+
2. **Simplicity**: No conditional logic in repositories
|
|
147
|
+
3. **Reliability**: Fail fast at startup if database has issues
|
|
148
|
+
4. **Maintainability**: Single source of truth for schema initialization
|
|
149
|
+
5. **DDD Alignment**: Infrastructure concerns handled in infrastructure layer
|
|
150
|
+
|
|
151
|
+
## Logging Output
|
|
152
|
+
|
|
153
|
+
When the application starts, you'll see clear logging:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
Initializing MongoDB schema - ensuring all collections exist...
|
|
157
|
+
Created MongoDB collection: Credential
|
|
158
|
+
MongoDB schema initialization complete - 13 collections verified (45ms)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
On subsequent startups (collections already exist):
|
|
162
|
+
```
|
|
163
|
+
Initializing MongoDB schema - ensuring all collections exist...
|
|
164
|
+
MongoDB schema initialization complete - 13 collections verified (12ms)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## References
|
|
168
|
+
|
|
169
|
+
- [Prisma Issue #8305](https://github.com/prisma/prisma/issues/8305) - MongoDB "Cannot create namespace" error
|
|
170
|
+
- [Mongoose Issue #6699](https://github.com/Automattic/mongoose/issues/6699) - Similar issue in Mongoose
|
|
171
|
+
- [MongoDB Transactions Documentation](https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-operations) - Operations allowed in transactions
|
|
172
|
+
- [Prisma MongoDB Guide](https://www.prisma.io/docs/guides/database/mongodb) - Using Prisma with MongoDB
|
|
173
|
+
|
|
174
|
+
## Future Considerations
|
|
175
|
+
|
|
176
|
+
### Automatic Schema Sync
|
|
177
|
+
Consider enhancing the system to:
|
|
178
|
+
- Parse Prisma schema file dynamically to extract collection names
|
|
179
|
+
- Auto-detect schema changes and create new collections
|
|
180
|
+
- Provide CLI command for manual schema initialization
|
|
181
|
+
|
|
182
|
+
### Migration Support
|
|
183
|
+
For production deployments with existing data:
|
|
184
|
+
- Document migration procedures for new collections
|
|
185
|
+
- Consider pre-migration scripts for blue-green deployments
|
|
186
|
+
- Add health check for schema initialization status
|
|
187
|
+
|
|
188
|
+
### Multi-Database Support
|
|
189
|
+
The system already handles:
|
|
190
|
+
- ✅ MongoDB - Full schema initialization
|
|
191
|
+
- ✅ PostgreSQL - Skips initialization (uses Prisma migrations)
|
|
192
|
+
- Consider adding explicit migration support for DocumentDB-specific features
|
|
193
|
+
|
|
194
|
+
### Index Creation
|
|
195
|
+
Future enhancement could also create indexes at startup:
|
|
196
|
+
- Parse Prisma schema for `@@index` directives
|
|
197
|
+
- Create indexes if they don't exist
|
|
198
|
+
- Provide index health checks
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda Invoker Adapter
|
|
3
|
+
* Infrastructure layer - handles AWS Lambda function invocations
|
|
4
|
+
*
|
|
5
|
+
* Part of Hexagonal Architecture:
|
|
6
|
+
* - Infrastructure Layer adapter for AWS SDK
|
|
7
|
+
* - Used by Domain Layer use cases
|
|
8
|
+
* - Isolates AWS-specific logic from business logic
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom error for Lambda invocation failures
|
|
15
|
+
* Provides structured error information for debugging
|
|
16
|
+
*/
|
|
17
|
+
class LambdaInvocationError extends Error {
|
|
18
|
+
constructor(message, functionName, statusCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'LambdaInvocationError';
|
|
21
|
+
this.functionName = functionName;
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adapter for invoking AWS Lambda functions
|
|
28
|
+
*
|
|
29
|
+
* Infrastructure layer - handles AWS SDK communication
|
|
30
|
+
* Converts AWS SDK responses to domain-friendly formats
|
|
31
|
+
*/
|
|
32
|
+
class LambdaInvoker {
|
|
33
|
+
/**
|
|
34
|
+
* @param {LambdaClient} lambdaClient - AWS Lambda client (injected for testability)
|
|
35
|
+
*/
|
|
36
|
+
constructor(lambdaClient = new LambdaClient({})) {
|
|
37
|
+
this.client = lambdaClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Invoke Lambda function synchronously
|
|
42
|
+
*
|
|
43
|
+
* @param {string} functionName - Lambda function name or ARN
|
|
44
|
+
* @param {Object} payload - Event payload to send to Lambda
|
|
45
|
+
* @returns {Promise<Object>} Parsed response body
|
|
46
|
+
* @throws {LambdaInvocationError} If Lambda returns error status
|
|
47
|
+
* @throws {Error} If AWS SDK call fails
|
|
48
|
+
*/
|
|
49
|
+
async invoke(functionName, payload) {
|
|
50
|
+
try {
|
|
51
|
+
const command = new InvokeCommand({
|
|
52
|
+
FunctionName: functionName,
|
|
53
|
+
InvocationType: 'RequestResponse', // Synchronous
|
|
54
|
+
Payload: JSON.stringify(payload),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const response = await this.client.send(command);
|
|
58
|
+
|
|
59
|
+
// Parse response payload
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = JSON.parse(Buffer.from(response.Payload).toString());
|
|
63
|
+
} catch (parseError) {
|
|
64
|
+
throw new LambdaInvocationError(
|
|
65
|
+
`Failed to parse Lambda response: ${parseError.message}`,
|
|
66
|
+
functionName,
|
|
67
|
+
null
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check status code
|
|
72
|
+
if (result.statusCode === 200) {
|
|
73
|
+
return result.body;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Lambda returned error status
|
|
77
|
+
const errorMessage = result.body?.error || 'Lambda invocation failed';
|
|
78
|
+
throw new LambdaInvocationError(
|
|
79
|
+
`Lambda ${functionName} returned error: ${errorMessage}`,
|
|
80
|
+
functionName,
|
|
81
|
+
result.statusCode
|
|
82
|
+
);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Re-throw LambdaInvocationError as-is
|
|
85
|
+
if (error instanceof LambdaInvocationError) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wrap AWS SDK errors
|
|
90
|
+
throw new Error(`Failed to invoke Lambda ${functionName}: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { LambdaInvoker, LambdaInvocationError };
|
|
96
|
+
|
|
97
|
+
|
package/database/config.js
CHANGED
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Determines database type from app definition
|
|
8
|
-
*
|
|
7
|
+
* Determines database type from environment or app definition
|
|
8
|
+
*
|
|
9
|
+
* Detection order:
|
|
10
|
+
* 1. DB_TYPE environment variable (set for migration handlers)
|
|
11
|
+
* 2. App definition (backend/index.js Definition.database configuration)
|
|
9
12
|
*
|
|
10
13
|
* @returns {'mongodb'|'postgresql'} Database type
|
|
11
14
|
* @throws {Error} If database type cannot be determined or app definition missing
|
|
12
15
|
*/
|
|
13
16
|
function getDatabaseType() {
|
|
17
|
+
// First, check DB_TYPE environment variable (migration handlers set this)
|
|
18
|
+
if (process.env.DB_TYPE) {
|
|
19
|
+
return process.env.DB_TYPE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback: Load app definition
|
|
14
23
|
try {
|
|
15
24
|
const path = require('node:path');
|
|
16
25
|
const fs = require('node:fs');
|