@friggframework/core 2.0.0-next.41 → 2.0.0-next.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
CredentialRepositoryInterface,
|
|
4
|
+
} = require('./credential-repository-interface');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PostgreSQL Credential Repository Adapter
|
|
8
|
+
* Handles OAuth credentials and API tokens persistence with PostgreSQL
|
|
9
|
+
*
|
|
10
|
+
* PostgreSQL-specific characteristics:
|
|
11
|
+
* - Uses Int IDs with autoincrement
|
|
12
|
+
* - Requires ID conversion: String (app layer) ↔ Int (database)
|
|
13
|
+
* - All returned IDs are converted to strings for application layer consistency
|
|
14
|
+
*/
|
|
15
|
+
class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.prisma = prisma;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert string ID to integer for PostgreSQL queries
|
|
23
|
+
* @private
|
|
24
|
+
* @param {string|number|null|undefined} id - ID to convert
|
|
25
|
+
* @returns {number|null|undefined} Integer ID or null/undefined
|
|
26
|
+
* @throws {Error} If ID cannot be converted to integer
|
|
27
|
+
*/
|
|
28
|
+
_convertId(id) {
|
|
29
|
+
if (id === null || id === undefined) return id;
|
|
30
|
+
const parsed = parseInt(id, 10);
|
|
31
|
+
if (isNaN(parsed)) {
|
|
32
|
+
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find credential by ID
|
|
39
|
+
* Replaces: Credential.findById(id)
|
|
40
|
+
*
|
|
41
|
+
* @param {string} id - Credential ID (string from application layer)
|
|
42
|
+
* @returns {Promise<Object|null>} Credential object with string IDs or null
|
|
43
|
+
*/
|
|
44
|
+
async findCredentialById(id) {
|
|
45
|
+
const intId = this._convertId(id);
|
|
46
|
+
const credential = await this.prisma.credential.findUnique({
|
|
47
|
+
where: { id: intId },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!credential) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract data from JSON field
|
|
55
|
+
const data = credential.data || {};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
_id: credential.id.toString(),
|
|
59
|
+
id: credential.id.toString(),
|
|
60
|
+
user: credential.userId?.toString(),
|
|
61
|
+
userId: credential.userId?.toString(),
|
|
62
|
+
externalId: credential.externalId,
|
|
63
|
+
authIsValid: credential.authIsValid,
|
|
64
|
+
subType: credential.subType,
|
|
65
|
+
...data, // Spread OAuth tokens from JSON field
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update authentication status
|
|
71
|
+
* Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
|
|
72
|
+
*
|
|
73
|
+
* @param {string} credentialId - Credential ID (string from application layer)
|
|
74
|
+
* @param {boolean} authIsValid - Authentication validity status
|
|
75
|
+
* @returns {Promise<Object>} Update result
|
|
76
|
+
*/
|
|
77
|
+
async updateAuthenticationStatus(credentialId, authIsValid) {
|
|
78
|
+
const intId = this._convertId(credentialId);
|
|
79
|
+
await this.prisma.credential.update({
|
|
80
|
+
where: { id: intId },
|
|
81
|
+
data: { authIsValid },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return { acknowledged: true, modifiedCount: 1 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Permanently remove a credential document
|
|
89
|
+
* Replaces: Credential.deleteOne({ _id: credentialId })
|
|
90
|
+
*
|
|
91
|
+
* @param {string} credentialId - Credential ID (string from application layer)
|
|
92
|
+
* @returns {Promise<Object>} Deletion result
|
|
93
|
+
*/
|
|
94
|
+
async deleteCredentialById(credentialId) {
|
|
95
|
+
try {
|
|
96
|
+
const intId = this._convertId(credentialId);
|
|
97
|
+
await this.prisma.credential.delete({
|
|
98
|
+
where: { id: intId },
|
|
99
|
+
});
|
|
100
|
+
return { acknowledged: true, deletedCount: 1 };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.code === 'P2025') {
|
|
103
|
+
// Record not found
|
|
104
|
+
return { acknowledged: true, deletedCount: 0 };
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create or update credential matching identifiers
|
|
112
|
+
* Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
|
|
113
|
+
*
|
|
114
|
+
* @param {{identifiers: Object, details: Object}} credentialDetails
|
|
115
|
+
* @returns {Promise<Object>} The persisted credential with string IDs
|
|
116
|
+
*/
|
|
117
|
+
async upsertCredential(credentialDetails) {
|
|
118
|
+
const { identifiers, details } = credentialDetails;
|
|
119
|
+
if (!identifiers)
|
|
120
|
+
throw new Error('identifiers required to upsert credential');
|
|
121
|
+
|
|
122
|
+
const where = this._convertIdentifiersToWhere(identifiers);
|
|
123
|
+
|
|
124
|
+
const { user, externalId } = identifiers;
|
|
125
|
+
|
|
126
|
+
// Separate schema fields from dynamic OAuth data
|
|
127
|
+
const { authIsValid, subType, ...oauthData } = details;
|
|
128
|
+
|
|
129
|
+
const existing = await this.prisma.credential.findFirst({ where });
|
|
130
|
+
|
|
131
|
+
if (existing) {
|
|
132
|
+
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
133
|
+
|
|
134
|
+
const updated = await this.prisma.credential.update({
|
|
135
|
+
where: { id: existing.id },
|
|
136
|
+
data: {
|
|
137
|
+
userId: this._convertId(user || existing.userId),
|
|
138
|
+
externalId:
|
|
139
|
+
externalId !== undefined
|
|
140
|
+
? externalId
|
|
141
|
+
: existing.externalId,
|
|
142
|
+
authIsValid:
|
|
143
|
+
authIsValid !== undefined
|
|
144
|
+
? authIsValid
|
|
145
|
+
: existing.authIsValid,
|
|
146
|
+
subType: subType !== undefined ? subType : existing.subType,
|
|
147
|
+
data: mergedData,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: updated.id.toString(),
|
|
153
|
+
externalId: updated.externalId,
|
|
154
|
+
userId: updated.userId?.toString(),
|
|
155
|
+
authIsValid: updated.authIsValid,
|
|
156
|
+
...(updated.data || {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const created = await this.prisma.credential.create({
|
|
161
|
+
data: {
|
|
162
|
+
userId: this._convertId(user),
|
|
163
|
+
externalId,
|
|
164
|
+
authIsValid: authIsValid,
|
|
165
|
+
subType,
|
|
166
|
+
data: oauthData,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
id: created.id.toString(),
|
|
172
|
+
externalId: created.externalId,
|
|
173
|
+
userId: created.userId?.toString(),
|
|
174
|
+
authIsValid: created.authIsValid,
|
|
175
|
+
...(created.data || {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find a credential by filter criteria
|
|
181
|
+
* Replaces: Credential.findOne(query)
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} filter
|
|
184
|
+
* @param {string} [filter.userId] - User ID (string from application layer)
|
|
185
|
+
* @param {string} [filter.externalId] - External ID
|
|
186
|
+
* @param {string} [filter.credentialId] - Credential ID (string from application layer)
|
|
187
|
+
* @returns {Promise<Object|null>} Credential object with string IDs or null if not found
|
|
188
|
+
*/
|
|
189
|
+
async findCredential(filter) {
|
|
190
|
+
const where = this._convertFilterToWhere(filter);
|
|
191
|
+
|
|
192
|
+
const credential = await this.prisma.credential.findFirst({
|
|
193
|
+
where,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!credential) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const data = credential.data || {};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
id: credential.id.toString(),
|
|
204
|
+
userId: credential.userId?.toString(),
|
|
205
|
+
externalId: credential.externalId,
|
|
206
|
+
authIsValid: credential.authIsValid,
|
|
207
|
+
access_token: data.access_token,
|
|
208
|
+
refresh_token: data.refresh_token,
|
|
209
|
+
domain: data.domain,
|
|
210
|
+
...data,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Update a credential by ID
|
|
216
|
+
* Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
|
|
217
|
+
*
|
|
218
|
+
* @param {string} credentialId - Credential ID (string from application layer)
|
|
219
|
+
* @param {Object} updates - Fields to update
|
|
220
|
+
* @returns {Promise<Object|null>} Updated credential object with string IDs or null if not found
|
|
221
|
+
*/
|
|
222
|
+
async updateCredential(credentialId, updates) {
|
|
223
|
+
// Get existing credential to merge OAuth data
|
|
224
|
+
const intId = this._convertId(credentialId);
|
|
225
|
+
const existing = await this.prisma.credential.findUnique({
|
|
226
|
+
where: { id: intId },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!existing) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Separate schema fields from OAuth data
|
|
234
|
+
const { user, authIsValid, subType, ...oauthData } =
|
|
235
|
+
updates;
|
|
236
|
+
|
|
237
|
+
// Merge OAuth data with existing
|
|
238
|
+
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
239
|
+
|
|
240
|
+
const updated = await this.prisma.credential.update({
|
|
241
|
+
where: { id: intId },
|
|
242
|
+
data: {
|
|
243
|
+
userId: this._convertId(userId || user || existing.userId),
|
|
244
|
+
externalId:
|
|
245
|
+
externalId !== undefined ? externalId : existing.externalId,
|
|
246
|
+
authIsValid:
|
|
247
|
+
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
248
|
+
subType: subType !== undefined ? subType : existing.subType,
|
|
249
|
+
data: mergedData,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const data = updated.data || {};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
id: updated.id.toString(),
|
|
257
|
+
userId: updated.userId?.toString(),
|
|
258
|
+
externalId: updated.externalId,
|
|
259
|
+
authIsValid: updated.authIsValid,
|
|
260
|
+
access_token: data.access_token,
|
|
261
|
+
refresh_token: data.refresh_token,
|
|
262
|
+
domain: data.domain,
|
|
263
|
+
...data,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Convert identifiers to Prisma where clause (converting IDs to Int)
|
|
269
|
+
* @private
|
|
270
|
+
* @param {Object} identifiers - Identifier fields
|
|
271
|
+
* @returns {Object} Prisma where clause with Int IDs
|
|
272
|
+
*/
|
|
273
|
+
_convertIdentifiersToWhere(identifiers) {
|
|
274
|
+
const where = {};
|
|
275
|
+
|
|
276
|
+
if (identifiers.id) where.id = this._convertId(identifiers.id);
|
|
277
|
+
if (identifiers.user) where.userId = this._convertId(identifiers.user);
|
|
278
|
+
if (identifiers.userId)
|
|
279
|
+
where.userId = this._convertId(identifiers.userId);
|
|
280
|
+
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
281
|
+
if (identifiers.subType) where.subType = identifiers.subType;
|
|
282
|
+
|
|
283
|
+
return where;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Convert filter to Prisma where clause (converting IDs to Int)
|
|
288
|
+
* @private
|
|
289
|
+
* @param {Object} filter - Filter criteria
|
|
290
|
+
* @returns {Object} Prisma where clause with Int IDs
|
|
291
|
+
*/
|
|
292
|
+
_convertFilterToWhere(filter) {
|
|
293
|
+
const where = {};
|
|
294
|
+
|
|
295
|
+
if (filter.credentialId)
|
|
296
|
+
where.id = this._convertId(filter.credentialId);
|
|
297
|
+
if (filter.id) where.id = this._convertId(filter.id);
|
|
298
|
+
if (filter.user) where.userId = this._convertId(filter.user);
|
|
299
|
+
if (filter.userId) where.userId = this._convertId(filter.userId);
|
|
300
|
+
if (filter.externalId) where.externalId = filter.externalId;
|
|
301
|
+
if (filter.subType) where.subType = filter.subType;
|
|
302
|
+
|
|
303
|
+
return where;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { CredentialRepositoryPostgres };
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
CredentialRepositoryInterface,
|
|
4
|
+
} = require('./credential-repository-interface');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prisma-based Credential Repository
|
|
8
|
+
* Handles OAuth credentials and API tokens persistence
|
|
9
|
+
*
|
|
10
|
+
* Works identically for both MongoDB and PostgreSQL:
|
|
11
|
+
* - MongoDB: String IDs with @db.ObjectId
|
|
12
|
+
* - PostgreSQL: Integer IDs with auto-increment
|
|
13
|
+
* - Both use same query patterns (no many-to-many differences)
|
|
14
|
+
*
|
|
15
|
+
* Migration from Mongoose:
|
|
16
|
+
* - Constructor injection of Prisma client
|
|
17
|
+
* - Dynamic schema (strict: false) → JSON field (data)
|
|
18
|
+
* - All OAuth tokens stored in data JSON field
|
|
19
|
+
* - Mongoose field names → Prisma field names (user → userId)
|
|
20
|
+
*/
|
|
21
|
+
class CredentialRepository extends CredentialRepositoryInterface {
|
|
22
|
+
constructor(prismaClient = prisma) {
|
|
23
|
+
super();
|
|
24
|
+
this.prisma = prismaClient; // Allow injection for testing
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find credential by ID
|
|
29
|
+
* Replaces: Credential.findById(id)
|
|
30
|
+
*
|
|
31
|
+
* @param {string} id - Credential ID
|
|
32
|
+
* @returns {Promise<Object|null>} Credential object or null
|
|
33
|
+
*/
|
|
34
|
+
async findCredentialById(id) {
|
|
35
|
+
const credential = await this.prisma.credential.findUnique({
|
|
36
|
+
where: { id },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!credential) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract data from JSON field
|
|
44
|
+
const data = credential.data || {};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
_id: credential.id,
|
|
48
|
+
id: credential.id,
|
|
49
|
+
user: credential.userId,
|
|
50
|
+
userId: credential.userId,
|
|
51
|
+
externalId: credential.externalId,
|
|
52
|
+
authIsValid: credential.authIsValid,
|
|
53
|
+
subType: credential.subType,
|
|
54
|
+
...data, // Spread OAuth tokens from JSON field
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update authentication status
|
|
60
|
+
* Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
|
|
61
|
+
*
|
|
62
|
+
* @param {string} credentialId - Credential ID
|
|
63
|
+
* @param {boolean} authIsValid - Authentication validity status
|
|
64
|
+
* @returns {Promise<Object>} Update result
|
|
65
|
+
*/
|
|
66
|
+
async updateAuthenticationStatus(credentialId, authIsValid) {
|
|
67
|
+
await this.prisma.credential.update({
|
|
68
|
+
where: { id: credentialId },
|
|
69
|
+
data: { authIsValid },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { acknowledged: true, modifiedCount: 1 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Permanently remove a credential document
|
|
77
|
+
* Replaces: Credential.deleteOne({ _id: credentialId })
|
|
78
|
+
*
|
|
79
|
+
* @param {string} credentialId - Credential ID
|
|
80
|
+
* @returns {Promise<Object>} Deletion result
|
|
81
|
+
*/
|
|
82
|
+
async deleteCredentialById(credentialId) {
|
|
83
|
+
try {
|
|
84
|
+
await this.prisma.credential.delete({
|
|
85
|
+
where: { id: credentialId },
|
|
86
|
+
});
|
|
87
|
+
return { acknowledged: true, deletedCount: 1 };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error.code === 'P2025') {
|
|
90
|
+
// Record not found
|
|
91
|
+
return { acknowledged: true, deletedCount: 0 };
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create or update credential matching identifiers
|
|
99
|
+
* Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
|
|
100
|
+
*
|
|
101
|
+
* @param {{identifiers: Object, details: Object}} credentialDetails
|
|
102
|
+
* @returns {Promise<Object>} The persisted credential
|
|
103
|
+
*/
|
|
104
|
+
async upsertCredential(credentialDetails) {
|
|
105
|
+
const { identifiers, details } = credentialDetails;
|
|
106
|
+
if (!identifiers)
|
|
107
|
+
throw new Error('identifiers required to upsert credential');
|
|
108
|
+
|
|
109
|
+
// Build where clause from identifiers
|
|
110
|
+
const where = this._convertIdentifiersToWhere(identifiers);
|
|
111
|
+
|
|
112
|
+
// Separate schema fields from dynamic OAuth data
|
|
113
|
+
const {
|
|
114
|
+
user,
|
|
115
|
+
userId,
|
|
116
|
+
externalId,
|
|
117
|
+
authIsValid,
|
|
118
|
+
subType,
|
|
119
|
+
...oauthData
|
|
120
|
+
} = details;
|
|
121
|
+
|
|
122
|
+
// Find existing credential
|
|
123
|
+
const existing = await this.prisma.credential.findFirst({ where });
|
|
124
|
+
|
|
125
|
+
if (existing) {
|
|
126
|
+
// Update existing - merge OAuth data into existing data JSON
|
|
127
|
+
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
128
|
+
|
|
129
|
+
const updated = await this.prisma.credential.update({
|
|
130
|
+
where: { id: existing.id },
|
|
131
|
+
data: {
|
|
132
|
+
userId: userId || user || existing.userId,
|
|
133
|
+
externalId:
|
|
134
|
+
externalId !== undefined
|
|
135
|
+
? externalId
|
|
136
|
+
: existing.externalId,
|
|
137
|
+
authIsValid:
|
|
138
|
+
authIsValid !== undefined
|
|
139
|
+
? authIsValid
|
|
140
|
+
: existing.authIsValid,
|
|
141
|
+
subType: subType !== undefined ? subType : existing.subType,
|
|
142
|
+
data: mergedData,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
id: updated.id,
|
|
148
|
+
externalId: updated.externalId,
|
|
149
|
+
userId: updated.userId,
|
|
150
|
+
authIsValid: updated.authIsValid,
|
|
151
|
+
...(updated.data || {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create new credential
|
|
156
|
+
const created = await this.prisma.credential.create({
|
|
157
|
+
data: {
|
|
158
|
+
userId: userId || user,
|
|
159
|
+
externalId,
|
|
160
|
+
authIsValid: authIsValid,
|
|
161
|
+
subType,
|
|
162
|
+
data: oauthData,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
id: created.id,
|
|
168
|
+
externalId: created.externalId,
|
|
169
|
+
userId: created.userId,
|
|
170
|
+
authIsValid: created.authIsValid,
|
|
171
|
+
...(created.data || {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Find a credential by filter criteria
|
|
177
|
+
* Replaces: Credential.findOne(query)
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} filter
|
|
180
|
+
* @param {string} [filter.userId] - User ID
|
|
181
|
+
* @param {string} [filter.externalId] - External ID
|
|
182
|
+
* @param {string} [filter.credentialId] - Credential ID
|
|
183
|
+
* @returns {Promise<Object|null>} Credential object or null if not found
|
|
184
|
+
*/
|
|
185
|
+
async findCredential(filter) {
|
|
186
|
+
const where = this._convertFilterToWhere(filter);
|
|
187
|
+
|
|
188
|
+
const credential = await this.prisma.credential.findFirst({
|
|
189
|
+
where,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!credential) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = credential.data || {};
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
id: credential.id,
|
|
200
|
+
userId: credential.userId,
|
|
201
|
+
externalId: credential.externalId,
|
|
202
|
+
authIsValid: credential.authIsValid,
|
|
203
|
+
access_token: data.access_token,
|
|
204
|
+
refresh_token: data.refresh_token,
|
|
205
|
+
domain: data.domain,
|
|
206
|
+
...data,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update a credential by ID
|
|
212
|
+
* Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
|
|
213
|
+
*
|
|
214
|
+
* @param {string} credentialId - Credential ID
|
|
215
|
+
* @param {Object} updates - Fields to update
|
|
216
|
+
* @returns {Promise<Object|null>} Updated credential object or null if not found
|
|
217
|
+
*/
|
|
218
|
+
async updateCredential(credentialId, updates) {
|
|
219
|
+
// Get existing credential to merge OAuth data
|
|
220
|
+
const existing = await this.prisma.credential.findUnique({
|
|
221
|
+
where: { id: credentialId },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!existing) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Separate schema fields from OAuth data
|
|
229
|
+
const {
|
|
230
|
+
user,
|
|
231
|
+
userId,
|
|
232
|
+
externalId,
|
|
233
|
+
authIsValid,
|
|
234
|
+
subType,
|
|
235
|
+
...oauthData
|
|
236
|
+
} = updates;
|
|
237
|
+
|
|
238
|
+
// Merge OAuth data with existing
|
|
239
|
+
const mergedData = { ...(existing.data || {}), ...oauthData };
|
|
240
|
+
|
|
241
|
+
const updated = await this.prisma.credential.update({
|
|
242
|
+
where: { id: credentialId },
|
|
243
|
+
data: {
|
|
244
|
+
userId: userId || user || existing.userId,
|
|
245
|
+
externalId:
|
|
246
|
+
externalId !== undefined ? externalId : existing.externalId,
|
|
247
|
+
authIsValid:
|
|
248
|
+
authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
249
|
+
subType: subType !== undefined ? subType : existing.subType,
|
|
250
|
+
data: mergedData,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const data = updated.data || {};
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
id: updated.id,
|
|
258
|
+
userId: updated.userId,
|
|
259
|
+
externalId: updated.externalId,
|
|
260
|
+
authIsValid: updated.authIsValid,
|
|
261
|
+
access_token: data.access_token,
|
|
262
|
+
refresh_token: data.refresh_token,
|
|
263
|
+
domain: data.domain,
|
|
264
|
+
...data,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Convert identifiers to Prisma where clause
|
|
270
|
+
* @private
|
|
271
|
+
* @param {Object} identifiers - Identifier fields
|
|
272
|
+
* @returns {Object} Prisma where clause
|
|
273
|
+
*/
|
|
274
|
+
_convertIdentifiersToWhere(identifiers) {
|
|
275
|
+
const where = {};
|
|
276
|
+
|
|
277
|
+
if (identifiers._id) where.id = identifiers._id;
|
|
278
|
+
if (identifiers.id) where.id = identifiers.id;
|
|
279
|
+
if (identifiers.user) where.userId = identifiers.user;
|
|
280
|
+
if (identifiers.userId) where.userId = identifiers.userId;
|
|
281
|
+
if (identifiers.externalId) where.externalId = identifiers.externalId;
|
|
282
|
+
if (identifiers.subType) where.subType = identifiers.subType;
|
|
283
|
+
|
|
284
|
+
return where;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convert filter to Prisma where clause
|
|
289
|
+
* @private
|
|
290
|
+
* @param {Object} filter - Filter criteria
|
|
291
|
+
* @returns {Object} Prisma where clause
|
|
292
|
+
*/
|
|
293
|
+
_convertFilterToWhere(filter) {
|
|
294
|
+
const where = {};
|
|
295
|
+
|
|
296
|
+
if (filter.credentialId) where.id = filter.credentialId;
|
|
297
|
+
if (filter.id) where.id = filter.id;
|
|
298
|
+
if (filter.user) where.userId = filter.user;
|
|
299
|
+
if (filter.userId) where.userId = filter.userId;
|
|
300
|
+
if (filter.externalId) where.externalId = filter.externalId;
|
|
301
|
+
if (filter.subType) where.subType = filter.subType;
|
|
302
|
+
|
|
303
|
+
return where;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { CredentialRepository };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class GetCredentialForUser {
|
|
2
|
+
constructor({ credentialRepository }) {
|
|
3
|
+
this.credentialRepository = credentialRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async execute(credentialId, userId) {
|
|
7
|
+
const credential = await this.credentialRepository.findCredentialById(credentialId);
|
|
8
|
+
|
|
9
|
+
if (!credential) {
|
|
10
|
+
throw new Error(`Credential with id ${credentialId} not found`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (credential.user.toString() !== userId.toString()) {
|
|
14
|
+
throw new Error(`Credential ${credentialId} does not belong to user ${userId}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return credential;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { GetCredentialForUser };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class UpdateAuthenticationStatus {
|
|
2
|
+
constructor({ credentialRepository }) {
|
|
3
|
+
this.credentialRepository = credentialRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} credentialId
|
|
8
|
+
* @param {boolean} authIsValid
|
|
9
|
+
*/
|
|
10
|
+
async execute(credentialId, authIsValid) {
|
|
11
|
+
await this.credentialRepository.updateAuthenticationStatus(credentialId, authIsValid);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { UpdateAuthenticationStatus };
|