@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.
Files changed (56) hide show
  1. package/CLAUDE.md +2 -1
  2. package/application/commands/integration-commands.js +1 -1
  3. package/application/index.js +1 -1
  4. package/credential/repositories/credential-repository-documentdb.js +300 -0
  5. package/credential/repositories/credential-repository-factory.js +8 -1
  6. package/database/config.js +4 -4
  7. package/database/documentdb-encryption-service.js +330 -0
  8. package/database/documentdb-utils.js +136 -0
  9. package/database/encryption/README.md +50 -0
  10. package/database/encryption/documentdb-encryption-service.md +3270 -0
  11. package/database/encryption/encryption-schema-registry.js +46 -0
  12. package/database/prisma.js +7 -47
  13. package/database/repositories/health-check-repository-documentdb.js +134 -0
  14. package/database/repositories/health-check-repository-factory.js +6 -1
  15. package/database/repositories/health-check-repository-interface.js +29 -34
  16. package/database/repositories/health-check-repository-mongodb.js +1 -3
  17. package/database/use-cases/check-database-state-use-case.js +3 -3
  18. package/database/use-cases/run-database-migration-use-case.js +6 -4
  19. package/database/use-cases/trigger-database-migration-use-case.js +2 -2
  20. package/database/utils/mongodb-schema-init.js +5 -5
  21. package/database/utils/prisma-runner.js +15 -9
  22. package/generated/prisma-mongodb/edge.js +3 -3
  23. package/generated/prisma-mongodb/index.d.ts +10 -4
  24. package/generated/prisma-mongodb/index.js +3 -3
  25. package/generated/prisma-mongodb/package.json +1 -1
  26. package/generated/prisma-mongodb/schema.prisma +1 -3
  27. package/generated/prisma-mongodb/wasm.js +2 -2
  28. package/generated/prisma-postgresql/edge.js +3 -3
  29. package/generated/prisma-postgresql/index.d.ts +10 -4
  30. package/generated/prisma-postgresql/index.js +3 -3
  31. package/generated/prisma-postgresql/package.json +1 -1
  32. package/generated/prisma-postgresql/schema.prisma +1 -3
  33. package/generated/prisma-postgresql/wasm.js +2 -2
  34. package/handlers/routers/db-migration.js +2 -3
  35. package/handlers/routers/health.js +0 -3
  36. package/handlers/workers/db-migration.js +8 -8
  37. package/integrations/repositories/integration-mapping-repository-documentdb.js +135 -0
  38. package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
  39. package/integrations/repositories/integration-repository-documentdb.js +189 -0
  40. package/integrations/repositories/integration-repository-factory.js +8 -1
  41. package/integrations/repositories/process-repository-documentdb.js +141 -0
  42. package/integrations/repositories/process-repository-factory.js +8 -1
  43. package/modules/repositories/module-repository-documentdb.js +307 -0
  44. package/modules/repositories/module-repository-factory.js +8 -1
  45. package/package.json +5 -5
  46. package/prisma-mongodb/schema.prisma +1 -3
  47. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
  48. package/prisma-postgresql/schema.prisma +1 -3
  49. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  50. package/syncs/repositories/sync-repository-factory.js +6 -1
  51. package/token/repositories/token-repository-documentdb.js +125 -0
  52. package/token/repositories/token-repository-factory.js +8 -1
  53. package/user/repositories/user-repository-documentdb.js +292 -0
  54. package/user/repositories/user-repository-factory.js +6 -1
  55. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  56. package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
