@friggframework/core 2.0.0-next.53 → 2.0.0-next.54
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/integration-commands.js +1 -1
- package/application/index.js +1 -1
- package/credential/repositories/credential-repository-documentdb.js +300 -0
- package/credential/repositories/credential-repository-factory.js +8 -1
- 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 -0
- 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/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/repositories/integration-mapping-repository-documentdb.js +135 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
- package/integrations/repositories/integration-repository-documentdb.js +189 -0
- package/integrations/repositories/integration-repository-factory.js +8 -1
- package/integrations/repositories/process-repository-documentdb.js +141 -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 +125 -0
- package/token/repositories/token-repository-factory.js +8 -1
- package/user/repositories/user-repository-documentdb.js +292 -0
- package/user/repositories/user-repository-factory.js +6 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { SyncRepositoryMongo } = require('./sync-repository-mongo');
|
|
2
2
|
const { SyncRepositoryPostgres } = require('./sync-repository-postgres');
|
|
3
|
+
const { SyncRepositoryDocumentDB } = require('./sync-repository-documentdb');
|
|
3
4
|
const config = require('../../database/config');
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -23,9 +24,12 @@ function createSyncRepository() {
|
|
|
23
24
|
case 'postgresql':
|
|
24
25
|
return new SyncRepositoryPostgres();
|
|
25
26
|
|
|
27
|
+
case 'documentdb':
|
|
28
|
+
return new SyncRepositoryDocumentDB();
|
|
29
|
+
|
|
26
30
|
default:
|
|
27
31
|
throw new Error(
|
|
28
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
32
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
29
33
|
);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -35,4 +39,5 @@ module.exports = {
|
|
|
35
39
|
// Export adapters for direct testing
|
|
36
40
|
SyncRepositoryMongo,
|
|
37
41
|
SyncRepositoryPostgres,
|
|
42
|
+
SyncRepositoryDocumentDB,
|
|
38
43
|
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const {
|
|
4
|
+
toObjectId,
|
|
5
|
+
fromObjectId,
|
|
6
|
+
findMany,
|
|
7
|
+
findOne,
|
|
8
|
+
insertOne,
|
|
9
|
+
deleteOne,
|
|
10
|
+
deleteMany,
|
|
11
|
+
} = require('../../database/documentdb-utils');
|
|
12
|
+
const { TokenRepositoryInterface } = require('./token-repository-interface');
|
|
13
|
+
|
|
14
|
+
const BCRYPT_ROUNDS = 10;
|
|
15
|
+
|
|
16
|
+
class TokenRepositoryDocumentDB extends TokenRepositoryInterface {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.prisma = prisma;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async createTokenWithExpire(userId, rawToken, minutes) {
|
|
23
|
+
const tokenHash = await bcrypt.hash(rawToken, BCRYPT_ROUNDS);
|
|
24
|
+
const expires = new Date(Date.now() + minutes * 60000);
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const document = {
|
|
27
|
+
token: tokenHash,
|
|
28
|
+
expires,
|
|
29
|
+
userId: toObjectId(userId),
|
|
30
|
+
created: now,
|
|
31
|
+
};
|
|
32
|
+
const insertedId = await insertOne(this.prisma, 'Token', document);
|
|
33
|
+
const created = await findOne(this.prisma, 'Token', { _id: insertedId });
|
|
34
|
+
return this._mapToken(created);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async validateAndGetToken(tokenObj) {
|
|
38
|
+
const objectId = toObjectId(tokenObj.id);
|
|
39
|
+
if (!objectId) {
|
|
40
|
+
throw new Error('Invalid Token: Token does not exist');
|
|
41
|
+
}
|
|
42
|
+
const record = await findOne(this.prisma, 'Token', { _id: objectId });
|
|
43
|
+
if (!record) {
|
|
44
|
+
throw new Error('Invalid Token: Token does not exist');
|
|
45
|
+
}
|
|
46
|
+
const isValid = await bcrypt.compare(tokenObj.token, record.token);
|
|
47
|
+
if (!isValid) {
|
|
48
|
+
throw new Error('Invalid Token: Token does not match');
|
|
49
|
+
}
|
|
50
|
+
if (record.expires && new Date(record.expires) < new Date()) {
|
|
51
|
+
throw new Error('Invalid Token: Token is expired');
|
|
52
|
+
}
|
|
53
|
+
return this._mapToken(record);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async findTokenById(tokenId) {
|
|
57
|
+
const objectId = toObjectId(tokenId);
|
|
58
|
+
if (!objectId) return null;
|
|
59
|
+
const record = await findOne(this.prisma, 'Token', { _id: objectId });
|
|
60
|
+
return record ? this._mapToken(record) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async findTokensByUserId(userId) {
|
|
64
|
+
const objectId = toObjectId(userId);
|
|
65
|
+
const filter = objectId ? { userId: objectId } : {};
|
|
66
|
+
const records = await findMany(this.prisma, 'Token', filter);
|
|
67
|
+
return records.map((record) => this._mapToken(record));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async deleteToken(tokenId) {
|
|
71
|
+
const objectId = toObjectId(tokenId);
|
|
72
|
+
if (!objectId) return { acknowledged: true, deletedCount: 0 };
|
|
73
|
+
const result = await deleteOne(this.prisma, 'Token', { _id: objectId });
|
|
74
|
+
const deleted = result?.n ?? 0;
|
|
75
|
+
return { acknowledged: true, deletedCount: deleted };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async deleteExpiredTokens() {
|
|
79
|
+
const result = await deleteMany(this.prisma, 'Token', {
|
|
80
|
+
expires: { $lt: new Date() },
|
|
81
|
+
});
|
|
82
|
+
const deleted = result?.n ?? 0;
|
|
83
|
+
return { acknowledged: true, deletedCount: deleted };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async deleteTokensByUserId(userId) {
|
|
87
|
+
const objectId = toObjectId(userId);
|
|
88
|
+
if (!objectId) return { acknowledged: true, deletedCount: 0 };
|
|
89
|
+
const result = await deleteMany(this.prisma, 'Token', { userId: objectId });
|
|
90
|
+
const deleted = result?.n ?? 0;
|
|
91
|
+
return { acknowledged: true, deletedCount: deleted };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createJSONToken(token, rawToken) {
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
id: token.id,
|
|
97
|
+
token: rawToken,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
createBase64BufferToken(token, rawToken) {
|
|
102
|
+
const jsonVal = this.createJSONToken(token, rawToken);
|
|
103
|
+
return Buffer.from(jsonVal).toString('base64');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getJSONTokenFromBase64BufferToken(buffer) {
|
|
107
|
+
const tokenStr = Buffer.from(buffer.trim(), 'base64').toString('ascii');
|
|
108
|
+
return JSON.parse(tokenStr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_mapToken(record) {
|
|
112
|
+
if (!record) return null;
|
|
113
|
+
return {
|
|
114
|
+
id: fromObjectId(record._id),
|
|
115
|
+
token: record.token,
|
|
116
|
+
expires: record.expires ? new Date(record.expires) : null,
|
|
117
|
+
userId: fromObjectId(record.userId),
|
|
118
|
+
created: record.created ? new Date(record.created) : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { TokenRepositoryDocumentDB };
|
|
124
|
+
|
|
125
|
+
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { TokenRepositoryMongo } = require('./token-repository-mongo');
|
|
2
2
|
const { TokenRepositoryPostgres } = require('./token-repository-postgres');
|
|
3
|
+
const {
|
|
4
|
+
TokenRepositoryDocumentDB,
|
|
5
|
+
} = require('./token-repository-documentdb');
|
|
3
6
|
const config = require('../../database/config');
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -18,9 +21,12 @@ function createTokenRepository() {
|
|
|
18
21
|
case 'postgresql':
|
|
19
22
|
return new TokenRepositoryPostgres();
|
|
20
23
|
|
|
24
|
+
case 'documentdb':
|
|
25
|
+
return new TokenRepositoryDocumentDB();
|
|
26
|
+
|
|
21
27
|
default:
|
|
22
28
|
throw new Error(
|
|
23
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
29
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
24
30
|
);
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -30,4 +36,5 @@ module.exports = {
|
|
|
30
36
|
// Export adapters for direct testing
|
|
31
37
|
TokenRepositoryMongo,
|
|
32
38
|
TokenRepositoryPostgres,
|
|
39
|
+
TokenRepositoryDocumentDB,
|
|
33
40
|
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const { prisma } = require('../../database/prisma');
|
|
3
|
+
const {
|
|
4
|
+
toObjectId,
|
|
5
|
+
fromObjectId,
|
|
6
|
+
findOne,
|
|
7
|
+
insertOne,
|
|
8
|
+
updateOne,
|
|
9
|
+
deleteOne,
|
|
10
|
+
} = require('../../database/documentdb-utils');
|
|
11
|
+
const { createTokenRepository } = require('../../token/repositories/token-repository-factory');
|
|
12
|
+
const { UserRepositoryInterface } = require('./user-repository-interface');
|
|
13
|
+
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User repository for DocumentDB.
|
|
17
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
18
|
+
*
|
|
19
|
+
* Encrypted fields: User.hashword
|
|
20
|
+
*
|
|
21
|
+
* @see DocumentDBEncryptionService
|
|
22
|
+
* @see encryption-schema-registry.js
|
|
23
|
+
*/
|
|
24
|
+
class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
this.prisma = prisma;
|
|
28
|
+
this.tokenRepository = createTokenRepository();
|
|
29
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getSessionToken(token) {
|
|
33
|
+
const jsonToken = this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
|
|
34
|
+
const sessionToken = await this.tokenRepository.validateAndGetToken(jsonToken);
|
|
35
|
+
return sessionToken;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async findOrganizationUserById(userId) {
|
|
39
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
40
|
+
_id: toObjectId(userId),
|
|
41
|
+
type: 'ORGANIZATION',
|
|
42
|
+
});
|
|
43
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
44
|
+
return this._mapUser(decrypted);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async findIndividualUserById(userId) {
|
|
48
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
49
|
+
_id: toObjectId(userId),
|
|
50
|
+
type: 'INDIVIDUAL',
|
|
51
|
+
});
|
|
52
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
53
|
+
return this._mapUser(decrypted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async createToken(userId, rawToken, minutes = 120) {
|
|
57
|
+
const createdToken = await this.tokenRepository.createTokenWithExpire(
|
|
58
|
+
fromObjectId(toObjectId(userId)),
|
|
59
|
+
rawToken,
|
|
60
|
+
minutes
|
|
61
|
+
);
|
|
62
|
+
return this.tokenRepository.createBase64BufferToken(createdToken, rawToken);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async createIndividualUser(params) {
|
|
66
|
+
const now = new Date();
|
|
67
|
+
const document = {
|
|
68
|
+
type: 'INDIVIDUAL',
|
|
69
|
+
email: params.email ?? null,
|
|
70
|
+
username: params.username ?? null,
|
|
71
|
+
appUserId: params.appUserId ?? null,
|
|
72
|
+
organizationId: params.organization
|
|
73
|
+
? toObjectId(params.organization)
|
|
74
|
+
: params.organizationId
|
|
75
|
+
? toObjectId(params.organizationId)
|
|
76
|
+
: null,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
params.hashword !== undefined &&
|
|
83
|
+
params.hashword !== null &&
|
|
84
|
+
params.hashword !== ''
|
|
85
|
+
) {
|
|
86
|
+
if (typeof params.hashword !== 'string') {
|
|
87
|
+
throw new Error('Password must be a string');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (params.hashword.startsWith('$2')) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Bcrypt hash the password
|
|
97
|
+
document.hashword = await bcrypt.hash(params.hashword, 10);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Encrypt sensitive fields before insert
|
|
101
|
+
const encryptedDocument = await this.encryptionService.encryptFields('User', document);
|
|
102
|
+
const insertedId = await insertOne(this.prisma, 'User', encryptedDocument);
|
|
103
|
+
const created = await findOne(this.prisma, 'User', { _id: insertedId });
|
|
104
|
+
|
|
105
|
+
// Defensive check: verify document was found after insert
|
|
106
|
+
if (!created) {
|
|
107
|
+
console.error('[UserRepositoryDocumentDB] User not found after insert', {
|
|
108
|
+
insertedId: fromObjectId(insertedId),
|
|
109
|
+
params: {
|
|
110
|
+
username: params.username,
|
|
111
|
+
appUserId: params.appUserId,
|
|
112
|
+
email: params.email
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
throw new Error(
|
|
116
|
+
'Failed to create individual user: Document not found after insert. ' +
|
|
117
|
+
'This indicates a database consistency issue.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Decrypt sensitive fields after read
|
|
122
|
+
const decrypted = await this.encryptionService.decryptFields('User', created);
|
|
123
|
+
|
|
124
|
+
return this._mapUser(decrypted);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async createOrganizationUser(params) {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const document = {
|
|
130
|
+
type: 'ORGANIZATION',
|
|
131
|
+
appOrgId: params.appOrgId ?? null,
|
|
132
|
+
name: params.name ?? null,
|
|
133
|
+
createdAt: now,
|
|
134
|
+
updatedAt: now,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const insertedId = await insertOne(this.prisma, 'User', document);
|
|
138
|
+
const created = await findOne(this.prisma, 'User', { _id: insertedId });
|
|
139
|
+
return this._mapUser(created);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async findIndividualUserByUsername(username) {
|
|
143
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
144
|
+
type: 'INDIVIDUAL',
|
|
145
|
+
username,
|
|
146
|
+
});
|
|
147
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
148
|
+
return this._mapUser(decrypted);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async findIndividualUserByAppUserId(appUserId) {
|
|
152
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
153
|
+
type: 'INDIVIDUAL',
|
|
154
|
+
appUserId,
|
|
155
|
+
});
|
|
156
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
157
|
+
return this._mapUser(decrypted);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async findOrganizationUserByAppOrgId(appOrgId) {
|
|
161
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
162
|
+
type: 'ORGANIZATION',
|
|
163
|
+
appOrgId,
|
|
164
|
+
});
|
|
165
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
166
|
+
return this._mapUser(decrypted);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async findUserById(userId) {
|
|
170
|
+
const doc = await findOne(this.prisma, 'User', { _id: toObjectId(userId) });
|
|
171
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
172
|
+
return this._mapUser(decrypted);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async findIndividualUserByEmail(email) {
|
|
176
|
+
const doc = await findOne(this.prisma, 'User', {
|
|
177
|
+
type: 'INDIVIDUAL',
|
|
178
|
+
email,
|
|
179
|
+
});
|
|
180
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
181
|
+
return this._mapUser(decrypted);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async updateIndividualUser(userId, updates) {
|
|
185
|
+
const objectId = toObjectId(userId);
|
|
186
|
+
if (!objectId) return null;
|
|
187
|
+
|
|
188
|
+
const payload = await this._prepareUpdatePayload(updates);
|
|
189
|
+
payload.updatedAt = new Date();
|
|
190
|
+
|
|
191
|
+
// Encrypt sensitive fields before update
|
|
192
|
+
const encryptedPayload = await this.encryptionService.encryptFields('User', payload);
|
|
193
|
+
|
|
194
|
+
await updateOne(
|
|
195
|
+
this.prisma,
|
|
196
|
+
'User',
|
|
197
|
+
{ _id: objectId, type: 'INDIVIDUAL' },
|
|
198
|
+
{ $set: encryptedPayload }
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const updated = await findOne(this.prisma, 'User', { _id: objectId });
|
|
202
|
+
const decrypted = await this.encryptionService.decryptFields('User', updated);
|
|
203
|
+
return this._mapUser(decrypted);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async updateOrganizationUser(userId, updates) {
|
|
207
|
+
const objectId = toObjectId(userId);
|
|
208
|
+
if (!objectId) return null;
|
|
209
|
+
|
|
210
|
+
const payload = { ...updates, updatedAt: new Date() };
|
|
211
|
+
|
|
212
|
+
await updateOne(
|
|
213
|
+
this.prisma,
|
|
214
|
+
'User',
|
|
215
|
+
{ _id: objectId, type: 'ORGANIZATION' },
|
|
216
|
+
{ $set: payload }
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const updated = await findOne(this.prisma, 'User', { _id: objectId });
|
|
220
|
+
const decrypted = await this.encryptionService.decryptFields('User', updated);
|
|
221
|
+
return this._mapUser(decrypted);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async deleteUser(userId) {
|
|
225
|
+
const objectId = toObjectId(userId);
|
|
226
|
+
if (!objectId) return false;
|
|
227
|
+
|
|
228
|
+
const result = await deleteOne(this.prisma, 'User', { _id: objectId });
|
|
229
|
+
const deleted = result?.n ?? 0;
|
|
230
|
+
return deleted > 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_mapUser(doc) {
|
|
234
|
+
if (!doc) {
|
|
235
|
+
console.warn('[UserRepositoryDocumentDB] _mapUser received null/undefined document');
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Use optional chaining for robustness
|
|
240
|
+
return {
|
|
241
|
+
id: fromObjectId(doc?._id),
|
|
242
|
+
type: doc?.type ?? null,
|
|
243
|
+
email: doc?.email ?? null,
|
|
244
|
+
username: doc?.username ?? null,
|
|
245
|
+
hashword: doc?.hashword ?? null,
|
|
246
|
+
appUserId: doc?.appUserId ?? null,
|
|
247
|
+
organizationId: doc?.organizationId ? fromObjectId(doc.organizationId) : null,
|
|
248
|
+
appOrgId: doc?.appOrgId ?? null,
|
|
249
|
+
name: doc?.name ?? null,
|
|
250
|
+
createdAt: doc?.createdAt ? new Date(doc.createdAt) : undefined,
|
|
251
|
+
updatedAt: doc?.updatedAt ? new Date(doc.updatedAt) : undefined,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async _prepareUpdatePayload(updates = {}) {
|
|
256
|
+
const payload = { ...updates };
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
payload.hashword !== undefined &&
|
|
260
|
+
payload.hashword !== null &&
|
|
261
|
+
payload.hashword !== ''
|
|
262
|
+
) {
|
|
263
|
+
if (typeof payload.hashword !== 'string') {
|
|
264
|
+
throw new Error('Password must be a string');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (payload.hashword.startsWith('$2')) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
payload.hashword = await bcrypt.hash(payload.hashword, 10);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (payload.organization !== undefined) {
|
|
277
|
+
payload.organizationId = toObjectId(payload.organization);
|
|
278
|
+
delete payload.organization;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (payload.organizationId !== undefined) {
|
|
282
|
+
payload.organizationId = payload.organizationId
|
|
283
|
+
? toObjectId(payload.organizationId)
|
|
284
|
+
: null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return payload;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = { UserRepositoryDocumentDB };
|
|
292
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { UserRepositoryMongo } = require('./user-repository-mongo');
|
|
2
2
|
const { UserRepositoryPostgres } = require('./user-repository-postgres');
|
|
3
|
+
const { UserRepositoryDocumentDB } = require('./user-repository-documentdb');
|
|
3
4
|
const databaseConfig = require('../../database/config');
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -31,9 +32,12 @@ function createUserRepository() {
|
|
|
31
32
|
case 'postgresql':
|
|
32
33
|
return new UserRepositoryPostgres();
|
|
33
34
|
|
|
35
|
+
case 'documentdb':
|
|
36
|
+
return new UserRepositoryDocumentDB();
|
|
37
|
+
|
|
34
38
|
default:
|
|
35
39
|
throw new Error(
|
|
36
|
-
`Unsupported DB_TYPE: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
40
|
+
`Unsupported DB_TYPE: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
37
41
|
);
|
|
38
42
|
}
|
|
39
43
|
}
|
|
@@ -43,4 +47,5 @@ module.exports = {
|
|
|
43
47
|
// Export adapters for direct testing
|
|
44
48
|
UserRepositoryMongo,
|
|
45
49
|
UserRepositoryPostgres,
|
|
50
|
+
UserRepositoryDocumentDB,
|
|
46
51
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
ApiGatewayManagementApiClient,
|
|
4
|
+
PostToConnectionCommand,
|
|
5
|
+
} = require('@aws-sdk/client-apigatewaymanagementapi');
|
|
6
|
+
const {
|
|
7
|
+
toObjectId,
|
|
8
|
+
fromObjectId,
|
|
9
|
+
findMany,
|
|
10
|
+
findOne,
|
|
11
|
+
insertOne,
|
|
12
|
+
deleteOne,
|
|
13
|
+
deleteMany,
|
|
14
|
+
} = require('../../database/documentdb-utils');
|
|
15
|
+
const {
|
|
16
|
+
WebsocketConnectionRepositoryInterface,
|
|
17
|
+
} = require('./websocket-connection-repository-interface');
|
|
18
|
+
|
|
19
|
+
class WebsocketConnectionRepositoryDocumentDB extends WebsocketConnectionRepositoryInterface {
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.prisma = prisma;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createConnection(connectionId) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const document = {
|
|
28
|
+
connectionId,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
};
|
|
32
|
+
const insertedId = await insertOne(this.prisma, 'WebsocketConnection', document);
|
|
33
|
+
const created = await findOne(this.prisma, 'WebsocketConnection', { _id: insertedId });
|
|
34
|
+
return this._mapConnection(created);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async deleteConnection(connectionId) {
|
|
38
|
+
const result = await deleteOne(this.prisma, 'WebsocketConnection', { connectionId });
|
|
39
|
+
const deleted = result?.n ?? 0;
|
|
40
|
+
return { acknowledged: true, deletedCount: deleted };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getActiveConnections() {
|
|
44
|
+
if (!process.env.WEBSOCKET_API_ENDPOINT) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const connections = await findMany(
|
|
49
|
+
this.prisma,
|
|
50
|
+
'WebsocketConnection',
|
|
51
|
+
{},
|
|
52
|
+
{ projection: { connectionId: 1 } }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return connections.map((conn) => ({
|
|
56
|
+
connectionId: conn.connectionId,
|
|
57
|
+
send: async (data) => {
|
|
58
|
+
const apigwManagementApi = new ApiGatewayManagementApiClient({
|
|
59
|
+
endpoint: process.env.WEBSOCKET_API_ENDPOINT,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const command = new PostToConnectionCommand({
|
|
64
|
+
ConnectionId: conn.connectionId,
|
|
65
|
+
Data: JSON.stringify(data),
|
|
66
|
+
});
|
|
67
|
+
await apigwManagementApi.send(command);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
|
|
70
|
+
console.log(`Stale connection ${conn.connectionId}`);
|
|
71
|
+
await deleteMany(this.prisma, 'WebsocketConnection', {
|
|
72
|
+
connectionId: conn.connectionId,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async findConnection(connectionId) {
|
|
83
|
+
const doc = await findOne(this.prisma, 'WebsocketConnection', { connectionId });
|
|
84
|
+
return doc ? this._mapConnection(doc) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async findConnectionById(id) {
|
|
88
|
+
const objectId = toObjectId(id);
|
|
89
|
+
if (!objectId) return null;
|
|
90
|
+
const doc = await findOne(this.prisma, 'WebsocketConnection', { _id: objectId });
|
|
91
|
+
return doc ? this._mapConnection(doc) : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getAllConnections() {
|
|
95
|
+
const docs = await findMany(this.prisma, 'WebsocketConnection');
|
|
96
|
+
return docs.map((doc) => this._mapConnection(doc));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async deleteAllConnections() {
|
|
100
|
+
const result = await deleteMany(this.prisma, 'WebsocketConnection', {});
|
|
101
|
+
const deleted = result?.n ?? 0;
|
|
102
|
+
return {
|
|
103
|
+
acknowledged: true,
|
|
104
|
+
deletedCount: deleted,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_mapConnection(doc) {
|
|
109
|
+
if (!doc) return null;
|
|
110
|
+
return {
|
|
111
|
+
id: fromObjectId(doc._id),
|
|
112
|
+
connectionId: doc.connectionId,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { WebsocketConnectionRepositoryDocumentDB };
|
|
118
|
+
|
|
119
|
+
|
|
@@ -4,6 +4,9 @@ const {
|
|
|
4
4
|
const {
|
|
5
5
|
WebsocketConnectionRepositoryPostgres,
|
|
6
6
|
} = require('./websocket-connection-repository-postgres');
|
|
7
|
+
const {
|
|
8
|
+
WebsocketConnectionRepositoryDocumentDB,
|
|
9
|
+
} = require('./websocket-connection-repository-documentdb');
|
|
7
10
|
const config = require('../../database/config');
|
|
8
11
|
|
|
9
12
|
/**
|
|
@@ -22,9 +25,12 @@ function createWebsocketConnectionRepository() {
|
|
|
22
25
|
case 'postgresql':
|
|
23
26
|
return new WebsocketConnectionRepositoryPostgres();
|
|
24
27
|
|
|
28
|
+
case 'documentdb':
|
|
29
|
+
return new WebsocketConnectionRepositoryDocumentDB();
|
|
30
|
+
|
|
25
31
|
default:
|
|
26
32
|
throw new Error(
|
|
27
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
33
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
28
34
|
);
|
|
29
35
|
}
|
|
30
36
|
}
|
|
@@ -34,4 +40,5 @@ module.exports = {
|
|
|
34
40
|
// Export adapters for direct testing
|
|
35
41
|
WebsocketConnectionRepositoryMongo,
|
|
36
42
|
WebsocketConnectionRepositoryPostgres,
|
|
43
|
+
WebsocketConnectionRepositoryDocumentDB,
|
|
37
44
|
};
|