@friggframework/core 2.0.0-next.53 → 2.0.0-next.55
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 +2 -1
- package/application/commands/credential-commands.js +1 -1
- package/application/commands/integration-commands.js +1 -1
- package/application/index.js +1 -1
- package/core/create-handler.js +12 -0
- package/credential/repositories/credential-repository-documentdb.js +304 -0
- package/credential/repositories/credential-repository-factory.js +8 -1
- package/credential/repositories/credential-repository-mongo.js +16 -54
- package/credential/repositories/credential-repository-postgres.js +14 -41
- package/credential/use-cases/get-credential-for-user.js +7 -3
- package/database/config.js +4 -4
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +136 -0
- package/database/encryption/README.md +50 -1
- package/database/encryption/documentdb-encryption-service.md +3270 -0
- package/database/encryption/encryption-schema-registry.js +46 -0
- package/database/prisma.js +7 -47
- package/database/repositories/health-check-repository-documentdb.js +134 -0
- package/database/repositories/health-check-repository-factory.js +6 -1
- package/database/repositories/health-check-repository-interface.js +29 -34
- package/database/repositories/health-check-repository-mongodb.js +1 -3
- package/database/use-cases/check-database-state-use-case.js +3 -3
- package/database/use-cases/run-database-migration-use-case.js +6 -4
- package/database/use-cases/trigger-database-migration-use-case.js +2 -2
- package/database/utils/mongodb-schema-init.js +5 -5
- package/database/utils/prisma-runner.js +15 -9
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +2 -1
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/edge.js +3 -3
- package/generated/prisma-mongodb/index.d.ts +10 -4
- package/generated/prisma-mongodb/index.js +3 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +1 -3
- package/generated/prisma-mongodb/wasm.js +2 -2
- package/generated/prisma-postgresql/edge.js +3 -3
- package/generated/prisma-postgresql/index.d.ts +10 -4
- package/generated/prisma-postgresql/index.js +3 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +1 -3
- package/generated/prisma-postgresql/wasm.js +2 -2
- package/handlers/routers/db-migration.js +2 -3
- package/handlers/routers/health.js +0 -3
- package/handlers/workers/db-migration.js +8 -8
- package/integrations/integration-router.js +6 -6
- package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
- package/integrations/repositories/integration-repository-documentdb.js +210 -0
- package/integrations/repositories/integration-repository-factory.js +8 -1
- package/integrations/repositories/process-repository-documentdb.js +243 -0
- package/integrations/repositories/process-repository-factory.js +8 -1
- package/modules/repositories/module-repository-documentdb.js +307 -0
- package/modules/repositories/module-repository-factory.js +8 -1
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +1 -3
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
- package/prisma-postgresql/schema.prisma +1 -3
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +6 -1
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +8 -1
- package/token/repositories/token-repository-mongo.js +10 -3
- package/token/repositories/token-repository-postgres.js +10 -3
- package/user/repositories/user-repository-documentdb.js +432 -0
- package/user/repositories/user-repository-factory.js +6 -1
- package/user/repositories/user-repository-mongo.js +3 -2
- package/user/repositories/user-repository-postgres.js +3 -2
- package/user/use-cases/login-user.js +1 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
package/CLAUDE.md
CHANGED
|
@@ -27,7 +27,7 @@ This file provides guidance to Claude Code when working with the Frigg Framework
|
|
|
27
27
|
`@friggframework/core` is the foundational package of the Frigg Framework, providing:
|
|
28
28
|
|
|
29
29
|
- **IntegrationBase**: Base class all integrations extend
|
|
30
|
-
- **Database Layer**: Multi-database support (MongoDB, PostgreSQL) with Prisma ORM
|
|
30
|
+
- **Database Layer**: Multi-database support (MongoDB, DocumentDB, PostgreSQL) with Prisma ORM
|
|
31
31
|
- **Encryption**: Transparent field-level encryption with AWS KMS or AES
|
|
32
32
|
- **User Management**: Individual and organizational user support
|
|
33
33
|
- **Module System**: API module loading and credential management
|
|
@@ -256,6 +256,7 @@ class MyIntegration extends IntegrationBase {
|
|
|
256
256
|
- `health-check-repository.js` - Database health monitoring
|
|
257
257
|
- `token-repository.js` - Authentication tokens
|
|
258
258
|
- `websocket-connection-repository.js` - WebSocket connections
|
|
259
|
+
- DocumentDB-enabled adapters mirror the MongoDB APIs but execute raw commands (`$runCommandRaw`, `$aggregateRaw`) for compatibility; encrypted models (e.g., credentials) still delegate reads to Prisma so the encryption extension can decrypt secrets transparently.
|
|
259
260
|
|
|
260
261
|
**Use Cases**:
|
|
261
262
|
- `check-database-health-use-case.js` - Database health checks
|
|
@@ -38,7 +38,7 @@ function mapErrorToResponse(error) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function createIntegrationCommands({ integrationClass }
|
|
41
|
+
function createIntegrationCommands({ integrationClass }) {
|
|
42
42
|
if (!integrationClass) {
|
|
43
43
|
throw new Error('integrationClass is required');
|
|
44
44
|
}
|
package/application/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const {
|
|
|
23
23
|
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
24
24
|
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
25
25
|
*/
|
|
26
|
-
function createFriggCommands({ integrationClass }
|
|
26
|
+
function createFriggCommands({ integrationClass }) {
|
|
27
27
|
// All commands use Frigg's default repositories and use cases
|
|
28
28
|
const integrationCommands = createIntegrationCommands({ integrationClass });
|
|
29
29
|
|
package/core/create-handler.js
CHANGED
|
@@ -39,6 +39,18 @@ const createHandler = (optionByName = {}) => {
|
|
|
39
39
|
|
|
40
40
|
// Don't leak implementation details to end users.
|
|
41
41
|
if (isUserFacingResponse) {
|
|
42
|
+
// Allow client-safe errors to pass through with their actual message
|
|
43
|
+
if (error.isClientSafe === true) {
|
|
44
|
+
const statusCode = error.statusCode || 400;
|
|
45
|
+
return {
|
|
46
|
+
statusCode,
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
error: error.message,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Hide other errors with generic message
|
|
42
54
|
return {
|
|
43
55
|
statusCode: 500,
|
|
44
56
|
body: JSON.stringify({
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
toObjectId,
|
|
4
|
+
fromObjectId,
|
|
5
|
+
findOne,
|
|
6
|
+
insertOne,
|
|
7
|
+
updateOne,
|
|
8
|
+
deleteOne,
|
|
9
|
+
} = require('../../database/documentdb-utils');
|
|
10
|
+
const {
|
|
11
|
+
CredentialRepositoryInterface,
|
|
12
|
+
} = require('./credential-repository-interface');
|
|
13
|
+
const {
|
|
14
|
+
DocumentDBEncryptionService,
|
|
15
|
+
} = require('../../database/documentdb-encryption-service');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Credential repository for DocumentDB.
|
|
19
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
20
|
+
*
|
|
21
|
+
* Encrypted fields:
|
|
22
|
+
* - Credential.data.access_token
|
|
23
|
+
* - Credential.data.refresh_token
|
|
24
|
+
* - Credential.data.id_token
|
|
25
|
+
*
|
|
26
|
+
* SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
|
|
27
|
+
*
|
|
28
|
+
* @see DocumentDBEncryptionService
|
|
29
|
+
* @see encryption-schema-registry.js
|
|
30
|
+
*/
|
|
31
|
+
class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.prisma = prisma;
|
|
35
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async findCredentialById(id) {
|
|
39
|
+
const objectId = toObjectId(id);
|
|
40
|
+
if (!objectId) return null;
|
|
41
|
+
const doc = await findOne(this.prisma, 'Credential', { _id: objectId });
|
|
42
|
+
if (!doc) return null;
|
|
43
|
+
|
|
44
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
45
|
+
'Credential',
|
|
46
|
+
doc
|
|
47
|
+
);
|
|
48
|
+
return this._mapCredentialById(decryptedCredential);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async updateAuthenticationStatus(credentialId, authIsValid) {
|
|
52
|
+
const objectId = toObjectId(credentialId);
|
|
53
|
+
if (!objectId) return { acknowledged: false, modifiedCount: 0 };
|
|
54
|
+
const result = await updateOne(
|
|
55
|
+
this.prisma,
|
|
56
|
+
'Credential',
|
|
57
|
+
{ _id: objectId },
|
|
58
|
+
{
|
|
59
|
+
$set: { authIsValid, updatedAt: new Date() },
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
const modified = result?.nModified ?? result?.n ?? 0;
|
|
63
|
+
return { acknowledged: true, modifiedCount: modified };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deleteCredentialById(credentialId) {
|
|
67
|
+
const objectId = toObjectId(credentialId);
|
|
68
|
+
if (!objectId) return { acknowledged: true, deletedCount: 0 };
|
|
69
|
+
const result = await deleteOne(this.prisma, 'Credential', {
|
|
70
|
+
_id: objectId,
|
|
71
|
+
});
|
|
72
|
+
const deleted = result?.n ?? 0;
|
|
73
|
+
return { acknowledged: true, deletedCount: deleted };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async upsertCredential(credentialDetails) {
|
|
77
|
+
const { identifiers, details } = credentialDetails;
|
|
78
|
+
if (!identifiers)
|
|
79
|
+
throw new Error('identifiers required to upsert credential');
|
|
80
|
+
if (!identifiers.userId) {
|
|
81
|
+
throw new Error('userId required in identifiers');
|
|
82
|
+
}
|
|
83
|
+
if (!identifiers.externalId) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'externalId required in identifiers to prevent credential collision. When multiple credentials exist for the same user, both userId and externalId are needed to uniquely identify which credential to update.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const filter = this._buildIdentifierFilter(identifiers);
|
|
90
|
+
const existing = await findOne(this.prisma, 'Credential', filter);
|
|
91
|
+
const now = new Date();
|
|
92
|
+
|
|
93
|
+
const { authIsValid, ...oauthData } = details || {};
|
|
94
|
+
|
|
95
|
+
if (existing) {
|
|
96
|
+
const decryptedExisting =
|
|
97
|
+
await this.encryptionService.decryptFields(
|
|
98
|
+
'Credential',
|
|
99
|
+
existing
|
|
100
|
+
);
|
|
101
|
+
const mergedData = {
|
|
102
|
+
...(decryptedExisting.data || {}),
|
|
103
|
+
...oauthData,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const updateDocument = {
|
|
107
|
+
userId: existing.userId,
|
|
108
|
+
externalId: existing.externalId,
|
|
109
|
+
authIsValid: authIsValid,
|
|
110
|
+
data: mergedData,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const encryptedUpdate = await this.encryptionService.encryptFields(
|
|
115
|
+
'Credential',
|
|
116
|
+
{ data: updateDocument.data }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await updateOne(
|
|
120
|
+
this.prisma,
|
|
121
|
+
'Credential',
|
|
122
|
+
{ _id: existing._id },
|
|
123
|
+
{
|
|
124
|
+
$set: {
|
|
125
|
+
userId: updateDocument.userId,
|
|
126
|
+
externalId: updateDocument.externalId,
|
|
127
|
+
authIsValid: updateDocument.authIsValid,
|
|
128
|
+
data: encryptedUpdate.data,
|
|
129
|
+
updatedAt: updateDocument.updatedAt,
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const updated = await findOne(this.prisma, 'Credential', {
|
|
135
|
+
_id: existing._id,
|
|
136
|
+
});
|
|
137
|
+
const decryptedCredential =
|
|
138
|
+
await this.encryptionService.decryptFields(
|
|
139
|
+
'Credential',
|
|
140
|
+
updated
|
|
141
|
+
);
|
|
142
|
+
return this._mapCredential(decryptedCredential);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const plainDocument = {
|
|
146
|
+
userId: identifiers.userId,
|
|
147
|
+
externalId: identifiers.externalId,
|
|
148
|
+
authIsValid: details.authIsValid,
|
|
149
|
+
data: { ...oauthData },
|
|
150
|
+
createdAt: now,
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
155
|
+
'Credential',
|
|
156
|
+
plainDocument
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const insertedId = await insertOne(
|
|
160
|
+
this.prisma,
|
|
161
|
+
'Credential',
|
|
162
|
+
encryptedDocument
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const created = await findOne(this.prisma, 'Credential', {
|
|
166
|
+
_id: insertedId,
|
|
167
|
+
});
|
|
168
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
169
|
+
'Credential',
|
|
170
|
+
created
|
|
171
|
+
);
|
|
172
|
+
return this._mapCredential(decryptedCredential);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async findCredential(filter) {
|
|
176
|
+
const query = this._buildFilter(filter);
|
|
177
|
+
const credential = await findOne(this.prisma, 'Credential', query);
|
|
178
|
+
if (!credential) return null;
|
|
179
|
+
|
|
180
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
181
|
+
'Credential',
|
|
182
|
+
credential
|
|
183
|
+
);
|
|
184
|
+
return this._mapCredential(decryptedCredential);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async updateCredential(credentialId, updates) {
|
|
188
|
+
const objectId = toObjectId(credentialId);
|
|
189
|
+
if (!objectId) return null;
|
|
190
|
+
const existing = await findOne(this.prisma, 'Credential', {
|
|
191
|
+
_id: objectId,
|
|
192
|
+
});
|
|
193
|
+
if (!existing) return null;
|
|
194
|
+
|
|
195
|
+
const { authIsValid, ...oauthData } = updates || {};
|
|
196
|
+
|
|
197
|
+
const decryptedExisting = await this.encryptionService.decryptFields(
|
|
198
|
+
'Credential',
|
|
199
|
+
existing
|
|
200
|
+
);
|
|
201
|
+
const mergedData = { ...(decryptedExisting.data || {}), ...oauthData };
|
|
202
|
+
|
|
203
|
+
const updateDocument = {
|
|
204
|
+
userId: existing.userId,
|
|
205
|
+
externalId: existing.externalId,
|
|
206
|
+
authIsValid: authIsValid,
|
|
207
|
+
data: mergedData,
|
|
208
|
+
updatedAt: new Date(),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const encryptedUpdate = await this.encryptionService.encryptFields(
|
|
212
|
+
'Credential',
|
|
213
|
+
{ data: updateDocument.data }
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await updateOne(
|
|
217
|
+
this.prisma,
|
|
218
|
+
'Credential',
|
|
219
|
+
{ _id: objectId },
|
|
220
|
+
{
|
|
221
|
+
$set: {
|
|
222
|
+
userId: updateDocument.userId,
|
|
223
|
+
externalId: updateDocument.externalId,
|
|
224
|
+
authIsValid: updateDocument.authIsValid,
|
|
225
|
+
data: encryptedUpdate.data,
|
|
226
|
+
updatedAt: updateDocument.updatedAt,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const updated = await findOne(this.prisma, 'Credential', {
|
|
232
|
+
_id: objectId,
|
|
233
|
+
});
|
|
234
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
235
|
+
'Credential',
|
|
236
|
+
updated
|
|
237
|
+
);
|
|
238
|
+
return this._mapCredential(decryptedCredential);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_buildIdentifierFilter(identifiers) {
|
|
242
|
+
const filter = {};
|
|
243
|
+
if (identifiers._id || identifiers.id) {
|
|
244
|
+
const idObj = toObjectId(identifiers._id || identifiers.id);
|
|
245
|
+
if (idObj) filter._id = idObj;
|
|
246
|
+
}
|
|
247
|
+
if (identifiers.userId) {
|
|
248
|
+
filter.userId = identifiers.userId;
|
|
249
|
+
}
|
|
250
|
+
if (identifiers.externalId !== undefined) {
|
|
251
|
+
filter.externalId = identifiers.externalId;
|
|
252
|
+
}
|
|
253
|
+
return filter;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_buildFilter(filter) {
|
|
257
|
+
const query = {};
|
|
258
|
+
if (!filter) return query;
|
|
259
|
+
if (filter.credentialId || filter.id) {
|
|
260
|
+
const idObj = toObjectId(filter.credentialId || filter.id);
|
|
261
|
+
if (idObj) query._id = idObj;
|
|
262
|
+
}
|
|
263
|
+
if (filter.userId !== undefined) {
|
|
264
|
+
query.userId = filter.userId;
|
|
265
|
+
}
|
|
266
|
+
if (filter.externalId !== undefined) {
|
|
267
|
+
query.externalId = filter.externalId;
|
|
268
|
+
}
|
|
269
|
+
return query;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Map credential document to application format
|
|
274
|
+
* Matches MongoDB repository format
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
277
|
+
_mapCredential(doc) {
|
|
278
|
+
const data = doc?.data || {};
|
|
279
|
+
const id = fromObjectId(doc?._id);
|
|
280
|
+
const userId = doc?.userId;
|
|
281
|
+
return {
|
|
282
|
+
id,
|
|
283
|
+
userId,
|
|
284
|
+
externalId: doc?.externalId ?? null,
|
|
285
|
+
authIsValid: doc?.authIsValid ?? null,
|
|
286
|
+
...data,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_mapCredentialById(doc) {
|
|
291
|
+
const data = doc?.data || {};
|
|
292
|
+
const id = fromObjectId(doc?._id);
|
|
293
|
+
const userId = doc?.userId;
|
|
294
|
+
return {
|
|
295
|
+
id,
|
|
296
|
+
userId,
|
|
297
|
+
externalId: doc?.externalId ?? null,
|
|
298
|
+
authIsValid: doc?.authIsValid ?? null,
|
|
299
|
+
...data,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = { CredentialRepositoryDocumentDB };
|
|
@@ -2,6 +2,9 @@ const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
|
|
|
2
2
|
const {
|
|
3
3
|
CredentialRepositoryPostgres,
|
|
4
4
|
} = require('./credential-repository-postgres');
|
|
5
|
+
const {
|
|
6
|
+
CredentialRepositoryDocumentDB,
|
|
7
|
+
} = require('./credential-repository-documentdb');
|
|
5
8
|
const config = require('../../database/config');
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -32,9 +35,12 @@ function createCredentialRepository() {
|
|
|
32
35
|
case 'postgresql':
|
|
33
36
|
return new CredentialRepositoryPostgres();
|
|
34
37
|
|
|
38
|
+
case 'documentdb':
|
|
39
|
+
return new CredentialRepositoryDocumentDB();
|
|
40
|
+
|
|
35
41
|
default:
|
|
36
42
|
throw new Error(
|
|
37
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
43
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
38
44
|
);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -44,4 +50,5 @@ module.exports = {
|
|
|
44
50
|
// Export adapters for direct testing
|
|
45
51
|
CredentialRepositoryMongo,
|
|
46
52
|
CredentialRepositoryPostgres,
|
|
53
|
+
CredentialRepositoryDocumentDB,
|
|
47
54
|
};
|
|
@@ -38,9 +38,7 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
38
38
|
const data = credential.data || {};
|
|
39
39
|
|
|
40
40
|
return {
|
|
41
|
-
_id: credential.id,
|
|
42
41
|
id: credential.id,
|
|
43
|
-
user: credential.userId,
|
|
44
42
|
userId: credential.userId,
|
|
45
43
|
externalId: credential.externalId,
|
|
46
44
|
authIsValid: credential.authIsValid,
|
|
@@ -80,7 +78,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
80
78
|
return { acknowledged: true, deletedCount: 1 };
|
|
81
79
|
} catch (error) {
|
|
82
80
|
if (error.code === 'P2025') {
|
|
83
|
-
// Record not found
|
|
84
81
|
return { acknowledged: true, deletedCount: 0 };
|
|
85
82
|
}
|
|
86
83
|
throw error;
|
|
@@ -99,49 +96,32 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
99
96
|
if (!identifiers)
|
|
100
97
|
throw new Error('identifiers required to upsert credential');
|
|
101
98
|
|
|
102
|
-
if (!identifiers.
|
|
103
|
-
throw new Error('
|
|
99
|
+
if (!identifiers.userId) {
|
|
100
|
+
throw new Error('userId required in identifiers');
|
|
104
101
|
}
|
|
105
102
|
if (!identifiers.externalId) {
|
|
106
103
|
throw new Error(
|
|
107
104
|
'externalId required in identifiers to prevent credential collision. ' +
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
'When multiple credentials exist for the same user, both userId and externalId ' +
|
|
106
|
+
'are needed to uniquely identify which credential to update.'
|
|
110
107
|
);
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
// Build where clause from identifiers
|
|
114
110
|
const where = this._convertIdentifiersToWhere(identifiers);
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
user,
|
|
119
|
-
userId,
|
|
120
|
-
externalId,
|
|
121
|
-
authIsValid,
|
|
122
|
-
|
|
123
|
-
...oauthData
|
|
124
|
-
} = details;
|
|
125
|
-
|
|
126
|
-
// Find existing credential
|
|
112
|
+
const { authIsValid, ...oauthData } = details;
|
|
113
|
+
|
|
127
114
|
const existing = await this.prisma.credential.findFirst({ where });
|
|
128
115
|
|
|
129
116
|
if (existing) {
|
|
130
|
-
// Update existing - merge OAuth data into existing data JSON
|
|
131
117
|
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
132
118
|
|
|
133
119
|
const updated = await this.prisma.credential.update({
|
|
134
120
|
where: { id: existing.id },
|
|
135
121
|
data: {
|
|
136
|
-
userId:
|
|
137
|
-
externalId:
|
|
138
|
-
|
|
139
|
-
? externalId
|
|
140
|
-
: existing.externalId,
|
|
141
|
-
authIsValid:
|
|
142
|
-
authIsValid !== undefined
|
|
143
|
-
? authIsValid
|
|
144
|
-
: existing.authIsValid,
|
|
122
|
+
userId: existing.userId,
|
|
123
|
+
externalId: existing.externalId,
|
|
124
|
+
authIsValid: authIsValid,
|
|
145
125
|
data: mergedData,
|
|
146
126
|
},
|
|
147
127
|
});
|
|
@@ -155,13 +135,11 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
155
135
|
};
|
|
156
136
|
}
|
|
157
137
|
|
|
158
|
-
// Create new credential
|
|
159
138
|
const created = await this.prisma.credential.create({
|
|
160
139
|
data: {
|
|
161
|
-
userId: userId
|
|
162
|
-
externalId,
|
|
140
|
+
userId: identifiers.userId,
|
|
141
|
+
externalId: identifiers.externalId,
|
|
163
142
|
authIsValid: authIsValid,
|
|
164
|
-
|
|
165
143
|
data: oauthData,
|
|
166
144
|
},
|
|
167
145
|
});
|
|
@@ -205,7 +183,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
205
183
|
authIsValid: credential.authIsValid,
|
|
206
184
|
access_token: data.access_token,
|
|
207
185
|
refresh_token: data.refresh_token,
|
|
208
|
-
domain: data.domain,
|
|
209
186
|
...data,
|
|
210
187
|
};
|
|
211
188
|
}
|
|
@@ -219,7 +196,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
219
196
|
* @returns {Promise<Object|null>} Updated credential object or null if not found
|
|
220
197
|
*/
|
|
221
198
|
async updateCredential(credentialId, updates) {
|
|
222
|
-
// Get existing credential to merge OAuth data
|
|
223
199
|
const existing = await this.prisma.credential.findUnique({
|
|
224
200
|
where: { id: credentialId },
|
|
225
201
|
});
|
|
@@ -228,27 +204,16 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
228
204
|
return null;
|
|
229
205
|
}
|
|
230
206
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
user,
|
|
234
|
-
userId,
|
|
235
|
-
externalId,
|
|
236
|
-
authIsValid,
|
|
237
|
-
|
|
238
|
-
...oauthData
|
|
239
|
-
} = updates;
|
|
240
|
-
|
|
241
|
-
// Merge OAuth data with existing
|
|
207
|
+
const { authIsValid, ...oauthData } = updates;
|
|
208
|
+
|
|
242
209
|
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
243
210
|
|
|
244
211
|
const updated = await this.prisma.credential.update({
|
|
245
212
|
where: { id: credentialId },
|
|
246
213
|
data: {
|
|
247
|
-
userId:
|
|
248
|
-
externalId:
|
|
249
|
-
|
|
250
|
-
authIsValid:
|
|
251
|
-
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
214
|
+
userId: existing.userId,
|
|
215
|
+
externalId: existing.externalId,
|
|
216
|
+
authIsValid: authIsValid,
|
|
252
217
|
data: mergedData,
|
|
253
218
|
},
|
|
254
219
|
});
|
|
@@ -262,7 +227,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
262
227
|
authIsValid: updated.authIsValid,
|
|
263
228
|
access_token: data.access_token,
|
|
264
229
|
refresh_token: data.refresh_token,
|
|
265
|
-
domain: data.domain,
|
|
266
230
|
...data,
|
|
267
231
|
};
|
|
268
232
|
}
|
|
@@ -278,7 +242,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
278
242
|
|
|
279
243
|
if (identifiers._id) where.id = identifiers._id;
|
|
280
244
|
if (identifiers.id) where.id = identifiers.id;
|
|
281
|
-
if (identifiers.user) where.userId = identifiers.user;
|
|
282
245
|
if (identifiers.userId) where.userId = identifiers.userId;
|
|
283
246
|
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
284
247
|
|
|
@@ -296,7 +259,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
296
259
|
|
|
297
260
|
if (filter.credentialId) where.id = filter.credentialId;
|
|
298
261
|
if (filter.id) where.id = filter.id;
|
|
299
|
-
if (filter.user) where.userId = filter.user;
|
|
300
262
|
if (filter.userId) where.userId = filter.userId;
|
|
301
263
|
if (filter.externalId) where.externalId = filter.externalId;
|
|
302
264
|
|