@@ -0,0 +1,307 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ toObjectId,
4
+ fromObjectId,
5
+ findMany,
6
+ findOne,
7
+ insertOne,
8
+ updateOne,
9
+ deleteOne,
10
+ } = require('../../database/documentdb-utils');
11
+ const { ModuleRepositoryInterface } = require('./module-repository-interface');
12
+ const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
13
+
14
+ /**
15
+ * Module/Entity repository for DocumentDB.
16
+ * Uses DocumentDBEncryptionService for credential decryption.
17
+ *
18
+ * Encrypted fields: Credential.data.*
19
+ *
20
+ * Note: This repository only reads credentials. CredentialRepository
21
+ * handles credential creation/updates with encryption.
22
+ *
23
+ * @see DocumentDBEncryptionService
24
+ * @see CredentialRepositoryDocumentDB
25
+ */
26
+ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
27
+ constructor() {
28
+ super();
29
+ this.prisma = prisma;
30
+ this.encryptionService = new DocumentDBEncryptionService();
31
+ }
32
+
33
+ async findEntityById(entityId) {
34
+ const objectId = toObjectId(entityId);
35
+ if (!objectId) {
36
+ throw new Error(`Entity ${entityId} not found`);
37
+ }
38
+ const doc = await findOne(this.prisma, 'Entity', { _id: objectId });
39
+ if (!doc) {
40
+ throw new Error(`Entity ${entityId} not found`);
41
+ }
42
+ const credential = await this._fetchCredential(doc.credentialId);
43
+ return this._mapEntity(doc, credential);
44
+ }
45
+
46
+ async findEntitiesByUserId(userId) {
47
+ const objectId = toObjectId(userId);
48
+ if (!objectId) {
49
+ throw new Error(`Invalid userId: ${userId}`);
50
+ }
51
+ const filter = { userId: objectId };
52
+ const docs = await findMany(this.prisma, 'Entity', filter);
53
+ const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
54
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
55
+ }
56
+
57
+ async findEntitiesByIds(entitiesIds) {
58
+ const ids = (entitiesIds || []).map((id) => toObjectId(id)).filter(Boolean);
59
+ if (ids.length === 0) return [];
60
+ const docs = await findMany(this.prisma, 'Entity', { _id: { $in: ids } });
61
+ const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
62
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
63
+ }
64
+
65
+ async findEntitiesByUserIdAndModuleName(userId, moduleName) {
66
+ const objectId = toObjectId(userId);
67
+ if (!objectId) {
68
+ throw new Error(`Invalid userId: ${userId}`);
69
+ }
70
+ const filter = {
71
+ userId: objectId,
72
+ moduleName,
73
+ };
74
+ const docs = await findMany(this.prisma, 'Entity', filter);
75
+ const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
76
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
77
+ }
78
+
79
+ async unsetCredential(entityId) {
80
+ const objectId = toObjectId(entityId);
81
+ if (!objectId) return false;
82
+ await updateOne(
83
+ this.prisma,
84
+ 'Entity',
85
+ { _id: objectId },
86
+ {
87
+ $set: {
88
+ credentialId: null,
89
+ },
90
+ }
91
+ );
92
+ return true;
93
+ }
94
+
95
+ async findEntity(filter) {
96
+ const query = this._buildFilter(filter);
97
+ const doc = await findOne(this.prisma, 'Entity', query);
98
+ if (!doc) return null;
99
+ const credential = await this._fetchCredential(doc.credentialId);
100
+ return this._mapEntity(doc, credential);
101
+ }
102
+
103
+ async createEntity(entityData) {
104
+ const document = {
105
+ userId: toObjectId(entityData.user || entityData.userId),
106
+ credentialId: toObjectId(entityData.credential || entityData.credentialId) || null,
107
+ name: entityData.name ?? null,
108
+ moduleName: entityData.moduleName ?? null,
109
+ externalId: entityData.externalId ?? null,
110
+ accountId: entityData.accountId ?? null,
111
+ };
112
+ const insertedId = await insertOne(this.prisma, 'Entity', document);
113
+ const created = await findOne(this.prisma, 'Entity', { _id: insertedId });
114
+ const credential = await this._fetchCredential(created?.credentialId);
115
+ return this._mapEntity(created, credential);
116
+ }
117
+
118
+ async updateEntity(entityId, updates) {
119
+ const objectId = toObjectId(entityId);
120
+ if (!objectId) return null;
121
+ const updatePayload = {};
122
+ if (updates.user !== undefined || updates.userId !== undefined) {
123
+ const userVal = updates.user !== undefined ? updates.user : updates.userId;
124
+ updatePayload.userId = toObjectId(userVal) || null;
125
+ }
126
+ if (updates.credential !== undefined || updates.credentialId !== undefined) {
127
+ const credVal = updates.credential !== undefined ? updates.credential : updates.credentialId;
128
+ updatePayload.credentialId = toObjectId(credVal) || null;
129
+ }
130
+ if (updates.name !== undefined) updatePayload.name = updates.name;
131
+ if (updates.moduleName !== undefined) updatePayload.moduleName = updates.moduleName;
132
+ if (updates.externalId !== undefined) updatePayload.externalId = updates.externalId;
133
+ if (updates.accountId !== undefined) updatePayload.accountId = updates.accountId;
134
+ const result = await updateOne(
135
+ this.prisma,
136
+ 'Entity',
137
+ { _id: objectId },
138
+ { $set: updatePayload }
139
+ );
140
+ const modified = result?.nModified ?? result?.n ?? 0;
141
+ if (modified === 0) return null;
142
+ const updated = await findOne(this.prisma, 'Entity', { _id: objectId });
143
+ const credential = await this._fetchCredential(updated?.credentialId);
144
+ return this._mapEntity(updated, credential);
145
+ }
146
+
147
+ async deleteEntity(entityId) {
148
+ const objectId = toObjectId(entityId);
149
+ if (!objectId) return false;
150
+ const result = await deleteOne(this.prisma, 'Entity', { _id: objectId });
151
+ const deleted = result?.n ?? 0;
152
+ return deleted > 0;
153
+ }
154
+
155
+ async _fetchCredential(credentialId) {
156
+ const id = fromObjectId(credentialId);
157
+ if (!id) return null;
158
+
159
+ try {
160
+ // Convert to ObjectId for raw query
161
+ const objectId = toObjectId(id);
162
+ if (!objectId) return null;
163
+
164
+ // Use raw findOne to bypass Prisma encryption extension
165
+ const rawCredential = await findOne(this.prisma, 'Credential', {
166
+ _id: objectId
167
+ });
168
+
169
+ if (!rawCredential) return null;
170
+
171
+ // Decrypt sensitive fields using service
172
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
173
+
174
+ // Return in same format
175
+ const credential = {
176
+ id: fromObjectId(decryptedCredential._id),
177
+ userId: fromObjectId(decryptedCredential.userId),
178
+ externalId: decryptedCredential.externalId ?? null,
179
+ authIsValid: decryptedCredential.authIsValid ?? null,
180
+ createdAt: decryptedCredential.createdAt,
181
+ updatedAt: decryptedCredential.updatedAt,
182
+ data: decryptedCredential.data
183
+ };
184
+
185
+ return this._convertCredentialIds(credential);
186
+ } catch (error) {
187
+ console.error(`Failed to fetch/decrypt credential ${id}:`, error.message);
188
+ // Return null instead of throwing to allow graceful degradation
189
+ // This repository is read-only (doesn't create/update credentials)
190
+ // Entities can still be loaded even if their credential is corrupted/unreadable
191
+ // The entity will have null credential, which calling code must handle
192
+ // This is intentional behavior: prefer partial data over complete failure
193
+ return null;
194
+ }
195
+ }
196
+
197
+ async _fetchCredentialsBulk(credentialIds) {
198
+ const ids = (credentialIds || [])
199
+ .map((value) => fromObjectId(value))
200
+ .filter((value) => value !== null && value !== undefined);
201
+ if (ids.length === 0) return new Map();
202
+
203
+ try {
204
+ // Convert string IDs to ObjectIds for bulk query
205
+ const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
206
+ if (objectIds.length === 0) return new Map();
207
+
208
+ // Use raw findMany to bypass Prisma encryption extension
209
+ const rawCredentials = await findMany(this.prisma, 'Credential', {
210
+ _id: { $in: objectIds }
211
+ });
212
+
213
+ // Decrypt all credentials in parallel
214
+ const decryptionPromises = rawCredentials.map(async (rawCredential) => {
215
+ try {
216
+ // Decrypt sensitive fields using service
217
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
218
+
219
+ // Build credential object in same format as Prisma would return
220
+ const credential = {
221
+ id: fromObjectId(decryptedCredential._id),
222
+ userId: fromObjectId(decryptedCredential.userId),
223
+ externalId: decryptedCredential.externalId ?? null,
224
+ authIsValid: decryptedCredential.authIsValid ?? null,
225
+ createdAt: decryptedCredential.createdAt,
226
+ updatedAt: decryptedCredential.updatedAt,
227
+ data: decryptedCredential.data
228
+ };
229
+
230
+ return this._convertCredentialIds(credential);
231
+ } catch (error) {
232
+ const credId = fromObjectId(rawCredential._id);
233
+ console.error(`Failed to decrypt credential ${credId}:`, error.message);
234
+ return null;
235
+ }
236
+ });
237
+
238
+ // Wait for all decryptions to complete
239
+ const decryptedCredentials = await Promise.all(decryptionPromises);
240
+
241
+ // Build Map from results, filtering out nulls
242
+ const map = new Map();
243
+ decryptedCredentials.forEach(credential => {
244
+ if (credential) {
245
+ map.set(credential.id, credential);
246
+ }
247
+ });
248
+
249
+ return map;
250
+ } catch (error) {
251
+ console.error('Failed to fetch credentials bulk:', error.message);
252
+ return new Map();
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Convert credential object IDs to strings for application layer
258
+ * Ensures consistent credential format across database adapters
259
+ * @private
260
+ * @param {Object|null} credential - Credential object from database
261
+ * @returns {Object|null} Credential with properly formatted IDs
262
+ */
263
+ _convertCredentialIds(credential) {
264
+ if (!credential) return credential;
265
+ return {
266
+ ...credential,
267
+ id: credential.id ? String(credential.id) : null,
268
+ userId: credential.userId ? String(credential.userId) : null,
269
+ };
270
+ }
271
+
272
+ _buildFilter(filter) {
273
+ const query = {};
274
+ if (!filter) return query;
275
+ if (filter._id || filter.id) {
276
+ const idObj = toObjectId(filter._id || filter.id);
277
+ if (idObj) query._id = idObj;
278
+ }
279
+ if (filter.user || filter.userId) {
280
+ const userObj = toObjectId(filter.user || filter.userId);
281
+ if (userObj) query.userId = userObj;
282
+ }
283
+ if (filter.credential || filter.credentialId) {
284
+ const credObj = toObjectId(filter.credential || filter.credentialId);
285
+ if (credObj) query.credentialId = credObj;
286
+ }
287
+ if (filter.name) query.name = filter.name;
288
+ if (filter.moduleName) query.moduleName = filter.moduleName;
289
+ if (filter.externalId) query.externalId = filter.externalId;
290
+ return query;
291
+ }
292
+
293
+ _mapEntity(doc, credential) {
294
+ return {
295
+ id: fromObjectId(doc?._id),
296
+ accountId: doc?.accountId ?? null,
297
+ credential,
298
+ userId: fromObjectId(doc?.userId),
299
+ name: doc?.name ?? null,
300
+ externalId: doc?.externalId ?? null,
301
+ moduleName: doc?.moduleName ?? null,
302
+ };
303
+ }
304
+ }
305
+
306
+ module.exports = { ModuleRepositoryDocumentDB };
307
+
@@ -1,5 +1,8 @@
1
1
  const { ModuleRepositoryMongo } = require('./module-repository-mongo');
2
2
  const { ModuleRepositoryPostgres } = require('./module-repository-postgres');
3
+ const {
4
+ ModuleRepositoryDocumentDB,
5
+ } = require('./module-repository-documentdb');
3
6
  const config = require('../../database/config');
4
7
 
5
8
  /**
@@ -18,9 +21,12 @@ function createModuleRepository() {
18
21
  case 'postgresql':
19
22
  return new ModuleRepositoryPostgres();
20
23
 
24
+ case 'documentdb':
25
+ return new ModuleRepositoryDocumentDB();
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
  ModuleRepositoryMongo,
32
38
  ModuleRepositoryPostgres,
39
+ ModuleRepositoryDocumentDB,
33
40
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.53",
4
+ "version": "2.0.0-next.54",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.53",
42
- "@friggframework/prettier-config": "2.0.0-next.53",
43
- "@friggframework/test": "2.0.0-next.53",
41
+ "@friggframework/eslint-config": "2.0.0-next.54",
42
+ "@friggframework/prettier-config": "2.0.0-next.54",
43
+ "@friggframework/test": "2.0.0-next.54",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "a2b844487df9cddad8ccb30c7091b7d1ebd909e3"
83
+ "gitHead": "d72f0af6966a5701fe2a4257139d40292972f92a"
84
84
  }
@@ -50,9 +50,7 @@ model User {
50
50
  integrations Integration[]
51
51
  processes Process[]
52
52
 
53
- @@unique([email])
54
- @@unique([username])
55
- @@unique([appOrgId])
53
+ @@unique([username, appUserId])
56
54
  @@index([type])
57
55
  @@index([appUserId])
58
56
  @@map("User")
@@ -0,0 +1,69 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - You are about to drop the column `subType` on the `Credential` table. All the data in the column will be lost.
5
+ - You are about to drop the column `subType` on the `Entity` table. All the data in the column will be lost.
6
+ - A unique constraint covering the columns `[username,appUserId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
7
+
8
+ */
9
+ -- DropIndex
10
+ DROP INDEX "User_appOrgId_key";
11
+
12
+ -- DropIndex
13
+ DROP INDEX "User_email_key";
14
+
15
+ -- DropIndex
16
+ DROP INDEX "User_username_key";
17
+
18
+ -- AlterTable
19
+ ALTER TABLE "Credential" DROP COLUMN "subType";
20
+
21
+ -- AlterTable
22
+ ALTER TABLE "Entity" DROP COLUMN "subType";
23
+
24
+ -- CreateTable
25
+ CREATE TABLE "Process" (
26
+ "id" SERIAL NOT NULL,
27
+ "userId" INTEGER NOT NULL,
28
+ "integrationId" INTEGER NOT NULL,
29
+ "name" TEXT NOT NULL,
30
+ "type" TEXT NOT NULL,
31
+ "state" TEXT NOT NULL,
32
+ "context" JSONB NOT NULL DEFAULT '{}',
33
+ "results" JSONB NOT NULL DEFAULT '{}',
34
+ "parentProcessId" INTEGER,
35
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+ "updatedAt" TIMESTAMP(3) NOT NULL,
37
+
38
+ CONSTRAINT "Process_pkey" PRIMARY KEY ("id")
39
+ );
40
+
41
+ -- CreateIndex
42
+ CREATE INDEX "Process_userId_idx" ON "Process"("userId");
43
+
44
+ -- CreateIndex
45
+ CREATE INDEX "Process_integrationId_idx" ON "Process"("integrationId");
46
+
47
+ -- CreateIndex
48
+ CREATE INDEX "Process_type_idx" ON "Process"("type");
49
+
50
+ -- CreateIndex
51
+ CREATE INDEX "Process_state_idx" ON "Process"("state");
52
+
53
+ -- CreateIndex
54
+ CREATE INDEX "Process_name_idx" ON "Process"("name");
55
+
56
+ -- CreateIndex
57
+ CREATE INDEX "Process_parentProcessId_idx" ON "Process"("parentProcessId");
58
+
59
+ -- CreateIndex
60
+ CREATE UNIQUE INDEX "User_username_appUserId_key" ON "User"("username", "appUserId");
61
+
62
+ -- AddForeignKey
63
+ ALTER TABLE "Process" ADD CONSTRAINT "Process_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64
+
65
+ -- AddForeignKey
66
+ ALTER TABLE "Process" ADD CONSTRAINT "Process_integrationId_fkey" FOREIGN KEY ("integrationId") REFERENCES "Integration"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67
+
68
+ -- AddForeignKey
69
+ ALTER TABLE "Process" ADD CONSTRAINT "Process_parentProcessId_fkey" FOREIGN KEY ("parentProcessId") REFERENCES "Process"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -50,9 +50,7 @@ model User {
50
50
  integrations Integration[]
51
51
  processes Process[]
52
52
 
53
- @@unique([email])
54
- @@unique([username])
55
- @@unique([appOrgId])
53
+ @@unique([username, appUserId])
56
54
  @@index([type])
57
55
  @@index([appUserId])
58
56
  }
@@ -0,0 +1,240 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ toObjectId,
4
+ fromObjectId,
5
+ findMany,
6
+ findOne,
7
+ insertOne,
8
+ updateOne,
9
+ deleteOne,
10
+ } = require('../../database/documentdb-utils');
11
+ const { SyncRepositoryInterface } = require('./sync-repository-interface');
12
+
13
+ class SyncRepositoryDocumentDB extends SyncRepositoryInterface {
14
+ constructor() {
15
+ super();
16
+ this.prisma = prisma;
17
+ }
18
+
19
+ async getSyncObject(name, dataIdentifier, entity) {
20
+ const pipeline = [
21
+ {
22
+ $match: {
23
+ name,
24
+ dataIdentifiers: {
25
+ $elemMatch: {
26
+ idData: dataIdentifier,
27
+ entityId: toObjectId(entity),
28
+ },
29
+ },
30
+ },
31
+ },
32
+ {
33
+ $limit: 2,
34
+ },
35
+ ];
36
+
37
+ const result = await this.prisma.$runCommandRaw({
38
+ aggregate: 'Sync',
39
+ pipeline,
40
+ cursor: {},
41
+ });
42
+
43
+ const syncList = result?.cursor?.firstBatch || [];
44
+
45
+ if (syncList.length === 1) {
46
+ const doc = syncList[0];
47
+ return this._mapSync(doc);
48
+ } else if (syncList.length === 0) {
49
+ return null;
50
+ }
51
+ throw new Error(
52
+ `There are multiple sync objects with the name ${name}, for entities [${syncList[0]?.entities}] [${syncList[1]?.entities}]`
53
+ );
54
+ }
55
+
56
+ async upsertSync(filter, syncData) {
57
+ const query = this._convertFilter(filter);
58
+ const existing = await findOne(this.prisma, 'Sync', query);
59
+
60
+ const now = new Date();
61
+ const documentData = this._prepareSyncData(syncData, now);
62
+
63
+ if (existing) {
64
+ await updateOne(
65
+ this.prisma,
66
+ 'Sync',
67
+ { _id: existing._id },
68
+ {
69
+ $set: documentData,
70
+ }
71
+ );
72
+ const updated = await findOne(this.prisma, 'Sync', { _id: existing._id });
73
+ return this._mapSync(updated);
74
+ }
75
+
76
+ const insertedId = await insertOne(this.prisma, 'Sync', {
77
+ ...documentData,
78
+ createdAt: now,
79
+ });
80
+ const created = await findOne(this.prisma, 'Sync', { _id: insertedId });
81
+ return this._mapSync(created);
82
+ }
83
+
84
+ async updateSync(id, updates) {
85
+ const objectId = toObjectId(id);
86
+ if (!objectId) return null;
87
+ const documentData = this._prepareSyncData(updates, new Date());
88
+ await updateOne(
89
+ this.prisma,
90
+ 'Sync',
91
+ { _id: objectId },
92
+ {
93
+ $set: documentData,
94
+ }
95
+ );
96
+ const updated = await findOne(this.prisma, 'Sync', { _id: objectId });
97
+ return updated ? this._mapSync(updated) : null;
98
+ }
99
+
100
+ async addDataIdentifier(syncId, dataIdentifier) {
101
+ const syncObjectId = toObjectId(syncId);
102
+ if (!syncObjectId) return null;
103
+ const doc = await findOne(this.prisma, 'Sync', { _id: syncObjectId });
104
+ if (!doc) return null;
105
+
106
+ const identifiers = Array.isArray(doc.dataIdentifiers) ? [...doc.dataIdentifiers] : [];
107
+ identifiers.push({
108
+ syncId: syncObjectId,
109
+ entityId: toObjectId(dataIdentifier.entity),
110
+ idData: dataIdentifier.id,
111
+ hash: dataIdentifier.hash,
112
+ createdAt: new Date(),
113
+ });
114
+
115
+ await updateOne(
116
+ this.prisma,
117
+ 'Sync',
118
+ { _id: syncObjectId },
119
+ {
120
+ $set: {
121
+ dataIdentifiers: identifiers,
122
+ updatedAt: new Date(),
123
+ },
124
+ }
125
+ );
126
+
127
+ const updated = await findOne(this.prisma, 'Sync', { _id: syncObjectId });
128
+ return updated ? this._mapSync(updated) : null;
129
+ }
130
+
131
+ getEntityObjIdForEntityIdFromObject(syncObj, entityId) {
132
+ if (!syncObj || !Array.isArray(syncObj.dataIdentifiers)) {
133
+ throw new Error('Sync object must include dataIdentifiers');
134
+ }
135
+
136
+ const entry = syncObj.dataIdentifiers.find(
137
+ (identifier) => fromObjectId(identifier.entityId) === String(entityId)
138
+ );
139
+
140
+ if (entry) {
141
+ return entry.idData;
142
+ }
143
+
144
+ throw new Error(
145
+ `Sync object ${syncObj.id} does not contain a data identifier for entity ${entityId}`
146
+ );
147
+ }
148
+
149
+ async findSyncs(filter) {
150
+ const query = this._convertFilter(filter);
151
+ const docs = await findMany(this.prisma, 'Sync', query);
152
+ return docs.map((doc) => this._mapSync(doc));
153
+ }
154
+
155
+ async findOneSync(filter) {
156
+ const query = this._convertFilter(filter);
157
+ const doc = await findOne(this.prisma, 'Sync', query);
158
+ return doc ? this._mapSync(doc) : null;
159
+ }
160
+
161
+ async deleteSync(id) {
162
+ const objectId = toObjectId(id);
163
+ if (!objectId) return { acknowledged: true, deletedCount: 0 };
164
+ const result = await deleteOne(this.prisma, 'Sync', { _id: objectId });
165
+ const deleted = result?.n ?? 0;
166
+ return { acknowledged: true, deletedCount: deleted };
167
+ }
168
+
169
+ _convertFilter(filter = {}) {
170
+ const query = { ...filter };
171
+ if (filter._id || filter.id) {
172
+ const idObj = toObjectId(filter._id || filter.id);
173
+ if (idObj) query._id = idObj;
174
+ delete query._id;
175
+ delete query.id;
176
+ }
177
+ if (filter.integrationId) {
178
+ query.integrationId = toObjectId(filter.integrationId);
179
+ }
180
+ if (filter.entities) {
181
+ query.entityIds = (filter.entities || []).map((id) => toObjectId(id)).filter(Boolean);
182
+ delete query.entities;
183
+ }
184
+ return query;
185
+ }
186
+
187
+ _prepareSyncData(data = {}, timestamp) {
188
+ const prepared = {};
189
+ if (data.integrationId !== undefined) {
190
+ prepared.integrationId = toObjectId(data.integrationId);
191
+ }
192
+ if (data.entities !== undefined || data.entityIds !== undefined) {
193
+ const list = data.entities !== undefined ? data.entities : data.entityIds;
194
+ prepared.entityIds = (list || []).map((id) => toObjectId(id)).filter(Boolean);
195
+ }
196
+ if (data.hash !== undefined) prepared.hash = data.hash;
197
+ if (data.name !== undefined) prepared.name = data.name;
198
+ if (data.context !== undefined) prepared.context = data.context;
199
+ if (data.results !== undefined) prepared.results = data.results;
200
+ if (timestamp) prepared.updatedAt = timestamp;
201
+ if (data.dataIdentifiers !== undefined) {
202
+ prepared.dataIdentifiers = (data.dataIdentifiers || []).map((identifier) => ({
203
+ syncId: toObjectId(identifier.syncId),
204
+ entityId: toObjectId(identifier.entityId),
205
+ idData: identifier.idData,
206
+ hash: identifier.hash,
207
+ createdAt: identifier.createdAt ? new Date(identifier.createdAt) : new Date(),
208
+ }));
209
+ }
210
+ return prepared;
211
+ }
212
+
213
+ _mapSync(doc) {
214
+ if (!doc) return null;
215
+ return {
216
+ id: fromObjectId(doc._id),
217
+ integrationId: doc.integrationId ? fromObjectId(doc.integrationId) : null,
218
+ entities: Array.isArray(doc.entityIds)
219
+ ? doc.entityIds.map((id) => fromObjectId(id))
220
+ : [],
221
+ entityIds: Array.isArray(doc.entityIds)
222
+ ? doc.entityIds.map((id) => fromObjectId(id))
223
+ : [],
224
+ hash: doc.hash ?? null,
225
+ name: doc.name ?? null,
226
+ dataIdentifiers: Array.isArray(doc.dataIdentifiers)
227
+ ? doc.dataIdentifiers.map((identifier) => ({
228
+ syncId: identifier.syncId ? fromObjectId(identifier.syncId) : null,
229
+ entityId: identifier.entityId ? fromObjectId(identifier.entityId) : null,
230
+ idData: identifier.idData,
231
+ hash: identifier.hash,
232
+ }))
233
+ : [],
234
+ };
235
+ }
236
+ }
237
+
238
+ module.exports = { SyncRepositoryDocumentDB };
239
+
240
+