@friggframework/core 2.0.0-next.40 → 2.0.0-next.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +25 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +121 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +321 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Prisma Client Extension for transparent field-level encryption.
3
+ * Intercepts Prisma queries to encrypt on write and decrypt on read.
4
+ */
5
+
6
+ const { getEncryptedFields } = require('./encryption-schema-registry');
7
+ const { FieldEncryptionService } = require('./field-encryption-service');
8
+
9
+ function createEncryptionExtension({ cryptor, enabled = true }) {
10
+ if (!enabled) {
11
+ return (client) => client;
12
+ }
13
+
14
+ if (!cryptor) {
15
+ throw new Error(
16
+ 'Cryptor instance required for encryption extension'
17
+ );
18
+ }
19
+
20
+ const encryptionService = new FieldEncryptionService({
21
+ cryptor,
22
+ schema: { getEncryptedFields },
23
+ });
24
+
25
+ return {
26
+ name: 'frigg-field-encryption',
27
+ query: {
28
+ $allModels: {
29
+ async create({ model, args, query }) {
30
+ if (args.data) {
31
+ args.data = await encryptionService.encryptFields(
32
+ model,
33
+ args.data
34
+ );
35
+ }
36
+
37
+ const result = await query(args);
38
+
39
+ if (result) {
40
+ return await encryptionService.decryptFields(
41
+ model,
42
+ result
43
+ );
44
+ }
45
+
46
+ return result;
47
+ },
48
+
49
+ async createMany({ model, args, query }) {
50
+ if (args.data && Array.isArray(args.data)) {
51
+ args.data =
52
+ await encryptionService.encryptFieldsInBulk(
53
+ model,
54
+ args.data
55
+ );
56
+ } else if (args.data) {
57
+ args.data = await encryptionService.encryptFields(
58
+ model,
59
+ args.data
60
+ );
61
+ }
62
+
63
+ return await query(args);
64
+ },
65
+
66
+ async update({ model, args, query }) {
67
+ if (args.data) {
68
+ args.data = await encryptionService.encryptFields(
69
+ model,
70
+ args.data
71
+ );
72
+ }
73
+
74
+ const result = await query(args);
75
+
76
+ if (result) {
77
+ return await encryptionService.decryptFields(
78
+ model,
79
+ result
80
+ );
81
+ }
82
+
83
+ return result;
84
+ },
85
+
86
+ async updateMany({ model, args, query }) {
87
+ if (args.data) {
88
+ args.data = await encryptionService.encryptFields(
89
+ model,
90
+ args.data
91
+ );
92
+ }
93
+
94
+ return await query(args);
95
+ },
96
+
97
+ async upsert({ model, args, query }) {
98
+ if (args.create) {
99
+ args.create = await encryptionService.encryptFields(
100
+ model,
101
+ args.create
102
+ );
103
+ }
104
+
105
+ if (args.update) {
106
+ args.update = await encryptionService.encryptFields(
107
+ model,
108
+ args.update
109
+ );
110
+ }
111
+
112
+ const result = await query(args);
113
+
114
+ if (result) {
115
+ return await encryptionService.decryptFields(
116
+ model,
117
+ result
118
+ );
119
+ }
120
+
121
+ return result;
122
+ },
123
+
124
+ async findUnique({ model, args, query }) {
125
+ const result = await query(args);
126
+
127
+ if (result) {
128
+ return await encryptionService.decryptFields(
129
+ model,
130
+ result
131
+ );
132
+ }
133
+
134
+ return result;
135
+ },
136
+
137
+ async findFirst({ model, args, query }) {
138
+ const result = await query(args);
139
+
140
+ if (result) {
141
+ return await encryptionService.decryptFields(
142
+ model,
143
+ result
144
+ );
145
+ }
146
+
147
+ return result;
148
+ },
149
+
150
+ async findMany({ model, args, query }) {
151
+ const results = await query(args);
152
+
153
+ if (results && Array.isArray(results)) {
154
+ return await encryptionService.decryptFieldsInBulk(
155
+ model,
156
+ results
157
+ );
158
+ }
159
+
160
+ return results;
161
+ },
162
+
163
+ async delete({ model, args, query }) {
164
+ const result = await query(args);
165
+
166
+ if (result) {
167
+ return await encryptionService.decryptFields(
168
+ model,
169
+ result
170
+ );
171
+ }
172
+
173
+ return result;
174
+ },
175
+
176
+ async deleteMany({ model, args, query }) {
177
+ return await query(args);
178
+ },
179
+
180
+ async count({ model, args, query }) {
181
+ return await query(args);
182
+ },
183
+
184
+ async aggregate({ model, args, query }) {
185
+ return await query(args);
186
+ },
187
+
188
+ async groupBy({ model, args, query }) {
189
+ return await query(args);
190
+ },
191
+
192
+ async findFirstOrThrow({ model, args, query }) {
193
+ const result = await query(args);
194
+
195
+ if (result) {
196
+ return await encryptionService.decryptFields(
197
+ model,
198
+ result
199
+ );
200
+ }
201
+
202
+ return result;
203
+ },
204
+
205
+ async findUniqueOrThrow({ model, args, query }) {
206
+ const result = await query(args);
207
+
208
+ if (result) {
209
+ return await encryptionService.decryptFields(
210
+ model,
211
+ result
212
+ );
213
+ }
214
+
215
+ return result;
216
+ },
217
+ },
218
+ },
219
+ };
220
+ }
221
+
222
+ module.exports = { createEncryptionExtension };
@@ -0,0 +1,439 @@
1
+ const { createEncryptionExtension } = require('./prisma-encryption-extension');
2
+
3
+ describe('Prisma Encryption Extension', () => {
4
+ let mockCryptor;
5
+ let mockQuery;
6
+
7
+ beforeEach(() => {
8
+ // Mock Cryptor
9
+ mockCryptor = {
10
+ encrypt: jest
11
+ .fn()
12
+ .mockImplementation(
13
+ (value) => `encrypted:${value}:iv:enckey`
14
+ ),
15
+ decrypt: jest
16
+ .fn()
17
+ .mockImplementation((value) => {
18
+ const parts = value.split(':');
19
+ return parts[1]; // Extract original value
20
+ }),
21
+ };
22
+
23
+ // Mock Prisma query function
24
+ mockQuery = jest.fn().mockImplementation((args) => args.mockResult);
25
+ });
26
+
27
+ describe('createEncryptionExtension', () => {
28
+ it('should create extension with valid config', () => {
29
+ const extension = createEncryptionExtension({
30
+ cryptor: mockCryptor,
31
+ enabled: true,
32
+ });
33
+
34
+ expect(extension).toBeDefined();
35
+ expect(extension.name).toBe('frigg-field-encryption');
36
+ expect(extension.query).toBeDefined();
37
+ expect(extension.query.$allModels).toBeDefined();
38
+ });
39
+
40
+ it('should return no-op extension when disabled', () => {
41
+ const extension = createEncryptionExtension({
42
+ cryptor: mockCryptor,
43
+ enabled: false,
44
+ });
45
+
46
+ // No-op extension is a function that returns its input
47
+ const mockClient = { $extends: jest.fn() };
48
+ const result = extension(mockClient);
49
+
50
+ expect(result).toBe(mockClient);
51
+ });
52
+
53
+ it('should throw if cryptor not provided when enabled', () => {
54
+ expect(() => {
55
+ createEncryptionExtension({ enabled: true });
56
+ }).toThrow('Cryptor instance required');
57
+ });
58
+
59
+ it('should not throw if cryptor not provided when disabled', () => {
60
+ expect(() => {
61
+ createEncryptionExtension({ enabled: false });
62
+ }).not.toThrow();
63
+ });
64
+ });
65
+
66
+ describe('Query Interceptors', () => {
67
+ let extension;
68
+ let handlers;
69
+
70
+ beforeEach(() => {
71
+ extension = createEncryptionExtension({
72
+ cryptor: mockCryptor,
73
+ enabled: true,
74
+ });
75
+ handlers = extension.query.$allModels;
76
+ });
77
+
78
+ describe('create', () => {
79
+ it('should encrypt data before create', async () => {
80
+ const args = {
81
+ data: {
82
+ data: { access_token: 'secret123' },
83
+ },
84
+ mockResult: {
85
+ id: '1',
86
+ data: { access_token: 'encrypted:secret123:iv:enckey' },
87
+ },
88
+ };
89
+
90
+ await handlers.create({
91
+ model: 'Credential',
92
+ operation: 'create',
93
+ args,
94
+ query: mockQuery,
95
+ });
96
+
97
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret123');
98
+ expect(args.data.data.access_token).toBe(
99
+ 'encrypted:secret123:iv:enckey'
100
+ );
101
+ });
102
+
103
+ it('should decrypt result after create', async () => {
104
+ const args = {
105
+ data: { data: { access_token: 'secret123' } },
106
+ mockResult: {
107
+ id: '1',
108
+ data: { access_token: 'encrypted:secret123:iv:enckey' },
109
+ },
110
+ };
111
+
112
+ const result = await handlers.create({
113
+ model: 'Credential',
114
+ operation: 'create',
115
+ args,
116
+ query: mockQuery,
117
+ });
118
+
119
+ expect(mockCryptor.decrypt).toHaveBeenCalledWith(
120
+ 'encrypted:secret123:iv:enckey'
121
+ );
122
+ expect(result.data.access_token).toBe('secret123');
123
+ });
124
+
125
+ it('should handle null result', async () => {
126
+ const args = {
127
+ data: { data: { access_token: 'secret123' } },
128
+ mockResult: null,
129
+ };
130
+
131
+ const result = await handlers.create({
132
+ model: 'Credential',
133
+ operation: 'create',
134
+ args,
135
+ query: mockQuery,
136
+ });
137
+
138
+ expect(result).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe('createMany', () => {
143
+ it('should encrypt array of data', async () => {
144
+ const args = {
145
+ data: [
146
+ { data: { access_token: 'secret1' } },
147
+ { data: { access_token: 'secret2' } },
148
+ ],
149
+ mockResult: { count: 2 },
150
+ };
151
+
152
+ await handlers.createMany({
153
+ model: 'Credential',
154
+ operation: 'createMany',
155
+ args,
156
+ query: mockQuery,
157
+ });
158
+
159
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret1');
160
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret2');
161
+ expect(args.data[0].data.access_token).toBe(
162
+ 'encrypted:secret1:iv:enckey'
163
+ );
164
+ expect(args.data[1].data.access_token).toBe(
165
+ 'encrypted:secret2:iv:enckey'
166
+ );
167
+ });
168
+
169
+ it('should handle single object in createMany', async () => {
170
+ const args = {
171
+ data: { data: { access_token: 'secret' } },
172
+ mockResult: { count: 1 },
173
+ };
174
+
175
+ await handlers.createMany({
176
+ model: 'Credential',
177
+ operation: 'createMany',
178
+ args,
179
+ query: mockQuery,
180
+ });
181
+
182
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret');
183
+ });
184
+ });
185
+
186
+ describe('update', () => {
187
+ it('should encrypt update data', async () => {
188
+ const args = {
189
+ data: { data: { access_token: 'newsecret' } },
190
+ mockResult: {
191
+ id: '1',
192
+ data: { access_token: 'encrypted:newsecret:iv:enckey' },
193
+ },
194
+ };
195
+
196
+ await handlers.update({
197
+ model: 'Credential',
198
+ operation: 'update',
199
+ args,
200
+ query: mockQuery,
201
+ });
202
+
203
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('newsecret');
204
+ });
205
+
206
+ it('should decrypt result after update', async () => {
207
+ const args = {
208
+ data: { data: { access_token: 'newsecret' } },
209
+ mockResult: {
210
+ id: '1',
211
+ data: { access_token: 'encrypted:newsecret:iv:enckey' },
212
+ },
213
+ };
214
+
215
+ const result = await handlers.update({
216
+ model: 'Credential',
217
+ operation: 'update',
218
+ args,
219
+ query: mockQuery,
220
+ });
221
+
222
+ expect(result.data.access_token).toBe('newsecret');
223
+ });
224
+ });
225
+
226
+ describe('upsert', () => {
227
+ it('should encrypt both create and update data', async () => {
228
+ const args = {
229
+ create: { data: { access_token: 'createsecret' } },
230
+ update: { data: { access_token: 'updatesecret' } },
231
+ mockResult: {
232
+ id: '1',
233
+ data: { access_token: 'encrypted:createsecret:iv:enckey' },
234
+ },
235
+ };
236
+
237
+ await handlers.upsert({
238
+ model: 'Credential',
239
+ operation: 'upsert',
240
+ args,
241
+ query: mockQuery,
242
+ });
243
+
244
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('createsecret');
245
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('updatesecret');
246
+ });
247
+ });
248
+
249
+ describe('findUnique', () => {
250
+ it('should decrypt result', async () => {
251
+ const args = {
252
+ where: { id: '1' },
253
+ mockResult: {
254
+ id: '1',
255
+ data: { access_token: 'encrypted:secret:iv:enckey' },
256
+ },
257
+ };
258
+
259
+ const result = await handlers.findUnique({
260
+ model: 'Credential',
261
+ operation: 'findUnique',
262
+ args,
263
+ query: mockQuery,
264
+ });
265
+
266
+ expect(mockCryptor.decrypt).toHaveBeenCalledWith(
267
+ 'encrypted:secret:iv:enckey'
268
+ );
269
+ expect(result.data.access_token).toBe('secret');
270
+ });
271
+
272
+ it('should handle null result', async () => {
273
+ const args = {
274
+ where: { id: '999' },
275
+ mockResult: null,
276
+ };
277
+
278
+ const result = await handlers.findUnique({
279
+ model: 'Credential',
280
+ operation: 'findUnique',
281
+ args,
282
+ query: mockQuery,
283
+ });
284
+
285
+ expect(result).toBeNull();
286
+ expect(mockCryptor.decrypt).not.toHaveBeenCalled();
287
+ });
288
+ });
289
+
290
+ describe('findMany', () => {
291
+ it('should decrypt array of results', async () => {
292
+ const args = {
293
+ mockResult: [
294
+ {
295
+ id: '1',
296
+ data: { access_token: 'encrypted:secret1:iv:enckey' },
297
+ },
298
+ {
299
+ id: '2',
300
+ data: { access_token: 'encrypted:secret2:iv:enckey' },
301
+ },
302
+ ],
303
+ };
304
+
305
+ const results = await handlers.findMany({
306
+ model: 'Credential',
307
+ operation: 'findMany',
308
+ args,
309
+ query: mockQuery,
310
+ });
311
+
312
+ expect(results).toHaveLength(2);
313
+ expect(results[0].data.access_token).toBe('secret1');
314
+ expect(results[1].data.access_token).toBe('secret2');
315
+ });
316
+
317
+ it('should handle empty array', async () => {
318
+ const args = {
319
+ mockResult: [],
320
+ };
321
+
322
+ const results = await handlers.findMany({
323
+ model: 'Credential',
324
+ operation: 'findMany',
325
+ args,
326
+ query: mockQuery,
327
+ });
328
+
329
+ expect(results).toEqual([]);
330
+ });
331
+ });
332
+
333
+ describe('delete', () => {
334
+ it('should decrypt deleted record', async () => {
335
+ const args = {
336
+ where: { id: '1' },
337
+ mockResult: {
338
+ id: '1',
339
+ data: { access_token: 'encrypted:secret:iv:enckey' },
340
+ },
341
+ };
342
+
343
+ const result = await handlers.delete({
344
+ model: 'Credential',
345
+ operation: 'delete',
346
+ args,
347
+ query: mockQuery,
348
+ });
349
+
350
+ expect(result.data.access_token).toBe('secret');
351
+ });
352
+ });
353
+
354
+ describe('count', () => {
355
+ it('should pass through without encryption', async () => {
356
+ const args = {
357
+ mockResult: { count: 5 },
358
+ };
359
+
360
+ const result = await handlers.count({
361
+ model: 'Credential',
362
+ operation: 'count',
363
+ args,
364
+ query: mockQuery,
365
+ });
366
+
367
+ expect(result).toEqual({ count: 5 });
368
+ expect(mockCryptor.encrypt).not.toHaveBeenCalled();
369
+ expect(mockCryptor.decrypt).not.toHaveBeenCalled();
370
+ });
371
+ });
372
+
373
+ describe('Model without encrypted fields', () => {
374
+ it('should pass through State model without encryption', async () => {
375
+ const args = {
376
+ data: { state: { some: 'data' } },
377
+ mockResult: { id: '1', state: { some: 'data' } },
378
+ };
379
+
380
+ const result = await handlers.create({
381
+ model: 'State',
382
+ operation: 'create',
383
+ args,
384
+ query: mockQuery,
385
+ });
386
+
387
+ expect(result.state).toEqual({ some: 'data' });
388
+ expect(mockCryptor.encrypt).not.toHaveBeenCalled();
389
+ expect(mockCryptor.decrypt).not.toHaveBeenCalled();
390
+ });
391
+ });
392
+ });
393
+
394
+ describe('Integration with FieldEncryptionService', () => {
395
+ it('should handle nested JSON paths correctly', async () => {
396
+ const extension = createEncryptionExtension({
397
+ cryptor: mockCryptor,
398
+ enabled: true,
399
+ });
400
+ const handlers = extension.query.$allModels;
401
+
402
+ const args = {
403
+ data: {
404
+ id: '123',
405
+ data: {
406
+ access_token: 'secret',
407
+ refresh_token: 'refresh',
408
+ other: 'public',
409
+ },
410
+ },
411
+ mockResult: {
412
+ id: '123',
413
+ data: {
414
+ access_token: 'encrypted:secret:iv:enckey',
415
+ refresh_token: 'encrypted:refresh:iv:enckey',
416
+ other: 'public',
417
+ },
418
+ },
419
+ };
420
+
421
+ const result = await handlers.create({
422
+ model: 'Credential',
423
+ operation: 'create',
424
+ args,
425
+ query: mockQuery,
426
+ });
427
+
428
+ // Verify encryption was called for encrypted fields
429
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret');
430
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith('refresh');
431
+ // 'other' should not be encrypted
432
+
433
+ // Verify decryption in result
434
+ expect(result.data.access_token).toBe('secret');
435
+ expect(result.data.refresh_token).toBe('refresh');
436
+ expect(result.data.other).toBe('public');
437
+ });
438
+ });
439
+ });
package/database/index.js CHANGED
@@ -1,25 +1,38 @@
1
+ //todo: probably most of this file content can be removed
2
+
3
+ /**
4
+ * Database Module Index
5
+ * Exports Mongoose models and connection utilities
6
+ *
7
+ * Note: Frigg uses the Repository pattern for data access.
8
+ * Models are not meant to be used directly - use repositories instead:
9
+ * - SyncRepository (syncs/sync-repository.js)
10
+ * - IntegrationRepository (integrations/integration-repository.js)
11
+ * - CredentialRepository (credential/credential-repository.js)
12
+ * etc.
13
+ */
14
+
1
15
  const { mongoose } = require('./mongoose');
2
- const {
3
- connectToDatabase,
4
- disconnectFromDatabase,
5
- createObjectId,
6
- } = require('./mongo');
7
16
  const { IndividualUser } = require('./models/IndividualUser');
8
17
  const { OrganizationUser } = require('./models/OrganizationUser');
9
- const { State } = require('./models/State');
10
- const { Token } = require('./models/Token');
11
18
  const { UserModel } = require('./models/UserModel');
12
19
  const { WebsocketConnection } = require('./models/WebsocketConnection');
13
20
 
21
+ // Prisma exports
22
+ const { prisma } = require('./prisma');
23
+ const { TokenRepository } = require('../token/repositories/token-repository');
24
+ const {
25
+ WebsocketConnectionRepository,
26
+ } = require('../websocket/repositories/websocket-connection-repository');
27
+
14
28
  module.exports = {
15
29
  mongoose,
16
- connectToDatabase,
17
- disconnectFromDatabase,
18
- createObjectId,
19
30
  IndividualUser,
20
31
  OrganizationUser,
21
- State,
22
- Token,
23
32
  UserModel,
24
33
  WebsocketConnection,
34
+ // Prisma
35
+ prisma,
36
+ TokenRepository,
37
+ WebsocketConnectionRepository,
25
38
  };