1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const EOL = require('os').EOL;
|
17 |
|
18 | const Joi = require('joi');
|
19 |
|
20 | const { AllowedBasicCollPermissions, AllowedCollectionHooks, AllowedRolesPermissions, AllowedServiceTypesToApply,
|
21 | AllowedCLIRuntimes, ConfigType, ConfigFiles, PermissionsOperation } = require('./Constants');
|
22 | const KinveyError = require('./KinveyError');
|
23 |
|
24 | const typeOfCollAndCollHookSchema = Joi.string().valid([ConfigFiles.AllowedCollTypes]).required();
|
25 | const codeSchema = Joi.string().min(1);
|
26 | const codeFileSchema = Joi.string().min(6);
|
27 | const permissionOperationSchema = Joi.string().optional().valid(AllowedRolesPermissions);
|
28 | const serviceSchemaInEnv = Joi.string()
|
29 | .when('type', { is: ConfigFiles.CollType.EXTERNAL, then: Joi.required() })
|
30 | .when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
|
31 | const svcEnvSchemaInEnv = Joi.string()
|
32 | .when('type', { is: ConfigFiles.CollType.EXTERNAL, then: Joi.required() })
|
33 | .when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
|
34 | const handlerNameSchema = Joi.string().when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
|
35 |
|
36 |
|
37 | const collHookSchema = Joi.object().optional().keys(
|
38 | {
|
39 | type: typeOfCollAndCollHookSchema,
|
40 | code: codeSchema,
|
41 | codeFile: codeFileSchema,
|
42 | service: serviceSchemaInEnv,
|
43 | serviceEnvironment: svcEnvSchemaInEnv,
|
44 | handlerName: handlerNameSchema
|
45 | })
|
46 | .and('service', 'handlerName')
|
47 | .without('code', ['codeFile']);
|
48 |
|
49 | const collHooksPlainObj = {};
|
50 | AllowedCollectionHooks.forEach((hook) => { collHooksPlainObj[hook] = collHookSchema; });
|
51 |
|
52 | const endpointSchema = Joi.object().optional().keys(
|
53 | {
|
54 | type: typeOfCollAndCollHookSchema,
|
55 | code: codeSchema,
|
56 | codeFile: codeFileSchema,
|
57 | service: serviceSchemaInEnv,
|
58 | serviceEnvironment: svcEnvSchemaInEnv,
|
59 | handlerName: handlerNameSchema,
|
60 | schedule: Joi.object().optional().keys({
|
61 | start: Joi.string().required(),
|
62 | interval: Joi.string().optional().valid(['weekly', 'daily', 'hourly', '30-minutes', '10-minutes', '5-minutes', '1-minute'])
|
63 | })
|
64 | })
|
65 | .and('service', 'handlerName')
|
66 | .without('code', ['codeFile']);
|
67 |
|
68 |
|
69 | const primarySchema = Joi.object().keys({
|
70 | schemaVersion: Joi.any().required().valid('1.0.0'),
|
71 | configType: Joi.string().required().valid(ConfigType.APP, ConfigType.ENV, ConfigType.ORG, ConfigType.SERVICE)
|
72 | });
|
73 |
|
74 | const rgxAnyExceptWhitespace = /\S/;
|
75 | const rgxCollName = /^[a-zA-Z0-9_\-]{1,123}$/;
|
76 | const rgxEndpointName = /^[a-zA-Z0-9_-]{2,}$/;
|
77 |
|
78 | const envSchemaKeys = {
|
79 | settings: Joi.object().optional().keys({
|
80 | apiVersion: Joi.number().optional(),
|
81 | name: Joi.string().optional(),
|
82 | emailVerification: Joi.object().optional().keys({
|
83 | auto: Joi.boolean().optional(),
|
84 | required: Joi.boolean().required(),
|
85 | since: Joi.string().optional()
|
86 | })
|
87 | }),
|
88 | collections: Joi.object({})
|
89 | .pattern(
|
90 | rgxCollName,
|
91 | Joi.object().keys({
|
92 | type: typeOfCollAndCollHookSchema,
|
93 | name: Joi.string().optional().regex(rgxCollName),
|
94 | permissions: Joi.alternatives().required().try(
|
95 | Joi.string().valid(AllowedBasicCollPermissions),
|
96 | Joi.object().keys({}).pattern(
|
97 | rgxAnyExceptWhitespace,
|
98 | Joi.object().required().keys({
|
99 | [PermissionsOperation.CREATE]: permissionOperationSchema,
|
100 | [PermissionsOperation.READ]: permissionOperationSchema,
|
101 | [PermissionsOperation.UPDATE]: permissionOperationSchema,
|
102 | [PermissionsOperation.DELETE]: permissionOperationSchema
|
103 | }))
|
104 | ),
|
105 | service: serviceSchemaInEnv,
|
106 | serviceEnvironment: svcEnvSchemaInEnv,
|
107 | serviceObject: handlerNameSchema
|
108 | })
|
109 | .and('service', 'serviceObject')
|
110 | ),
|
111 | commonCode: Joi.object({})
|
112 | .pattern(
|
113 | /^[a-zA-Z0-9_-]{2,}$/,
|
114 | Joi.object().keys({
|
115 | code: codeSchema,
|
116 | codeFile: codeFileSchema
|
117 | })
|
118 | .xor('code', 'codeFile')
|
119 | ),
|
120 | collectionHooks: Joi.object({})
|
121 | .pattern(
|
122 | rgxCollName,
|
123 | Joi.object().required().keys(collHooksPlainObj)
|
124 | ),
|
125 | customEndpoints: Joi.object({})
|
126 | .pattern(
|
127 | rgxEndpointName,
|
128 | endpointSchema
|
129 | ),
|
130 | roles: Joi.object({})
|
131 | .pattern(
|
132 | rgxAnyExceptWhitespace,
|
133 | Joi.object().keys({
|
134 | name: Joi.string().optional().regex(rgxAnyExceptWhitespace),
|
135 | description: Joi.string().optional()
|
136 | })
|
137 | ),
|
138 | groups: Joi.object({})
|
139 | .pattern(
|
140 | rgxAnyExceptWhitespace,
|
141 | Joi.object().keys({
|
142 | name: Joi.string().optional().regex(rgxAnyExceptWhitespace),
|
143 | description: Joi.string().optional(),
|
144 | groups: Joi.any().optional()
|
145 | })
|
146 | ),
|
147 | push: Joi.object().keys({
|
148 | android: Joi.object().optional().keys({
|
149 | senderId: Joi.string().required().min(2),
|
150 | apiKey: Joi.string().required().min(2)
|
151 | }),
|
152 | ios: Joi.object().optional().keys({
|
153 | production: Joi.boolean().required(),
|
154 | certificateFilePath: Joi.string().required(),
|
155 | password: Joi.string().optional().min(1)
|
156 | })
|
157 | })
|
158 | .or('android', 'ios')
|
159 | };
|
160 |
|
161 | const envSchema = primarySchema.keys(envSchemaKeys);
|
162 |
|
163 | const envSchemaInApp = Joi.object().keys({
|
164 | schemaVersion: Joi.any().required().valid('1.0.0')
|
165 | }).keys(envSchemaKeys)
|
166 | .keys({ configType: Joi.string().optional().valid(ConfigType.ENV) });
|
167 |
|
168 | const rapidBaseSrvSchemaKeys = {
|
169 | version: Joi.string().optional(),
|
170 | connectionOptions: Joi.object().optional(),
|
171 | authentication: Joi.object().optional().keys({
|
172 | type: Joi.string().optional().valid(['ServiceAccount', 'ServiceAccountOAuth', 'MIC', 'WindowsServiceAccount', 'None', 'Basic', 'oauthClientCredentials']),
|
173 | }).unknown(),
|
174 | mapping: Joi.object().optional().pattern(
|
175 | rgxAnyExceptWhitespace,
|
176 | Joi.object().keys({
|
177 | sourceObject: Joi.object().keys({
|
178 | objectName: Joi.string()
|
179 | }).unknown(),
|
180 | fields: Joi.array().optional(),
|
181 | methods: Joi.object().optional().keys({
|
182 | getAll: Joi.object(),
|
183 | getById: Joi.object(),
|
184 | getByQuery: Joi.object(),
|
185 | insert: Joi.object(),
|
186 | insertMany: Joi.object(),
|
187 | update: Joi.object(),
|
188 | deleteById: Joi.object(),
|
189 | deleteByQuery: Joi.object(),
|
190 | getCount: Joi.object(),
|
191 | getCountByQuery: Joi.object()
|
192 | })
|
193 | })
|
194 | )
|
195 | };
|
196 |
|
197 | const rapidHealthSrvSchemaKeys = Joi.object().required()
|
198 | .pattern(
|
199 | rgxAnyExceptWhitespace,
|
200 | Joi.object().keys(rapidBaseSrvSchemaKeys)
|
201 | );
|
202 |
|
203 | const rapidSrvEnvsSchema = Joi.object().allow(null)
|
204 | .pattern(
|
205 | rgxAnyExceptWhitespace,
|
206 | Joi.object()
|
207 | .keys({ host: Joi.string().required() })
|
208 | .keys(rapidBaseSrvSchemaKeys)
|
209 | );
|
210 |
|
211 | const rgxEnvVarKey = /^[_a-z][_a-z0-9]*$/i;
|
212 | const flexInternalSrvEnvsSchema = Joi.object().allow(null)
|
213 | .pattern(
|
214 | rgxAnyExceptWhitespace,
|
215 | Joi.object().keys({
|
216 | secret: Joi.string().required().min(2),
|
217 | sourcePath: Joi.string(),
|
218 | host: Joi.any().forbidden(),
|
219 | description: Joi.string().optional(),
|
220 | environmentVariables: Joi.object().allow(null)
|
221 | .pattern(
|
222 | rgxEnvVarKey,
|
223 | Joi.string()
|
224 | ),
|
225 | runtime: Joi.string().optional().valid(AllowedCLIRuntimes)
|
226 | })
|
227 | );
|
228 |
|
229 | const flexExternalSrvEnvsSchema = Joi.object().allow(null)
|
230 | .pattern(
|
231 | rgxAnyExceptWhitespace,
|
232 | Joi.object().keys({
|
233 | secret: Joi.string().required().min(2),
|
234 | host: Joi.string().required().min(1),
|
235 | description: Joi.string().optional()
|
236 | })
|
237 | );
|
238 |
|
239 | const serviceSchemaKeys = {
|
240 | type: Joi.string().required().valid([AllowedServiceTypesToApply]),
|
241 | name: Joi.string().min(2),
|
242 | description: Joi.string().optional().min(1),
|
243 | environments: Joi.any().allow(null)
|
244 | .when('type', { is: ConfigFiles.ServiceType.FLEX_INTERNAL, then: flexInternalSrvEnvsSchema })
|
245 | .when('type', { is: ConfigFiles.ServiceType.FLEX_EXTERNAL, then: flexExternalSrvEnvsSchema })
|
246 | .when('type', { is: ConfigFiles.ServiceType.SP, then: rapidSrvEnvsSchema })
|
247 | .when('type', { is: ConfigFiles.ServiceType.SF, then: rapidSrvEnvsSchema })
|
248 | .when('type', { is: ConfigFiles.ServiceType.SQL, then: rapidSrvEnvsSchema })
|
249 | .when('type', { is: ConfigFiles.ServiceType.REST, then: rapidSrvEnvsSchema })
|
250 | .when('type', { is: ConfigFiles.ServiceType.PROGRESS_DATA, then: rapidSrvEnvsSchema })
|
251 | .when('type', { is: ConfigFiles.ServiceType.DATA_DIRECT, then: rapidSrvEnvsSchema })
|
252 | .when('type', { is: ConfigFiles.ServiceType.POKIT_DOK, then: rapidHealthSrvSchemaKeys })
|
253 | };
|
254 |
|
255 | const serviceSchema = primarySchema.keys(serviceSchemaKeys);
|
256 |
|
257 | const serviceSchemaInAppOrOrg = Joi.object().keys({
|
258 | schemaVersion: Joi.any().required().valid('1.0.0')
|
259 | }).keys(serviceSchemaKeys)
|
260 | .keys({ configType: Joi.string().optional().valid(ConfigType.SERVICE) });
|
261 |
|
262 | const rgxMinTwoCharsNoWhitespaceAtStartAndEnd = /^\S.*\S$/;
|
263 | const rgxEnvName = rgxMinTwoCharsNoWhitespaceAtStartAndEnd;
|
264 | const rgxAppName = rgxMinTwoCharsNoWhitespaceAtStartAndEnd;
|
265 | const appSchemaKeys = {
|
266 | settings: Joi.object().optional().keys({
|
267 | realtime: Joi.object().optional().keys({
|
268 | enabled: Joi.boolean()
|
269 | }),
|
270 | sessionTimeoutInSeconds: Joi.number().optional()
|
271 | }),
|
272 | environments: Joi.object({}).optional()
|
273 | .pattern(
|
274 | rgxEnvName,
|
275 | envSchemaInApp
|
276 | ),
|
277 | services: Joi.object({}).optional()
|
278 | .pattern(
|
279 | rgxAnyExceptWhitespace,
|
280 | serviceSchemaInAppOrOrg
|
281 | )
|
282 | };
|
283 | const appSchema = primarySchema.keys(appSchemaKeys);
|
284 | const appInOrgSchema = Joi.object().keys({
|
285 | schemaVersion: Joi.any().required().valid('1.0.0')
|
286 | }).keys(appSchemaKeys)
|
287 | .keys({ configType: Joi.string().optional().valid(ConfigType.APP) });
|
288 |
|
289 | const orgSchema = primarySchema.keys({
|
290 | settings: Joi.object().optional().keys({
|
291 | security: Joi.object().optional().keys({
|
292 | requireApprovals: Joi.boolean().optional(),
|
293 | requireEmailVerification: Joi.boolean().optional(),
|
294 | requireTwoFactorAuth: Joi.boolean().optional()
|
295 | })
|
296 | }),
|
297 | applications: Joi.object({}).optional()
|
298 | .pattern(
|
299 | rgxAppName,
|
300 | appInOrgSchema
|
301 | ),
|
302 | services: Joi.object({}).optional()
|
303 | .pattern(
|
304 | rgxAnyExceptWhitespace,
|
305 | serviceSchemaInAppOrOrg
|
306 | )
|
307 | });
|
308 |
|
309 | const schemas = {
|
310 | [ConfigType.APP]: appSchema,
|
311 | [ConfigType.ENV]: envSchema,
|
312 | [ConfigType.SERVICE]: serviceSchema,
|
313 | [ConfigType.ORG]: orgSchema
|
314 | };
|
315 |
|
316 | class SchemaValidator {
|
317 | static validate(type, value, options, done) {
|
318 | const defaultOptions = { abortEarly: false };
|
319 | options = options || defaultOptions;
|
320 |
|
321 | schemas[type].validate(value, options, (err) => {
|
322 | if (err) {
|
323 | let transformedErrMsg = '';
|
324 | if (Array.isArray(err.details)) {
|
325 | err.details.forEach((d) => {
|
326 | const propertyPath = d.path.join('.');
|
327 | transformedErrMsg = `${transformedErrMsg}${EOL}\t${propertyPath}: ${d.message}`;
|
328 | });
|
329 |
|
330 | return done(new KinveyError(err.name, transformedErrMsg));
|
331 | }
|
332 |
|
333 | return done(err);
|
334 | }
|
335 |
|
336 | done();
|
337 | });
|
338 | }
|
339 | }
|
340 |
|
341 | module.exports = SchemaValidator;
|