@bedrockio/yada 1.0.0

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/src/object.js ADDED
@@ -0,0 +1,102 @@
1
+ import TypeSchema from './TypeSchema';
2
+ import { FieldError, LocalizedError } from './errors';
3
+ import { wrapSchema } from './utils';
4
+ import { isSchema } from './Schema';
5
+
6
+ const BASE_ASSERTIONS = ['type', 'transform', 'field'];
7
+
8
+ class ObjectSchema extends TypeSchema {
9
+ constructor(fields = {}) {
10
+ super(Object, { message: 'Object failed validation.', fields });
11
+ this.setup();
12
+ }
13
+
14
+ setup() {
15
+ this.assert('type', (val) => {
16
+ if (val === null || typeof val !== 'object') {
17
+ throw new LocalizedError('Must be an object.');
18
+ }
19
+ });
20
+ this.transform((obj, options) => {
21
+ const { fields, stripUnknown } = options;
22
+ const allowUnknown = Object.keys(fields).length === 0;
23
+ if (obj) {
24
+ const result = {};
25
+ for (let key of Object.keys(obj)) {
26
+ if (key in fields || allowUnknown) {
27
+ result[key] = obj[key];
28
+ } else if (!stripUnknown) {
29
+ throw new LocalizedError('Unknown field "{key}".', {
30
+ key,
31
+ });
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ });
37
+ for (let [key, schema] of Object.entries(this.meta.fields)) {
38
+ this.assert('field', async (obj, options) => {
39
+ if (obj) {
40
+ let val;
41
+ try {
42
+ val = await schema.validate(obj[key], options);
43
+ } catch (error) {
44
+ if (error.details?.length === 1) {
45
+ const { message, original } = error.details[0];
46
+ throw new FieldError(message, key, original);
47
+ } else {
48
+ throw new FieldError(
49
+ 'Field failed validation.',
50
+ key,
51
+ error.original,
52
+ error.details
53
+ );
54
+ }
55
+ }
56
+ return {
57
+ ...obj,
58
+ ...(val !== undefined && {
59
+ [key]: val,
60
+ }),
61
+ };
62
+ }
63
+ });
64
+ }
65
+ }
66
+
67
+ append(arg) {
68
+ const schema = isSchema(arg) ? arg : new ObjectSchema(arg);
69
+
70
+ const fields = {
71
+ ...this.meta.fields,
72
+ ...schema.meta.fields,
73
+ };
74
+
75
+ const merged = new ObjectSchema(fields);
76
+
77
+ const assertions = [...this.assertions, ...schema.assertions];
78
+ for (let assertion of assertions) {
79
+ const { type } = assertion;
80
+ if (!BASE_ASSERTIONS.includes(type)) {
81
+ merged.pushAssertion(assertion);
82
+ }
83
+ }
84
+
85
+ return merged;
86
+ }
87
+
88
+ toOpenApi(extra) {
89
+ const properties = {};
90
+ for (let [key, schema] of Object.entries(this.meta.fields)) {
91
+ properties[key] = schema.toOpenApi();
92
+ }
93
+ return {
94
+ ...super.toOpenApi(extra),
95
+ ...(Object.keys(properties).length > 0 && {
96
+ properties,
97
+ }),
98
+ };
99
+ }
100
+ }
101
+
102
+ export default wrapSchema(ObjectSchema);
@@ -0,0 +1,61 @@
1
+ import { LocalizedError } from './errors';
2
+
3
+ const LOWER_REG = /[a-z]/g;
4
+ const UPPER_REG = /[A-Z]/g;
5
+ const NUMBER_REG = /[0-9]/g;
6
+ const SYMBOL_REG = /[!@#$%^&*]/g;
7
+
8
+ export const PASSWORD_DEFAULTS = {
9
+ minLength: 12,
10
+ minLowercase: 0,
11
+ minUppercase: 0,
12
+ minNumbers: 0,
13
+ minSymbols: 0,
14
+ };
15
+
16
+ export function validateLength(expected) {
17
+ return (str = '') => {
18
+ if (str.length < expected) {
19
+ const s = expected === 1 ? '' : 's';
20
+ throw new LocalizedError('Must be at least {length} character{s}.', {
21
+ length: expected,
22
+ s,
23
+ });
24
+ }
25
+ };
26
+ }
27
+
28
+ export const validateLowercase = validateRegex(
29
+ LOWER_REG,
30
+ 'Must contain at least {length} lowercase character{s}.'
31
+ );
32
+
33
+ export const validateUppercase = validateRegex(
34
+ UPPER_REG,
35
+ 'Must contain at least {length} uppercase character{s}.'
36
+ );
37
+
38
+ export const validateNumbers = validateRegex(
39
+ NUMBER_REG,
40
+ 'Must contain at least {length} number{s}.'
41
+ );
42
+
43
+ export const validateSymbols = validateRegex(
44
+ SYMBOL_REG,
45
+ 'Must contain at least {length} symbol{s}.'
46
+ );
47
+
48
+ function validateRegex(reg, message) {
49
+ return (expected) => {
50
+ return (str = '') => {
51
+ const length = str.match(reg)?.length || 0;
52
+ if (length < expected) {
53
+ const s = expected === 1 ? '' : 's';
54
+ throw new LocalizedError(message, {
55
+ length: expected,
56
+ s,
57
+ });
58
+ }
59
+ };
60
+ };
61
+ }
package/src/string.js ADDED
@@ -0,0 +1,303 @@
1
+ import validator from 'validator';
2
+ import TypeSchema from './TypeSchema';
3
+ import { LocalizedError } from './errors';
4
+ import { wrapSchema } from './utils';
5
+ import {
6
+ PASSWORD_DEFAULTS,
7
+ validateLength,
8
+ validateLowercase,
9
+ validateUppercase,
10
+ validateNumbers,
11
+ validateSymbols,
12
+ } from './password';
13
+
14
+ const SLUG_REG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
+
16
+ class StringSchema extends TypeSchema {
17
+ constructor() {
18
+ super(String);
19
+ this.assert('type', (val, options) => {
20
+ if (typeof val !== 'string' && options.cast) {
21
+ val = String(val);
22
+ }
23
+ if (typeof val !== 'string') {
24
+ throw new LocalizedError('Must be a string.');
25
+ }
26
+ return val;
27
+ });
28
+ }
29
+
30
+ min(length) {
31
+ return this.clone({ min: length }).assert('length', (str) => {
32
+ if (str && str.length < length) {
33
+ throw new LocalizedError('Must be {length} characters or more.', {
34
+ length,
35
+ });
36
+ }
37
+ });
38
+ }
39
+
40
+ max(length) {
41
+ return this.clone({ max: length }).assert('length', (str) => {
42
+ if (str && str.length > length) {
43
+ throw new LocalizedError('Must be {length} characters or less.', {
44
+ length,
45
+ });
46
+ }
47
+ });
48
+ }
49
+
50
+ trim() {
51
+ return this.clone().transform((str) => {
52
+ return str.trim();
53
+ });
54
+ }
55
+
56
+ lowercase(assert = false) {
57
+ return this.clone().transform((str) => {
58
+ const lower = str.toLowerCase();
59
+ if (lower !== str) {
60
+ if (assert) {
61
+ throw new LocalizedError('Must be in lower case.');
62
+ }
63
+ return lower;
64
+ }
65
+ });
66
+ }
67
+
68
+ uppercase(assert = false) {
69
+ return this.clone().transform((str) => {
70
+ const upper = str.toUpperCase();
71
+ if (upper !== str) {
72
+ if (assert) {
73
+ throw new LocalizedError('Must be in upper case.');
74
+ }
75
+ return upper;
76
+ }
77
+ });
78
+ }
79
+
80
+ match(reg) {
81
+ if (!(reg instanceof RegExp)) {
82
+ throw new LocalizedError('Argument must be a regular expression');
83
+ }
84
+ return this.clone().assert('regex', (str) => {
85
+ if (str && !reg.test(str)) {
86
+ throw new LocalizedError('Must match pattern {reg}.', {
87
+ reg,
88
+ });
89
+ }
90
+ });
91
+ }
92
+
93
+ email() {
94
+ return this.format('email', (str) => {
95
+ if (!validator.isEmail(str)) {
96
+ throw new LocalizedError('Must be an email address.');
97
+ }
98
+ });
99
+ }
100
+
101
+ hex() {
102
+ return this.format('hex', (str) => {
103
+ if (!validator.isHexadecimal(str)) {
104
+ throw new LocalizedError('Must be hexadecimal.');
105
+ }
106
+ });
107
+ }
108
+
109
+ md5() {
110
+ return this.format('md5', (str) => {
111
+ if (!validator.isHash(str, 'md5')) {
112
+ throw new LocalizedError('Must be a hash in md5 format.');
113
+ }
114
+ });
115
+ }
116
+
117
+ sha1() {
118
+ return this.format('sha1', (str) => {
119
+ if (!validator.isHash(str, 'sha1')) {
120
+ throw new LocalizedError('Must be a hash in sha1 format.');
121
+ }
122
+ });
123
+ }
124
+
125
+ ascii() {
126
+ return this.format('ascii', (str) => {
127
+ if (!validator.isAscii(str)) {
128
+ throw new LocalizedError('Must be ASCII.');
129
+ }
130
+ });
131
+ }
132
+
133
+ base64(options) {
134
+ return this.format('base64', (str) => {
135
+ if (!validator.isBase64(str, options)) {
136
+ throw new LocalizedError('Must be base64.');
137
+ }
138
+ });
139
+ }
140
+
141
+ creditCard() {
142
+ return this.format('credit-card', (str) => {
143
+ if (!validator.isCreditCard(str)) {
144
+ throw new LocalizedError('Must be a valid credit card number.');
145
+ }
146
+ });
147
+ }
148
+
149
+ ip() {
150
+ return this.format('ip', (str) => {
151
+ if (!validator.isIP(str)) {
152
+ throw new LocalizedError('Must be a valid IP address.');
153
+ }
154
+ });
155
+ }
156
+
157
+ country() {
158
+ return this.format('country', (str) => {
159
+ if (!validator.isISO31661Alpha2(str)) {
160
+ throw new LocalizedError('Must be a valid country code.');
161
+ }
162
+ });
163
+ }
164
+
165
+ locale() {
166
+ return this.format('locale', (str) => {
167
+ if (!validator.isLocale(str)) {
168
+ throw new LocalizedError('Must be a valid locale code.');
169
+ }
170
+ });
171
+ }
172
+
173
+ jwt() {
174
+ return this.format('jwt', (str) => {
175
+ if (!validator.isJWT(str)) {
176
+ throw new LocalizedError('Must be a valid JWT token.');
177
+ }
178
+ });
179
+ }
180
+
181
+ slug() {
182
+ return this.format('slug', (str) => {
183
+ // Validator shows some issues here so use a custom regex.
184
+ if (!SLUG_REG.test(str)) {
185
+ throw new LocalizedError('Must be a valid slug.');
186
+ }
187
+ });
188
+ }
189
+
190
+ latlng() {
191
+ return this.format('latlng', (str) => {
192
+ if (!validator.isLatLong(str)) {
193
+ throw new LocalizedError('Must be a valid lat,lng coordinate.');
194
+ }
195
+ });
196
+ }
197
+
198
+ postalCode(locale = 'any') {
199
+ return this.format('postal-code', (str) => {
200
+ if (!validator.isPostalCode(str, locale)) {
201
+ throw new LocalizedError('Must be a valid postal code.');
202
+ }
203
+ });
204
+ }
205
+
206
+ password(options = {}) {
207
+ const { minLength, minLowercase, minUppercase, minNumbers, minSymbols } = {
208
+ ...PASSWORD_DEFAULTS,
209
+ ...options,
210
+ };
211
+
212
+ const schema = this.clone();
213
+
214
+ if (minLength) {
215
+ schema.assert('password', validateLength(minLength));
216
+ }
217
+ if (minLowercase) {
218
+ schema.assert('password', validateLowercase(minLowercase));
219
+ }
220
+ if (minUppercase) {
221
+ schema.assert('password', validateUppercase(minUppercase));
222
+ }
223
+ if (minNumbers) {
224
+ schema.assert('password', validateNumbers(minNumbers));
225
+ }
226
+ if (minSymbols) {
227
+ schema.assert('password', validateSymbols(minSymbols));
228
+ }
229
+
230
+ return schema;
231
+ }
232
+
233
+ url(options) {
234
+ return this.format('url', (str) => {
235
+ if (!validator.isURL(str, options)) {
236
+ throw new LocalizedError('Must be a valid URL.');
237
+ }
238
+ });
239
+ }
240
+
241
+ domain(options) {
242
+ return this.format('domain', (str) => {
243
+ if (!validator.isFQDN(str, options)) {
244
+ throw new LocalizedError('Must be a valid domain.');
245
+ }
246
+ });
247
+ }
248
+
249
+ uuid(version) {
250
+ return this.format('uuid', (str) => {
251
+ if (!validator.isUUID(str, version)) {
252
+ throw new LocalizedError('Must be a valid unique id.');
253
+ }
254
+ });
255
+ }
256
+
257
+ btc() {
258
+ return this.format('bitcoin-address', (str) => {
259
+ if (!validator.isBtcAddress(str)) {
260
+ throw new LocalizedError('Must be a valid Bitcoin address.');
261
+ }
262
+ });
263
+ }
264
+
265
+ eth() {
266
+ return this.format('etherium-address', (str) => {
267
+ if (!validator.isEthereumAddress(str)) {
268
+ throw new LocalizedError('Must be a valid Ethereum address.');
269
+ }
270
+ });
271
+ }
272
+
273
+ swift() {
274
+ return this.format('swift-code', (str) => {
275
+ if (!validator.isBIC(str)) {
276
+ throw new LocalizedError('Must be a valid SWIFT code.');
277
+ }
278
+ });
279
+ }
280
+
281
+ mongo() {
282
+ return this.format('mongo-object-id', (str) => {
283
+ if (!validator.isMongoId(str)) {
284
+ throw new LocalizedError('Must be a valid ObjectId.');
285
+ }
286
+ });
287
+ }
288
+
289
+ toOpenApi(extra) {
290
+ const { min, max } = this.meta;
291
+ return {
292
+ ...super.toOpenApi(extra),
293
+ ...(min != null && {
294
+ minLength: min,
295
+ }),
296
+ ...(max != null && {
297
+ maxLength: max,
298
+ }),
299
+ };
300
+ }
301
+ }
302
+
303
+ export default wrapSchema(StringSchema);
package/src/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ import Schema from './Schema';
2
+
3
+ export function wrapSchema(Class) {
4
+ return (...args) => {
5
+ return new Class(...args);
6
+ };
7
+ }
8
+
9
+ export function wrapArgs(name) {
10
+ return (...args) => {
11
+ return new Schema()[name](...args);
12
+ };
13
+ }
14
+
15
+ export function wrapAny() {
16
+ return () => {
17
+ return new Schema();
18
+ };
19
+ }
20
+
21
+ export { isSchema } from './Schema';
22
+ export { isSchemaError } from './errors';