UNPKG

12.1 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2018, Kinvey, Inc. All rights reserved.
3 *
4 * This software is licensed to you under the Kinvey terms of service located at
5 * http://www.kinvey.com/terms-of-use. By downloading, accessing and/or using this
6 * software, you hereby accept such terms of service (and any agreement referenced
7 * therein) and agree that you have read, understand and agree to be bound by such
8 * terms of service and are of legal age to agree to such terms with Kinvey.
9 *
10 * This software contains valuable confidential and proprietary information of
11 * KINVEY, INC and is subject to applicable licensing agreements.
12 * Unauthorized reproduction, transmission or distribution of this file and its
13 * contents is a violation of applicable laws.
14 */
15
16const EOL = require('os').EOL;
17
18const Joi = require('joi');
19
20const { AllowedBasicCollPermissions, AllowedCollectionHooks, AllowedRolesPermissions, AllowedServiceTypesToApply,
21 AllowedCLIRuntimes, ConfigType, ConfigFiles, PermissionsOperation } = require('./Constants');
22const KinveyError = require('./KinveyError');
23
24const typeOfCollAndCollHookSchema = Joi.string().valid([ConfigFiles.AllowedCollTypes]).required();
25const codeSchema = Joi.string().min(1);
26const codeFileSchema = Joi.string().min(6);
27const permissionOperationSchema = Joi.string().optional().valid(AllowedRolesPermissions);
28const serviceSchemaInEnv = Joi.string()
29 .when('type', { is: ConfigFiles.CollType.EXTERNAL, then: Joi.required() })
30 .when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
31const svcEnvSchemaInEnv = Joi.string()
32 .when('type', { is: ConfigFiles.CollType.EXTERNAL, then: Joi.required() })
33 .when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
34const handlerNameSchema = Joi.string().when('type', { is: ConfigFiles.CollType.INTERNAL, then: Joi.forbidden() });
35
36// TODO: config-management Find a way to require code or codeFile if type is internal
37const 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
49const collHooksPlainObj = {};
50AllowedCollectionHooks.forEach((hook) => { collHooksPlainObj[hook] = collHookSchema; });
51
52const 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
69const 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
74const rgxAnyExceptWhitespace = /\S/;
75const rgxCollName = /^[a-zA-Z0-9_\-]{1,123}$/; //eslint-disable-line
76const rgxEndpointName = /^[a-zA-Z0-9_-]{2,}$/;
77
78const 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
161const envSchema = primarySchema.keys(envSchemaKeys);
162
163const 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
168const 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, // service object name
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
197const rapidHealthSrvSchemaKeys = Joi.object().required()
198 .pattern(
199 rgxAnyExceptWhitespace,
200 Joi.object().keys(rapidBaseSrvSchemaKeys)
201 );
202
203const rapidSrvEnvsSchema = Joi.object().allow(null)
204 .pattern(
205 rgxAnyExceptWhitespace,
206 Joi.object()
207 .keys({ host: Joi.string().required() })
208 .keys(rapidBaseSrvSchemaKeys)
209 );
210
211const rgxEnvVarKey = /^[_a-z][_a-z0-9]*$/i;
212const 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
229const 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
239const 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
255const serviceSchema = primarySchema.keys(serviceSchemaKeys);
256
257const 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
262const rgxMinTwoCharsNoWhitespaceAtStartAndEnd = /^\S.*\S$/;
263const rgxEnvName = rgxMinTwoCharsNoWhitespaceAtStartAndEnd;
264const rgxAppName = rgxMinTwoCharsNoWhitespaceAtStartAndEnd;
265const 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};
283const appSchema = primarySchema.keys(appSchemaKeys);
284const 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
289const 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
309const schemas = {
310 [ConfigType.APP]: appSchema,
311 [ConfigType.ENV]: envSchema,
312 [ConfigType.SERVICE]: serviceSchema,
313 [ConfigType.ORG]: orgSchema
314};
315
316class SchemaValidator {
317 static validate(type, value, options, done) {
318 const defaultOptions = { abortEarly: false };
319 options = options || defaultOptions; // eslint-disable-line no-param-reassign
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
341module.exports = SchemaValidator;