@friggframework/core 2.0.0-next.41 → 2.0.0-next.43
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 +27 -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 +122 -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 +318 -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/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +300 -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,250 @@
|
|
|
1
|
+
//todo: this repository is tightly coupled to the token repository.
|
|
2
|
+
const { prisma } = require('../../database/prisma');
|
|
3
|
+
const {
|
|
4
|
+
createTokenRepository,
|
|
5
|
+
} = require('../../token/repositories/token-repository-factory');
|
|
6
|
+
const { UserRepositoryInterface } = require('./user-repository-interface');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MongoDB User Repository Adapter
|
|
10
|
+
* Handles user operations with discriminator pattern support
|
|
11
|
+
*
|
|
12
|
+
* MongoDB-specific characteristics:
|
|
13
|
+
* - Uses String IDs (ObjectId)
|
|
14
|
+
* - No ID conversion needed (IDs are already strings)
|
|
15
|
+
* - IndividualUser/OrganizationUser discriminators → User model with type field
|
|
16
|
+
*/
|
|
17
|
+
class UserRepositoryMongo extends UserRepositoryInterface {
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.prisma = prisma;
|
|
21
|
+
this.tokenRepository = createTokenRepository();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get session token from base64 buffer token
|
|
26
|
+
* Delegates to TokenRepository
|
|
27
|
+
*
|
|
28
|
+
* @param {string} token - Base64 buffer token
|
|
29
|
+
* @returns {Promise<Object>} Session token object with string IDs
|
|
30
|
+
*/
|
|
31
|
+
async getSessionToken(token) {
|
|
32
|
+
const jsonToken =
|
|
33
|
+
this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
|
|
34
|
+
const sessionToken = await this.tokenRepository.validateAndGetToken(
|
|
35
|
+
jsonToken
|
|
36
|
+
);
|
|
37
|
+
return sessionToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Find organization user by ID
|
|
42
|
+
* Replaces: OrganizationUser.findById(userId)
|
|
43
|
+
*
|
|
44
|
+
* @param {string} userId - User ID
|
|
45
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
46
|
+
*/
|
|
47
|
+
async findOrganizationUserById(userId) {
|
|
48
|
+
return await this.prisma.user.findFirst({
|
|
49
|
+
where: {
|
|
50
|
+
id: userId,
|
|
51
|
+
type: 'ORGANIZATION',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find individual user by ID
|
|
58
|
+
* Replaces: IndividualUser.findById(userId)
|
|
59
|
+
*
|
|
60
|
+
* @param {string} userId - User ID
|
|
61
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
62
|
+
*/
|
|
63
|
+
async findIndividualUserById(userId) {
|
|
64
|
+
return await this.prisma.user.findFirst({
|
|
65
|
+
where: {
|
|
66
|
+
id: userId,
|
|
67
|
+
type: 'INDIVIDUAL',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create token with expiration
|
|
74
|
+
* Delegates to TokenRepository
|
|
75
|
+
*
|
|
76
|
+
* @param {string} userId - User ID
|
|
77
|
+
* @param {string} rawToken - Raw unhashed token
|
|
78
|
+
* @param {number} minutes - Minutes until expiration (default 120)
|
|
79
|
+
* @returns {Promise<string>} Base64 buffer token
|
|
80
|
+
*/
|
|
81
|
+
async createToken(userId, rawToken, minutes = 120) {
|
|
82
|
+
const createdToken = await this.tokenRepository.createTokenWithExpire(
|
|
83
|
+
userId,
|
|
84
|
+
rawToken,
|
|
85
|
+
minutes
|
|
86
|
+
);
|
|
87
|
+
return this.tokenRepository.createBase64BufferToken(
|
|
88
|
+
createdToken,
|
|
89
|
+
rawToken
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create individual user
|
|
95
|
+
* Replaces: IndividualUser.create(params)
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} params - User creation parameters
|
|
98
|
+
* @returns {Promise<Object>} Created user object with string IDs
|
|
99
|
+
*/
|
|
100
|
+
async createIndividualUser(params) {
|
|
101
|
+
return await this.prisma.user.create({
|
|
102
|
+
data: {
|
|
103
|
+
type: 'INDIVIDUAL',
|
|
104
|
+
email: params.email,
|
|
105
|
+
username: params.username,
|
|
106
|
+
hashword: params.hashword,
|
|
107
|
+
appUserId: params.appUserId,
|
|
108
|
+
organizationId: params.organization || params.organizationId,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create organization user
|
|
115
|
+
* Replaces: OrganizationUser.create(params)
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} params - Organization creation parameters
|
|
118
|
+
* @returns {Promise<Object>} Created organization object with string IDs
|
|
119
|
+
*/
|
|
120
|
+
async createOrganizationUser(params) {
|
|
121
|
+
return await this.prisma.user.create({
|
|
122
|
+
data: {
|
|
123
|
+
type: 'ORGANIZATION',
|
|
124
|
+
appOrgId: params.appOrgId,
|
|
125
|
+
name: params.name,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find individual user by username
|
|
132
|
+
* Replaces: IndividualUser.findOne({ username })
|
|
133
|
+
*
|
|
134
|
+
* @param {string} username - Username to search for
|
|
135
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
136
|
+
*/
|
|
137
|
+
async findIndividualUserByUsername(username) {
|
|
138
|
+
return await this.prisma.user.findFirst({
|
|
139
|
+
where: {
|
|
140
|
+
type: 'INDIVIDUAL',
|
|
141
|
+
username,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Find individual user by app user ID
|
|
148
|
+
* Replaces: IndividualUser.getUserByAppUserId(appUserId)
|
|
149
|
+
*
|
|
150
|
+
* @param {string} appUserId - App user ID to search for
|
|
151
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
152
|
+
*/
|
|
153
|
+
async findIndividualUserByAppUserId(appUserId) {
|
|
154
|
+
return await this.prisma.user.findFirst({
|
|
155
|
+
where: {
|
|
156
|
+
type: 'INDIVIDUAL',
|
|
157
|
+
appUserId,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find organization user by app org ID
|
|
164
|
+
* Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
|
|
165
|
+
*
|
|
166
|
+
* @param {string} appOrgId - App organization ID to search for
|
|
167
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
168
|
+
*/
|
|
169
|
+
async findOrganizationUserByAppOrgId(appOrgId) {
|
|
170
|
+
return await this.prisma.user.findFirst({
|
|
171
|
+
where: {
|
|
172
|
+
type: 'ORGANIZATION',
|
|
173
|
+
appOrgId,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find user by ID (any type)
|
|
180
|
+
* @param {string} userId - User ID
|
|
181
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
182
|
+
*/
|
|
183
|
+
async findUserById(userId) {
|
|
184
|
+
return await this.prisma.user.findUnique({
|
|
185
|
+
where: { id: userId },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Find individual user by email
|
|
191
|
+
* @param {string} email - Email to search for
|
|
192
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
193
|
+
*/
|
|
194
|
+
async findIndividualUserByEmail(email) {
|
|
195
|
+
return await this.prisma.user.findFirst({
|
|
196
|
+
where: {
|
|
197
|
+
type: 'INDIVIDUAL',
|
|
198
|
+
email,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Update individual user
|
|
205
|
+
* @param {string} userId - User ID
|
|
206
|
+
* @param {Object} updates - Fields to update
|
|
207
|
+
* @returns {Promise<Object>} Updated user object with string IDs
|
|
208
|
+
*/
|
|
209
|
+
async updateIndividualUser(userId, updates) {
|
|
210
|
+
return await this.prisma.user.update({
|
|
211
|
+
where: { id: userId },
|
|
212
|
+
data: updates,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update organization user
|
|
218
|
+
* @param {string} userId - User ID
|
|
219
|
+
* @param {Object} updates - Fields to update
|
|
220
|
+
* @returns {Promise<Object>} Updated user object with string IDs
|
|
221
|
+
*/
|
|
222
|
+
async updateOrganizationUser(userId, updates) {
|
|
223
|
+
return await this.prisma.user.update({
|
|
224
|
+
where: { id: userId },
|
|
225
|
+
data: updates,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Delete user by ID
|
|
231
|
+
* @param {string} userId - User ID to delete
|
|
232
|
+
* @returns {Promise<boolean>} True if deleted successfully
|
|
233
|
+
*/
|
|
234
|
+
async deleteUser(userId) {
|
|
235
|
+
try {
|
|
236
|
+
await this.prisma.user.delete({
|
|
237
|
+
where: { id: userId },
|
|
238
|
+
});
|
|
239
|
+
return true;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error.code === 'P2025') {
|
|
242
|
+
// Record not found
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { UserRepositoryMongo };
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
//todo: this repository is tightly coupled to the token repository.
|
|
2
|
+
const { prisma } = require('../../database/prisma');
|
|
3
|
+
const {
|
|
4
|
+
createTokenRepository,
|
|
5
|
+
} = require('../../token/repositories/token-repository-factory');
|
|
6
|
+
const { UserRepositoryInterface } = require('./user-repository-interface');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PostgreSQL User Repository Adapter
|
|
10
|
+
* Handles user operations with discriminator pattern support
|
|
11
|
+
*
|
|
12
|
+
* PostgreSQL-specific characteristics:
|
|
13
|
+
* - Uses Int IDs with autoincrement
|
|
14
|
+
* - Requires ID conversion: String (app layer) ↔ Int (database)
|
|
15
|
+
* - All returned IDs are converted to strings for application layer consistency
|
|
16
|
+
*/
|
|
17
|
+
class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.prisma = prisma;
|
|
21
|
+
this.tokenRepository = createTokenRepository();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert string ID to integer for PostgreSQL queries
|
|
26
|
+
* @private
|
|
27
|
+
* @param {string|number|null|undefined} id - ID to convert
|
|
28
|
+
* @returns {number|null|undefined} Integer ID or null/undefined
|
|
29
|
+
* @throws {Error} If ID cannot be converted to integer
|
|
30
|
+
*/
|
|
31
|
+
_convertId(id) {
|
|
32
|
+
if (id === null || id === undefined) return id;
|
|
33
|
+
const parsed = parseInt(id, 10);
|
|
34
|
+
if (isNaN(parsed)) {
|
|
35
|
+
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert user object IDs to strings
|
|
42
|
+
* @private
|
|
43
|
+
* @param {Object|null} user - User object from database
|
|
44
|
+
* @returns {Object|null} User with string IDs
|
|
45
|
+
*/
|
|
46
|
+
_convertUserIds(user) {
|
|
47
|
+
if (!user) return user;
|
|
48
|
+
return {
|
|
49
|
+
...user,
|
|
50
|
+
id: user.id?.toString(),
|
|
51
|
+
organizationId: user.organizationId?.toString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get session token from base64 buffer token
|
|
57
|
+
* Delegates to TokenRepository
|
|
58
|
+
*
|
|
59
|
+
* @param {string} token - Base64 buffer token
|
|
60
|
+
* @returns {Promise<Object>} Session token object with string IDs
|
|
61
|
+
*/
|
|
62
|
+
async getSessionToken(token) {
|
|
63
|
+
const jsonToken =
|
|
64
|
+
this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
|
|
65
|
+
const sessionToken = await this.tokenRepository.validateAndGetToken(
|
|
66
|
+
jsonToken
|
|
67
|
+
);
|
|
68
|
+
return sessionToken;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Find organization user by ID
|
|
73
|
+
* Replaces: OrganizationUser.findById(userId)
|
|
74
|
+
*
|
|
75
|
+
* @param {string} userId - User ID (string from application layer)
|
|
76
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
77
|
+
*/
|
|
78
|
+
async findOrganizationUserById(userId) {
|
|
79
|
+
const intId = this._convertId(userId);
|
|
80
|
+
const user = await this.prisma.user.findFirst({
|
|
81
|
+
where: {
|
|
82
|
+
id: intId,
|
|
83
|
+
type: 'ORGANIZATION',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return this._convertUserIds(user);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find individual user by ID
|
|
91
|
+
* Replaces: IndividualUser.findById(userId)
|
|
92
|
+
*
|
|
93
|
+
* @param {string} userId - User ID (string from application layer)
|
|
94
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
95
|
+
*/
|
|
96
|
+
async findIndividualUserById(userId) {
|
|
97
|
+
const intId = this._convertId(userId);
|
|
98
|
+
const user = await this.prisma.user.findFirst({
|
|
99
|
+
where: {
|
|
100
|
+
id: intId,
|
|
101
|
+
type: 'INDIVIDUAL',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return this._convertUserIds(user);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create token with expiration
|
|
109
|
+
* Delegates to TokenRepository
|
|
110
|
+
*
|
|
111
|
+
* @param {string} userId - User ID (string from application layer)
|
|
112
|
+
* @param {string} rawToken - Raw unhashed token
|
|
113
|
+
* @param {number} minutes - Minutes until expiration (default 120)
|
|
114
|
+
* @returns {Promise<string>} Base64 buffer token
|
|
115
|
+
*/
|
|
116
|
+
async createToken(userId, rawToken, minutes = 120) {
|
|
117
|
+
const createdToken = await this.tokenRepository.createTokenWithExpire(
|
|
118
|
+
userId,
|
|
119
|
+
rawToken,
|
|
120
|
+
minutes
|
|
121
|
+
);
|
|
122
|
+
return this.tokenRepository.createBase64BufferToken(
|
|
123
|
+
createdToken,
|
|
124
|
+
rawToken
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create individual user
|
|
130
|
+
* Replaces: IndividualUser.create(params)
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} params - User creation parameters (with string IDs from application layer)
|
|
133
|
+
* @returns {Promise<Object>} Created user object with string IDs
|
|
134
|
+
*/
|
|
135
|
+
async createIndividualUser(params) {
|
|
136
|
+
const user = await this.prisma.user.create({
|
|
137
|
+
data: {
|
|
138
|
+
type: 'INDIVIDUAL',
|
|
139
|
+
email: params.email,
|
|
140
|
+
username: params.username,
|
|
141
|
+
hashword: params.hashword,
|
|
142
|
+
appUserId: params.appUserId,
|
|
143
|
+
organizationId: this._convertId(
|
|
144
|
+
params.organization || params.organizationId
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return this._convertUserIds(user);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create organization user
|
|
153
|
+
* Replaces: OrganizationUser.create(params)
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} params - Organization creation parameters
|
|
156
|
+
* @returns {Promise<Object>} Created organization object with string IDs
|
|
157
|
+
*/
|
|
158
|
+
async createOrganizationUser(params) {
|
|
159
|
+
const user = await this.prisma.user.create({
|
|
160
|
+
data: {
|
|
161
|
+
type: 'ORGANIZATION',
|
|
162
|
+
appOrgId: params.appOrgId,
|
|
163
|
+
name: params.name,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return this._convertUserIds(user);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find individual user by username
|
|
171
|
+
* Replaces: IndividualUser.findOne({ username })
|
|
172
|
+
*
|
|
173
|
+
* @param {string} username - Username to search for
|
|
174
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
175
|
+
*/
|
|
176
|
+
async findIndividualUserByUsername(username) {
|
|
177
|
+
const user = await this.prisma.user.findFirst({
|
|
178
|
+
where: {
|
|
179
|
+
type: 'INDIVIDUAL',
|
|
180
|
+
username,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return this._convertUserIds(user);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find individual user by app user ID
|
|
188
|
+
* Replaces: IndividualUser.getUserByAppUserId(appUserId)
|
|
189
|
+
*
|
|
190
|
+
* @param {string} appUserId - App user ID to search for
|
|
191
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
192
|
+
*/
|
|
193
|
+
async findIndividualUserByAppUserId(appUserId) {
|
|
194
|
+
const user = await this.prisma.user.findFirst({
|
|
195
|
+
where: {
|
|
196
|
+
type: 'INDIVIDUAL',
|
|
197
|
+
appUserId,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
return this._convertUserIds(user);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Find organization user by app org ID
|
|
205
|
+
* Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
|
|
206
|
+
*
|
|
207
|
+
* @param {string} appOrgId - App organization ID to search for
|
|
208
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
209
|
+
*/
|
|
210
|
+
async findOrganizationUserByAppOrgId(appOrgId) {
|
|
211
|
+
const user = await this.prisma.user.findFirst({
|
|
212
|
+
where: {
|
|
213
|
+
type: 'ORGANIZATION',
|
|
214
|
+
appOrgId,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
return this._convertUserIds(user);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Find user by ID (any type)
|
|
222
|
+
* @param {string} userId - User ID (string from application layer)
|
|
223
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
224
|
+
*/
|
|
225
|
+
async findUserById(userId) {
|
|
226
|
+
const intId = this._convertId(userId);
|
|
227
|
+
const user = await this.prisma.user.findUnique({
|
|
228
|
+
where: { id: intId },
|
|
229
|
+
});
|
|
230
|
+
return this._convertUserIds(user);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Find individual user by email
|
|
235
|
+
* @param {string} email - Email to search for
|
|
236
|
+
* @returns {Promise<Object|null>} User object with string IDs or null
|
|
237
|
+
*/
|
|
238
|
+
async findIndividualUserByEmail(email) {
|
|
239
|
+
const user = await this.prisma.user.findFirst({
|
|
240
|
+
where: {
|
|
241
|
+
type: 'INDIVIDUAL',
|
|
242
|
+
email,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
return this._convertUserIds(user);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Update individual user
|
|
250
|
+
* @param {string} userId - User ID (string from application layer)
|
|
251
|
+
* @param {Object} updates - Fields to update (with string IDs from application layer)
|
|
252
|
+
* @returns {Promise<Object>} Updated user object with string IDs
|
|
253
|
+
*/
|
|
254
|
+
async updateIndividualUser(userId, updates) {
|
|
255
|
+
const intId = this._convertId(userId);
|
|
256
|
+
|
|
257
|
+
// Convert organizationId if present in updates
|
|
258
|
+
const data = { ...updates };
|
|
259
|
+
if (data.organizationId !== undefined) {
|
|
260
|
+
data.organizationId = this._convertId(data.organizationId);
|
|
261
|
+
}
|
|
262
|
+
if (data.organization !== undefined) {
|
|
263
|
+
data.organizationId = this._convertId(data.organization);
|
|
264
|
+
delete data.organization;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const user = await this.prisma.user.update({
|
|
268
|
+
where: { id: intId },
|
|
269
|
+
data,
|
|
270
|
+
});
|
|
271
|
+
return this._convertUserIds(user);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update organization user
|
|
276
|
+
* @param {string} userId - User ID (string from application layer)
|
|
277
|
+
* @param {Object} updates - Fields to update
|
|
278
|
+
* @returns {Promise<Object>} Updated user object with string IDs
|
|
279
|
+
*/
|
|
280
|
+
async updateOrganizationUser(userId, updates) {
|
|
281
|
+
const intId = this._convertId(userId);
|
|
282
|
+
const user = await this.prisma.user.update({
|
|
283
|
+
where: { id: intId },
|
|
284
|
+
data: updates,
|
|
285
|
+
});
|
|
286
|
+
return this._convertUserIds(user);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Delete user by ID
|
|
291
|
+
* @param {string} userId - User ID to delete (string from application layer)
|
|
292
|
+
* @returns {Promise<boolean>} True if deleted successfully
|
|
293
|
+
*/
|
|
294
|
+
async deleteUser(userId) {
|
|
295
|
+
try {
|
|
296
|
+
const intId = this._convertId(userId);
|
|
297
|
+
await this.prisma.user.delete({
|
|
298
|
+
where: { id: intId },
|
|
299
|
+
});
|
|
300
|
+
return true;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error.code === 'P2025') {
|
|
303
|
+
// Record not found
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { UserRepositoryPostgres };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const { User } = require('../../user');
|
|
3
|
+
|
|
4
|
+
class TestUserRepository {
|
|
5
|
+
constructor({ userConfig }) {
|
|
6
|
+
this.individualUsers = new Map();
|
|
7
|
+
this.organizationUsers = new Map();
|
|
8
|
+
this.tokens = new Map();
|
|
9
|
+
this.userConfig = userConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getSessionToken(token) {
|
|
13
|
+
return this.tokens.get(token);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async findOrganizationUserById(userId) {
|
|
17
|
+
return this.organizationUsers.get(userId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async findIndividualUserById(userId) {
|
|
21
|
+
return this.individualUsers.get(userId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async createToken(userId, rawToken, minutes = 120) {
|
|
25
|
+
const token = `token-for-${userId}-for-${minutes}-mins`;
|
|
26
|
+
this.tokens.set(token, { user: userId, rawToken });
|
|
27
|
+
return token;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async createIndividualUser(params) {
|
|
31
|
+
const individualUserData = { id: `individual-${Date.now()}`, ...params };
|
|
32
|
+
this.individualUsers.set(individualUserData.id, individualUserData);
|
|
33
|
+
return individualUserData;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async createOrganizationUser(params) {
|
|
37
|
+
const orgUserData = { ...params, id: `org-${Date.now()}` };
|
|
38
|
+
this.organizationUsers.set(orgUserData.id, orgUserData);
|
|
39
|
+
return orgUserData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async findIndividualUserByUsername(username) {
|
|
43
|
+
for (const userDoc of this.individualUsers.values()) {
|
|
44
|
+
if (userDoc.username === username) {
|
|
45
|
+
return userDoc;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async findIndividualUserByAppUserId(appUserId) {
|
|
52
|
+
if (!appUserId) return null;
|
|
53
|
+
for (const userDoc of this.individualUsers.values()) {
|
|
54
|
+
if (userDoc.appUserId === appUserId) {
|
|
55
|
+
return userDoc;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async findOrganizationUserByAppOrgId(appOrgId) {
|
|
62
|
+
if (!appOrgId) return null;
|
|
63
|
+
for (const userDoc of this.organizationUsers.values()) {
|
|
64
|
+
if (userDoc.appOrgId === appOrgId) {
|
|
65
|
+
return userDoc;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { TestUserRepository };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const {
|
|
2
|
+
CreateIndividualUser,
|
|
3
|
+
} = require('../../use-cases/create-individual-user');
|
|
4
|
+
const { TestUserRepository } = require('../doubles/test-user-repository');
|
|
5
|
+
|
|
6
|
+
describe('CreateIndividualUser Use Case', () => {
|
|
7
|
+
it('should create and return an individual user via the repository', async () => {
|
|
8
|
+
const userConfig = { usePassword: true };
|
|
9
|
+
const userRepository = new TestUserRepository({ userConfig });
|
|
10
|
+
const createIndividualUser = new CreateIndividualUser({
|
|
11
|
+
userRepository,
|
|
12
|
+
userConfig,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const params = {
|
|
16
|
+
username: 'test-user',
|
|
17
|
+
password: 'password123',
|
|
18
|
+
};
|
|
19
|
+
const user = await createIndividualUser.execute(params);
|
|
20
|
+
|
|
21
|
+
expect(user).toBeDefined();
|
|
22
|
+
expect(user.getIndividualUser().username).toBe(params.username);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const {
|
|
2
|
+
CreateOrganizationUser,
|
|
3
|
+
} = require('../../use-cases/create-organization-user');
|
|
4
|
+
const { TestUserRepository } = require('../doubles/test-user-repository');
|
|
5
|
+
|
|
6
|
+
describe('CreateOrganizationUser Use Case', () => {
|
|
7
|
+
it('should create and return an organization user via the repository', async () => {
|
|
8
|
+
const userConfig = {
|
|
9
|
+
primary: 'organization',
|
|
10
|
+
organizationUserRequired: true,
|
|
11
|
+
individualUserRequired: false,
|
|
12
|
+
};
|
|
13
|
+
const userRepository = new TestUserRepository({ userConfig });
|
|
14
|
+
const createOrganizationUser = new CreateOrganizationUser({
|
|
15
|
+
userRepository,
|
|
16
|
+
userConfig,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const params = {
|
|
20
|
+
name: 'Test Org',
|
|
21
|
+
appOrgId: 'org-123',
|
|
22
|
+
};
|
|
23
|
+
const user = await createOrganizationUser.execute(params);
|
|
24
|
+
|
|
25
|
+
expect(user).toBeDefined();
|
|
26
|
+
expect(user.getOrganizationUser().name).toBe(params.name);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const {
|
|
2
|
+
CreateTokenForUserId,
|
|
3
|
+
} = require('../../use-cases/create-token-for-user-id');
|
|
4
|
+
const { TestUserRepository } = require('../doubles/test-user-repository');
|
|
5
|
+
|
|
6
|
+
describe('CreateTokenForUserId Use Case', () => {
|
|
7
|
+
it('should create and return a token via the repository', async () => {
|
|
8
|
+
const userConfig = {}; // Not used by this use case, but required by the test repo
|
|
9
|
+
const userRepository = new TestUserRepository({ userConfig });
|
|
10
|
+
const createTokenForUserId = new CreateTokenForUserId({ userRepository });
|
|
11
|
+
|
|
12
|
+
const userId = 'user-123';
|
|
13
|
+
const token = await createTokenForUserId.execute(userId);
|
|
14
|
+
|
|
15
|
+
expect(token).toBeDefined();
|
|
16
|
+
// The mock token is deterministic, so we can check it
|
|
17
|
+
expect(token).toContain(`token-for-${userId}`);
|
|
18
|
+
});
|
|
19
|
+
});
|