@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,213 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createUserRepository,
|
|
3
|
+
} = require('../../user/repositories/user-repository-factory');
|
|
4
|
+
|
|
5
|
+
const ERROR_CODE_MAP = {
|
|
6
|
+
USER_NOT_FOUND: 404,
|
|
7
|
+
USER_ALREADY_EXISTS: 409,
|
|
8
|
+
INVALID_USER_DATA: 400,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function mapErrorToResponse(error) {
|
|
12
|
+
const status = ERROR_CODE_MAP[error?.code] || 500;
|
|
13
|
+
return {
|
|
14
|
+
error: status,
|
|
15
|
+
reason: error?.message,
|
|
16
|
+
code: error?.code,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create user command factory
|
|
22
|
+
*
|
|
23
|
+
* NOTE: This is an internal API. Integration developers should use createFriggCommands() instead.
|
|
24
|
+
*
|
|
25
|
+
* @returns {Object} User command object with CRUD operations
|
|
26
|
+
*/
|
|
27
|
+
function createUserCommands() {
|
|
28
|
+
const userRepository = createUserRepository();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* Create a new individual user
|
|
33
|
+
* @param {Object} params
|
|
34
|
+
* @param {string} params.username - Username (usually email)
|
|
35
|
+
* @param {string} [params.email] - Email address
|
|
36
|
+
* @param {string} [params.appUserId] - External application user ID
|
|
37
|
+
* @param {string} [params.password] - Password (optional)
|
|
38
|
+
* @returns {Promise<Object>} Created user object
|
|
39
|
+
*/
|
|
40
|
+
async createUser({ username, email, appUserId, password } = {}) {
|
|
41
|
+
try {
|
|
42
|
+
if (!username) {
|
|
43
|
+
const error = new Error('username is required');
|
|
44
|
+
error.code = 'INVALID_USER_DATA';
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const userData = { username };
|
|
49
|
+
if (email) userData.email = email;
|
|
50
|
+
if (appUserId) userData.appUserId = appUserId;
|
|
51
|
+
if (password) userData.password = password;
|
|
52
|
+
|
|
53
|
+
const user = await userRepository.createIndividualUser(
|
|
54
|
+
userData
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: user.id,
|
|
59
|
+
username: user.username,
|
|
60
|
+
email: user.email,
|
|
61
|
+
appUserId: user.appUserId,
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code === 11000) {
|
|
65
|
+
// Duplicate key error
|
|
66
|
+
const duplicateError = new Error(
|
|
67
|
+
`User with username '${username}' already exists`
|
|
68
|
+
);
|
|
69
|
+
duplicateError.code = 'USER_ALREADY_EXISTS';
|
|
70
|
+
return mapErrorToResponse(duplicateError);
|
|
71
|
+
}
|
|
72
|
+
return mapErrorToResponse(error);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find a user by their application user ID
|
|
78
|
+
* @param {string} appUserId - External application user ID
|
|
79
|
+
* @returns {Promise<Object|null>} User object or null if not found
|
|
80
|
+
*/
|
|
81
|
+
async findUserByAppUserId(appUserId) {
|
|
82
|
+
try {
|
|
83
|
+
if (!appUserId) {
|
|
84
|
+
const error = new Error('appUserId is required');
|
|
85
|
+
error.code = 'INVALID_USER_DATA';
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const user = await userRepository.findIndividualUserByAppUserId(
|
|
90
|
+
appUserId
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!user) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
id: user.id,
|
|
99
|
+
username: user.username,
|
|
100
|
+
email: user.email,
|
|
101
|
+
appUserId: user.appUserId,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return mapErrorToResponse(error);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find a user by their username
|
|
110
|
+
* @param {string} username - Username to search for
|
|
111
|
+
* @returns {Promise<Object|null>} User object or null if not found
|
|
112
|
+
*/
|
|
113
|
+
async findUserByUsername(username) {
|
|
114
|
+
try {
|
|
115
|
+
if (!username) {
|
|
116
|
+
const error = new Error('username is required');
|
|
117
|
+
error.code = 'INVALID_USER_DATA';
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const user = await userRepository.findIndividualUserByUsername(
|
|
122
|
+
username
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (!user) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: user.id,
|
|
131
|
+
username: user.username,
|
|
132
|
+
email: user.email,
|
|
133
|
+
appUserId: user.appUserId,
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return mapErrorToResponse(error);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Find a user by their ID
|
|
142
|
+
* @param {string} userId - User ID to search for
|
|
143
|
+
* @returns {Promise<Object|null>} User object or null if not found
|
|
144
|
+
*/
|
|
145
|
+
async findUserById(userId) {
|
|
146
|
+
try {
|
|
147
|
+
if (!userId) {
|
|
148
|
+
const error = new Error('userId is required');
|
|
149
|
+
error.code = 'INVALID_USER_DATA';
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const user = await userRepository.findIndividualUserById(
|
|
154
|
+
userId
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!user) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id: user._id.toString(),
|
|
163
|
+
username: user.username,
|
|
164
|
+
email: user.email,
|
|
165
|
+
appUserId: user.appUserId,
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return mapErrorToResponse(error);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Update a user by ID
|
|
174
|
+
* @param {string} userId - User ID to update
|
|
175
|
+
* @param {Object} updates - Fields to update
|
|
176
|
+
* @returns {Promise<Object>} Updated user object
|
|
177
|
+
*/
|
|
178
|
+
async updateUser(userId, updates) {
|
|
179
|
+
try {
|
|
180
|
+
if (!userId) {
|
|
181
|
+
const error = new Error('userId is required');
|
|
182
|
+
error.code = 'INVALID_USER_DATA';
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const user = await userRepository.IndividualUser.update(
|
|
187
|
+
userId,
|
|
188
|
+
updates
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!user) {
|
|
192
|
+
const error = new Error(`User ${userId} not found`);
|
|
193
|
+
error.code = 'USER_NOT_FOUND';
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
id: user._id.toString(),
|
|
199
|
+
username: user.username,
|
|
200
|
+
email: user.email,
|
|
201
|
+
appUserId: user.appUserId,
|
|
202
|
+
};
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return mapErrorToResponse(error);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
createUserCommands,
|
|
212
|
+
ERROR_CODE_MAP,
|
|
213
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createIntegrationCommands,
|
|
3
|
+
findIntegrationContextByExternalEntityId,
|
|
4
|
+
} = require('./commands/integration-commands');
|
|
5
|
+
const { createUserCommands } = require('./commands/user-commands');
|
|
6
|
+
const { createEntityCommands } = require('./commands/entity-commands');
|
|
7
|
+
const {
|
|
8
|
+
createCredentialCommands,
|
|
9
|
+
} = require('./commands/credential-commands');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a unified command factory with all CRUD operations
|
|
13
|
+
*
|
|
14
|
+
* This is the main entry point for integration developers to access all
|
|
15
|
+
* database operations without directly touching Mongoose models.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} params
|
|
18
|
+
* @param {Object} params.integrationClass - Integration class (required)
|
|
19
|
+
* @returns {Object} Unified commands object with all CRUD operations
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
23
|
+
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
24
|
+
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
25
|
+
*/
|
|
26
|
+
function createFriggCommands({ integrationClass } = {}) {
|
|
27
|
+
// All commands use Frigg's default repositories and use cases
|
|
28
|
+
const integrationCommands = createIntegrationCommands({ integrationClass });
|
|
29
|
+
|
|
30
|
+
const userCommands = createUserCommands();
|
|
31
|
+
|
|
32
|
+
const entityCommands = createEntityCommands();
|
|
33
|
+
|
|
34
|
+
const credentialCommands = createCredentialCommands();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
// Integration commands
|
|
38
|
+
...integrationCommands,
|
|
39
|
+
|
|
40
|
+
// User commands
|
|
41
|
+
...userCommands,
|
|
42
|
+
|
|
43
|
+
// Entity commands
|
|
44
|
+
...entityCommands,
|
|
45
|
+
|
|
46
|
+
// Credential commands
|
|
47
|
+
...credentialCommands,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
// Unified factory
|
|
53
|
+
createFriggCommands,
|
|
54
|
+
|
|
55
|
+
// Individual factories
|
|
56
|
+
createIntegrationCommands,
|
|
57
|
+
createUserCommands,
|
|
58
|
+
createEntityCommands,
|
|
59
|
+
createCredentialCommands,
|
|
60
|
+
|
|
61
|
+
// Legacy standalone function
|
|
62
|
+
findIntegrationContextByExternalEntityId,
|
|
63
|
+
|
|
64
|
+
// Deprecated - use createFriggCommands instead
|
|
65
|
+
integrationCommands: {
|
|
66
|
+
create: createIntegrationCommands,
|
|
67
|
+
findIntegrationContextByExternalEntityId,
|
|
68
|
+
},
|
|
69
|
+
};
|