turbo-schema 0.1.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.
Files changed (3) hide show
  1. package/index.js +218 -0
  2. package/package.json +31 -0
  3. package/readme.md +452 -0
package/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // index.js:
2
+
3
+ "use strict";
4
+
5
+ const UUID =
6
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+ class ValidationError extends Error {
8
+ constructor(errors) {
9
+ const message = errors.length
10
+ ? `Schema validation failed: ${errors.join(", ")}`
11
+ : "Schema validation failed";
12
+
13
+ super(message);
14
+
15
+ this.name = "ValidationError";
16
+
17
+ // backward compatibility
18
+ this.errors = errors;
19
+
20
+ // structured version
21
+ this.details = errors.map((msg) => {
22
+ const [field, ...rest] = msg.split(" ");
23
+ return {
24
+ field,
25
+ message: rest.join(" "),
26
+ };
27
+ });
28
+ }
29
+ }
30
+
31
+ class Schema {
32
+ constructor(def = {}) {
33
+ if (!def || typeof def !== "object" || Array.isArray(def)) {
34
+ throw new TypeError("Schema must be an object");
35
+ }
36
+ this.def = def;
37
+ }
38
+
39
+ validate(
40
+ data = {},
41
+ { partial = false, coerce = true, stripUnknown = true } = {},
42
+ ) {
43
+ const out = {};
44
+ const errors = [];
45
+
46
+ for (const [k, c] of Object.entries(this.def)) {
47
+ let v = data[k];
48
+ const exists = Object.hasOwn(data, k);
49
+
50
+ if (partial && !exists) continue;
51
+
52
+ // default values
53
+ if (v == null && c.default !== undefined) {
54
+ v = typeof c.default === "function" ? c.default() : c.default;
55
+ }
56
+
57
+ // required check
58
+ if (c.required && v == null) {
59
+ errors.push(`${k} required`);
60
+ continue;
61
+ }
62
+
63
+ if (v == null) continue;
64
+
65
+ // envvar special case (ignore input value)
66
+ if (c.type === "envvar") {
67
+ const envName = c.varname;
68
+
69
+ if (!envName) {
70
+ errors.push(`${k} missing varname`);
71
+ continue;
72
+ }
73
+
74
+ v = process.env[envName];
75
+
76
+ if (v == null) {
77
+ errors.push(`${k} missing env var ${envName}`);
78
+ continue;
79
+ }
80
+ }
81
+
82
+ // coercion
83
+ if (coerce) v = this._coerce(v, c.type, c);
84
+
85
+ // type check
86
+ const typeErr = this._type(k, v, c.type);
87
+ if (typeErr) {
88
+ errors.push(typeErr);
89
+ continue;
90
+ }
91
+
92
+ // enum check
93
+ if (c.enum && !c.enum.includes(v)) {
94
+ errors.push(`${k} invalid enum`);
95
+ continue;
96
+ }
97
+
98
+ // pattern check
99
+ if (c.pattern) {
100
+ const ok =
101
+ c.pattern instanceof RegExp
102
+ ? c.pattern.test(v)
103
+ : typeof c.pattern.test === "function" && c.pattern.test(v);
104
+
105
+ if (!ok) {
106
+ errors.push(`${k} pattern mismatch`);
107
+ continue;
108
+ }
109
+ }
110
+
111
+ // min / max
112
+ if (c.min != null && v < c.min) {
113
+ errors.push(`${k} too small`);
114
+ continue;
115
+ }
116
+
117
+ if (c.max != null && v > c.max) {
118
+ errors.push(`${k} too large`);
119
+ continue;
120
+ }
121
+
122
+ out[k] = v;
123
+ }
124
+
125
+ // unknown fields
126
+ if (!stripUnknown) {
127
+ for (const k in data) {
128
+ if (!Object.hasOwn(this.def, k)) {
129
+ out[k] = data[k];
130
+ }
131
+ }
132
+ }
133
+
134
+ if (errors.length) {
135
+ throw new ValidationError(errors);
136
+ }
137
+
138
+ return out;
139
+ }
140
+
141
+ isValid(d, o) {
142
+ try {
143
+ this.validate(d, o);
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ fields() {
151
+ return Object.keys(this.def);
152
+ }
153
+
154
+ // ---------------- internal ----------------
155
+
156
+ _coerce(v, t, c) {
157
+ switch (t) {
158
+ case "string":
159
+ return String(v);
160
+
161
+ case "integer":
162
+ return Number.isInteger(+v) ? +v : v;
163
+
164
+ case "number":
165
+ return Number.isFinite(+v) ? +v : v;
166
+
167
+ case "boolean":
168
+ return v === "true" ? true : v === "false" ? false : v;
169
+
170
+ case "date":
171
+ return v instanceof Date ? v.toISOString().slice(0, 10) : String(v);
172
+
173
+ case "datetime":
174
+ return v instanceof Date ? v : new Date(v);
175
+
176
+ case "envvar":
177
+ return v;
178
+
179
+ default:
180
+ return v;
181
+ }
182
+ }
183
+
184
+ _type(k, v, t) {
185
+ switch (t) {
186
+ case "string":
187
+ return typeof v === "string" ? null : `${k} must be string`;
188
+
189
+ case "integer":
190
+ return Number.isInteger(v) ? null : `${k} must be integer`;
191
+
192
+ case "number":
193
+ return typeof v === "number" ? null : `${k} must be number`;
194
+
195
+ case "boolean":
196
+ return typeof v === "boolean" ? null : `${k} must be boolean`;
197
+
198
+ case "uuid":
199
+ return UUID.test(v) ? null : `${k} invalid uuid`;
200
+
201
+ case "array":
202
+ return Array.isArray(v) ? null : `${k} must be array`;
203
+
204
+ case "object":
205
+ return v && typeof v === "object" && !Array.isArray(v)
206
+ ? null
207
+ : `${k} must be object`;
208
+
209
+ case "envvar":
210
+ return v != null ? null : `${k} missing env value`;
211
+
212
+ default:
213
+ return null;
214
+ }
215
+ }
216
+ }
217
+
218
+ module.exports = { Schema, ValidationError };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "type": "commonjs",
3
+ "name": "turbo-schema",
4
+ "version": "0.1.0",
5
+ "description": "Light weight object validation schema",
6
+ "main": "index.js",
7
+ "keywords": [
8
+ "light",
9
+ "weight",
10
+ "object",
11
+ "schema",
12
+ "validate",
13
+ "validation"
14
+ ],
15
+ "homepage": "https://github.com/miketerry-org/turbo-schema#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/miketerry-org/turbo-schema/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/miketerry-org/turbo-schema.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Mike Terry",
25
+ "scripts": {
26
+ "test": "node --test",
27
+ "test:watch": "node --test --watch",
28
+ "test:coverage": "node --test --experimental-test-coverage",
29
+ "test:only": "node --test --test-only"
30
+ }
31
+ }
package/readme.md ADDED
@@ -0,0 +1,452 @@
1
+ # Turbo Schema
2
+
3
+ A lightweight, dependency-free schema validation library for Node.js.
4
+
5
+ Turbo Schema validates JavaScript objects against a declarative schema definition. It supports type coercion, default values, required fields, enumerations, regular expression validation, numeric ranges, UUID validation, and configurable handling of unknown properties.
6
+
7
+ ## Features
8
+
9
+ - Zero dependencies
10
+ - Declarative object schemas
11
+ - Type validation
12
+ - Optional type coercion
13
+ - Default values
14
+ - Required fields
15
+ - Enum validation
16
+ - Pattern (RegExp) validation
17
+ - Minimum and maximum value validation
18
+ - UUID validation
19
+ - Partial validation for updates
20
+ - Optional removal of unknown properties
21
+ - Structured validation errors
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install turbo-schema
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Basic Usage
34
+
35
+ ```javascript
36
+ const { Schema } = require("turbo-schema");
37
+
38
+ const userSchema = new Schema({
39
+ id: {
40
+ type: "uuid",
41
+ required: true,
42
+ },
43
+
44
+ name: {
45
+ type: "string",
46
+ required: true,
47
+ },
48
+
49
+ age: {
50
+ type: "integer",
51
+ min: 18,
52
+ },
53
+
54
+ active: {
55
+ type: "boolean",
56
+ default: true,
57
+ },
58
+ });
59
+
60
+ const user = userSchema.validate({
61
+ id: "550e8400-e29b-41d4-a716-446655440000",
62
+ name: "Alice",
63
+ age: "25",
64
+ });
65
+
66
+ console.log(user);
67
+ ```
68
+
69
+ Output:
70
+
71
+ ```javascript
72
+ {
73
+ id: "550e8400-e29b-41d4-a716-446655440000",
74
+ name: "Alice",
75
+ age: 25,
76
+ active: true
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ # Schema Definition
83
+
84
+ A schema consists of an object whose keys represent field names and whose values define validation rules.
85
+
86
+ Example:
87
+
88
+ ```javascript
89
+ const schema = new Schema({
90
+ username: {
91
+ type: "string",
92
+ required: true,
93
+ },
94
+ });
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Field Properties
100
+
101
+ | Property | Type | Description |
102
+ | ---------- | -------------- | ------------------------------------------------------ |
103
+ | `type` | String | Expected data type |
104
+ | `required` | Boolean | Field must be present |
105
+ | `default` | Any / Function | Default value if missing or `null` |
106
+ | `enum` | Array | List of allowed values |
107
+ | `pattern` | RegExp | Regular expression or any object implementing `test()` |
108
+ | `min` | Number | Minimum numeric value |
109
+ | `max` | Number | Maximum numeric value |
110
+
111
+ ---
112
+
113
+ ## Supported Types
114
+
115
+ | Type | Description |
116
+ | ---------- | -------------------------- |
117
+ | `string` | String value |
118
+ | `integer` | Integer number |
119
+ | `number` | Floating point or integer |
120
+ | `boolean` | Boolean value |
121
+ | `uuid` | UUID string |
122
+ | `array` | JavaScript array |
123
+ | `object` | Plain JavaScript object |
124
+ | `date` | Date string (`YYYY-MM-DD`) |
125
+ | `datetime` | JavaScript `Date` object |
126
+
127
+ ---
128
+
129
+ # Validation
130
+
131
+ ```javascript
132
+ const validated = schema.validate(data);
133
+ ```
134
+
135
+ Returns a new validated object.
136
+
137
+ If validation fails, a `ValidationError` is thrown.
138
+
139
+ ---
140
+
141
+ # Validation Options
142
+
143
+ ```javascript
144
+ schema.validate(data, {
145
+ partial: false,
146
+ coerce: true,
147
+ stripUnknown: true,
148
+ });
149
+ ```
150
+
151
+ ## partial
152
+
153
+ Default: `false`
154
+
155
+ When `true`, fields that are missing from the input object are ignored instead of being validated.
156
+
157
+ Useful for PATCH requests or partial updates.
158
+
159
+ ```javascript
160
+ schema.validate(update, {
161
+ partial: true,
162
+ });
163
+ ```
164
+
165
+ ---
166
+
167
+ ## coerce
168
+
169
+ Default: `true`
170
+
171
+ Automatically converts values into the expected type when possible.
172
+
173
+ Example:
174
+
175
+ Input:
176
+
177
+ ```javascript
178
+ {
179
+ age: "42";
180
+ }
181
+ ```
182
+
183
+ Output:
184
+
185
+ ```javascript
186
+ {
187
+ age: 42;
188
+ }
189
+ ```
190
+
191
+ Disable coercion:
192
+
193
+ ```javascript
194
+ schema.validate(data, {
195
+ coerce: false,
196
+ });
197
+ ```
198
+
199
+ ---
200
+
201
+ ## stripUnknown
202
+
203
+ Default: `true`
204
+
205
+ Controls how properties not defined in the schema are handled.
206
+
207
+ Input:
208
+
209
+ ```javascript
210
+ {
211
+ name: "Alice",
212
+ admin: true
213
+ }
214
+ ```
215
+
216
+ Schema:
217
+
218
+ ```javascript
219
+ const schema = new Schema({
220
+ name: {
221
+ type: "string",
222
+ },
223
+ });
224
+ ```
225
+
226
+ With the default setting:
227
+
228
+ ```javascript
229
+ {
230
+ name: "Alice";
231
+ }
232
+ ```
233
+
234
+ To preserve unknown properties:
235
+
236
+ ```javascript
237
+ schema.validate(data, {
238
+ stripUnknown: false,
239
+ });
240
+ ```
241
+
242
+ Result:
243
+
244
+ ```javascript
245
+ {
246
+ name: "Alice",
247
+ admin: true
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ # Default Values
254
+
255
+ Defaults are applied when a property is `null` or `undefined`.
256
+
257
+ Static default:
258
+
259
+ ```javascript
260
+ count: {
261
+ type: "integer",
262
+ default: 0
263
+ }
264
+ ```
265
+
266
+ Function default:
267
+
268
+ ```javascript
269
+ created: {
270
+ type: "datetime",
271
+ default: () => new Date()
272
+ }
273
+ ```
274
+
275
+ ---
276
+
277
+ # Required Fields
278
+
279
+ ```javascript
280
+ email: {
281
+ type: "string",
282
+ required: true
283
+ }
284
+ ```
285
+
286
+ Missing required fields generate validation errors.
287
+
288
+ ---
289
+
290
+ # Enumerations
291
+
292
+ ```javascript
293
+ status: {
294
+ enum: ["active", "inactive", "pending"];
295
+ }
296
+ ```
297
+
298
+ Only values contained in the array are accepted.
299
+
300
+ ---
301
+
302
+ # Pattern Validation
303
+
304
+ ```javascript
305
+ zip: {
306
+ type: "string",
307
+ pattern: /^\d{5}$/
308
+ }
309
+ ```
310
+
311
+ Any object implementing a `test()` method may also be supplied.
312
+
313
+ ---
314
+
315
+ # Numeric Ranges
316
+
317
+ ```javascript
318
+ age: {
319
+ type: "integer",
320
+ min: 18,
321
+ max: 120
322
+ }
323
+ ```
324
+
325
+ ---
326
+
327
+ # UUID Validation
328
+
329
+ ```javascript
330
+ id: {
331
+ type: "uuid";
332
+ }
333
+ ```
334
+
335
+ UUID values are validated using a built-in regular expression.
336
+
337
+ ---
338
+
339
+ # Validation Errors
340
+
341
+ Validation failures throw a `ValidationError`.
342
+
343
+ ```javascript
344
+ try {
345
+ schema.validate(data);
346
+ } catch (err) {
347
+ console.error(err.message);
348
+ }
349
+ ```
350
+
351
+ Example:
352
+
353
+ ```
354
+ Schema validation failed: age too small, email required
355
+ ```
356
+
357
+ ---
358
+
359
+ ## ValidationError Properties
360
+
361
+ ### message
362
+
363
+ A human-readable summary of the validation failures.
364
+
365
+ Example:
366
+
367
+ ```
368
+ Schema validation failed: age too small, email required
369
+ ```
370
+
371
+ ---
372
+
373
+ ### errors
374
+
375
+ An array of validation messages.
376
+
377
+ ```javascript
378
+ ["age too small", "email required"];
379
+ ```
380
+
381
+ ---
382
+
383
+ ### details
384
+
385
+ A structured version of each validation error.
386
+
387
+ ```javascript
388
+ [
389
+ {
390
+ field: "age",
391
+ message: "too small",
392
+ },
393
+ {
394
+ field: "email",
395
+ message: "required",
396
+ },
397
+ ];
398
+ ```
399
+
400
+ ---
401
+
402
+ # isValid()
403
+
404
+ Returns `true` if validation succeeds, otherwise `false`.
405
+
406
+ ```javascript
407
+ if (schema.isValid(data)) {
408
+ console.log("Valid");
409
+ }
410
+ ```
411
+
412
+ Equivalent to calling `validate()` and catching any `ValidationError`.
413
+
414
+ ---
415
+
416
+ # fields()
417
+
418
+ Returns an array of field names defined by the schema.
419
+
420
+ ```javascript
421
+ schema.fields();
422
+ ```
423
+
424
+ Example:
425
+
426
+ ```javascript
427
+ ["id", "name", "email"];
428
+ ```
429
+
430
+ ---
431
+
432
+ # Type Coercion
433
+
434
+ When coercion is enabled (the default), Turbo Schema performs the following conversions where possible.
435
+
436
+ | Type | Example |
437
+ | ---------- | --------------------- |
438
+ | `string` | `123 → "123"` |
439
+ | `integer` | `"42" → 42` |
440
+ | `number` | `"3.14" → 3.14` |
441
+ | `boolean` | `"true" → true` |
442
+ | `boolean` | `"false" → false` |
443
+ | `date` | `Date → "YYYY-MM-DD"` |
444
+ | `datetime` | ISO string → `Date` |
445
+
446
+ If a value cannot be coerced, the original value is retained and normal validation determines whether it is valid.
447
+
448
+ ---
449
+
450
+ # License
451
+
452
+ MIT