schematox 0.0.1

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 (39) hide show
  1. package/.eslintrc.json +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +373 -0
  4. package/dist/base-schema-parser.d.ts +6 -0
  5. package/dist/base-schema-parser.js +325 -0
  6. package/dist/base-schema-validator.d.ts +6 -0
  7. package/dist/base-schema-validator.js +246 -0
  8. package/dist/error.d.ts +54 -0
  9. package/dist/error.js +33 -0
  10. package/dist/general-schema-parser.d.ts +4 -0
  11. package/dist/general-schema-parser.js +125 -0
  12. package/dist/general-schema-validator.d.ts +4 -0
  13. package/dist/general-schema-validator.js +116 -0
  14. package/dist/index.js +36 -0
  15. package/dist/index.ts +56 -0
  16. package/dist/programmatic-schema/array.d.ts +23 -0
  17. package/dist/programmatic-schema/array.js +40 -0
  18. package/dist/programmatic-schema/boolean.d.ts +57 -0
  19. package/dist/programmatic-schema/boolean.js +57 -0
  20. package/dist/programmatic-schema/buffer.d.ts +72 -0
  21. package/dist/programmatic-schema/buffer.js +61 -0
  22. package/dist/programmatic-schema/number-union.d.ts +66 -0
  23. package/dist/programmatic-schema/number-union.js +61 -0
  24. package/dist/programmatic-schema/number.d.ts +81 -0
  25. package/dist/programmatic-schema/number.js +71 -0
  26. package/dist/programmatic-schema/object.d.ts +25 -0
  27. package/dist/programmatic-schema/object.js +43 -0
  28. package/dist/programmatic-schema/string-union.d.ts +66 -0
  29. package/dist/programmatic-schema/string-union.js +61 -0
  30. package/dist/programmatic-schema/string.d.ts +81 -0
  31. package/dist/programmatic-schema/string.js +71 -0
  32. package/dist/types/base-detailed-schema-types.ts +135 -0
  33. package/dist/types/base-short-schema-types.ts +53 -0
  34. package/dist/types/compound-schema-types.ts +156 -0
  35. package/dist/utils/fp.d.ts +14 -0
  36. package/dist/utils/fp.js +19 -0
  37. package/dist/x-closure.d.ts +8 -0
  38. package/dist/x-closure.js +18 -0
  39. package/package.json +48 -0
package/.eslintrc.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "es6": true
5
+ },
6
+ "parser": "@typescript-eslint/parser",
7
+ "plugins": ["@typescript-eslint"],
8
+ "parserOptions": {
9
+ "sourceType": "module",
10
+ "ecmaVersion": 2017
11
+ },
12
+ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
13
+ "rules": {
14
+ "indent": "off",
15
+ "no-async-promise-executor": "off",
16
+ "quotes": "off",
17
+ "semi": "off",
18
+ "@typescript-eslint/no-var-requires": "off"
19
+ },
20
+ "ignorePatterns": ["/dist"]
21
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Konstantin Mazur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # SchematoX
2
+
3
+ SchmatoX is a lightweight library for creating JSON compatible schemas. The subject of the schema can be parsed or validated by the library in a type-safe manner. Instead of focusing on supporting all possible JS/TS data structures, we provide a set of constraints that help reduce the complexity of communication between different JS tools and runtimes.
4
+
5
+ ### Pros
6
+
7
+ - The statically defined JSON compatible schema is a killer feature:
8
+ - It has great potential for automation in the context of DB model creation, CRUD backends, documentation, outer interface requirements etc.
9
+ - One can store schema changes as static JSON and potentially use it for creating coherent DB Model migration logic.
10
+ - One can check if the defined schema is compatible with our constraints using the `as const satisfies Schema` TypeScript statement.
11
+ - Programmatically defined schemas are also supported, but as a means of creating statically defined schemas.
12
+ - A separate `x` closure wrapper exists for both schema definition and is used for validating/parsing the schema subject on demand. It is also compatible with other programmatically defined schemas.
13
+ - There is a clear separation of concerns for validating and parsing logic.
14
+ - The Schematox parser is used for dealing with an outer interface where the data type is not guaranteed.
15
+ - The Schematox validator is used for dealing with an internal interface where the data type is known and satisfies the schema requirements.
16
+ - Schematox uses an Ether-style error handling system. It never throws an error on an unsuccessful schema subject check. Instead, our parser/validator always returns an `EitherError` object with one of two keys: `error` or `data`. We believe that the parser/validator should not throw errors because dealing with an invalid schema subject is part of its internal logic. Therefore, invalid data cases should not be considered as exceptions or errors.
17
+ - We offer first-class support for branded base schema types. Currently, `schematoX` might be the only library that promotes this technique, which in our opinion, is highly underestimated.
18
+ - Our library is lightweight. We plan to have a max unpacked size of 150 kB. Currently our stats are:
19
+ - npm notice package size: 13.0 kB
20
+ - npm notice unpacked size: 93.4 kB
21
+ - We have zero dependencies, a plain implementation code style, and 100% test coverage. The runtime code logic is small and easy to grasp, consisting of just a couple of functions. Most of the library code is tests, currently over 450, which includes extensive generic type testing. We aim high and aspire to be completely bug-free.
22
+
23
+ Essentially, to define a schema, one doesn't even need to import any functions from our library, only the `Schema` type. This approach does come with a few limitations. The first is `stringUnion` and `numberUnion` schema default values are not constrained by the defined union choices, only the primitive type. We might fix this issue later, but for now, we prioritize this over the case when `default` extends union choice by its definition.
24
+
25
+ A second limitation is the depth of the compound schema data structure. Currently, we support 7 layers of depth. It's easy to increase this number, but because of the exponential nature of stored type variants in memory, we want to determine how much RAM each next layer will use before increasing it.
26
+
27
+ It's crucial to separate parsing/validation logic from the schema itself. Libraries like `superstract` or `zod` mix these two elements. While that's handy for starters, it reduces our ability to create independent parsers/validators. The same goes for building infrastructure based on top of these great alternatives. As mentioned, our schemas are just JSON compatible objects, which can be completely independent from our library.
28
+
29
+ ### Cons
30
+
31
+ - Currently we support only 7 layers of compound structure depth but most likely it will be higher soon
32
+ - We do not support records, discriminated unions, object unions, array unions, intersections, functions, NaN, Infinity and other not JSON compatible structures
33
+ - Null value is acceptable by the parser but will be treated as undefined and transformed to undefined
34
+ - We only plan to support custom parser/normalizer integration logic.
35
+
36
+ ## Installation
37
+
38
+ ```sh
39
+ npm install schematox
40
+ ```
41
+
42
+ ## Statically defined schema example
43
+
44
+ ```typescript
45
+ import type { Schema } from 'schematox'
46
+
47
+ export const userSchema = {
48
+ type: 'object',
49
+ of: {
50
+ id: {
51
+ type: 'string',
52
+ brand: ['idFor', 'User'],
53
+ },
54
+ email: {
55
+ type: 'string',
56
+ optional: true,
57
+ brand: ['format', 'email'],
58
+ },
59
+ updatedAt: {
60
+ type: 'number',
61
+ brand: ['format', 'msUnixTimestamp'],
62
+ },
63
+ canRead: {
64
+ type: 'array',
65
+ of: {
66
+ type: 'string',
67
+ brand: ['idFor', 'User'],
68
+ },
69
+ minLength: 1,
70
+ },
71
+ canWrite: {
72
+ type: 'array',
73
+ of: {
74
+ type: 'string',
75
+ brand: ['idFor', 'User'],
76
+ },
77
+ minLength: 1,
78
+ },
79
+ bio: 'string?',
80
+ },
81
+ } as const satisfies Schema
82
+ ```
83
+
84
+ ### Programmatically defined schema example
85
+
86
+ ```typescript
87
+ import { object, string, number, boolean, array } from 'schematox'
88
+
89
+ const userIdX = string().brand('idFor', 'User')
90
+ const emailX = string().brand('format', 'email')
91
+ const msUnixTimestampX = number().brand('format', 'msUnixTimestamp')
92
+
93
+ export const userSchema = object({
94
+ id: userIdX,
95
+ email: emailX,
96
+ updatedAt: updatedAtX,
97
+ canRead: array(userId),
98
+ canWrite: array(userId),
99
+ bio: string().optional(true),
100
+ })
101
+ ```
102
+
103
+ ### Parse/validate
104
+
105
+ ```typescript
106
+ import { parse, validate, x } from 'schematox'
107
+ import { userSchema, userId, emailX, msUnixTimestampX } './schema'
108
+
109
+ import type { XParse } from 'schematox'
110
+
111
+ type UserId = XParse<typeof userId>
112
+ type Email = XParse<typeof emailX>
113
+ type MsUnixTimestamp = XParse<typeof msUnixTimestamp>
114
+
115
+ const adminUserId = '1' as UserId
116
+
117
+ const schemaSubject = {
118
+ id: adminUserId,
119
+ email: 'john@dow.com' as Email,
120
+ updatedAt: Date.now() as MsUnixTimestamp,
121
+ canRead: [userId],
122
+ canWrite: [userId],
123
+ bio: undefined,
124
+ }
125
+
126
+ const parsed = parse(userSchema, schemaSubject)
127
+
128
+ // We need a type guard before accessing the prased.data
129
+ if (parsed.error) {
130
+ throw Error('Not expected')
131
+ }
132
+
133
+ console.log(parsed.data)
134
+
135
+ const validated = validate(userSchema, schemaSubject)
136
+
137
+ // We need a type guard before accessing the validated.data
138
+ if (parsed.error) {
139
+ throw Error('Not expected')
140
+ }
141
+
142
+ console.log(validated.data)
143
+
144
+ /* Another way of doing the same */
145
+
146
+ const userSchemaX = x(userSchema)
147
+
148
+ const parsedByX = userSchemaX.parse(schemaSubject)
149
+ const validatedByX = userSchemaX.validate(schemaSubject)
150
+ ```
151
+
152
+ ### Result type inference
153
+
154
+ ```typescript
155
+ type ResultType = {
156
+ id: string & { __idFor: 'User' }
157
+ email: string & { __format: 'email' }
158
+ updatedAt: number & { __format: 'msUnixTimestamp' }
159
+ canRead: Array<string & { __idFor: 'User' }>
160
+ canWrite: Array<string & { __idFor: 'User' }>
161
+ }
162
+ ```
163
+
164
+ ## Supported data types
165
+
166
+ ```typescript
167
+ import {
168
+ array,
169
+ object,
170
+ string,
171
+ number,
172
+ boolean,
173
+ buffer,
174
+ stringUnion,
175
+ numberUnion,
176
+ } from 'schematox'
177
+
178
+ import type { Schema } from 'schematox'
179
+
180
+ /* Base types */
181
+
182
+ // string
183
+
184
+ const staticShortStringRequired = 'string' satisfies Schema
185
+ const staticShortStringOptional = 'string?' satisfies Schema
186
+
187
+ const staticDetailedString = {
188
+ type: 'string',
189
+ optional: true,
190
+ default: 'x',
191
+ brand: ['x', 'y'],
192
+ minLength: 1,
193
+ maxLength: 1,
194
+ description: 'x',
195
+ } as const satisfies Schema
196
+
197
+ const programmaticString = string()
198
+ .optional()
199
+ .default('x')
200
+ .minLength(1)
201
+ .maxLength(1)
202
+ .brand('x', 'y')
203
+ .description('y')
204
+
205
+ // number
206
+
207
+ const staticShortNumberRequired = 'number' satisfies Schema
208
+ const staticShortNumberOptional = 'number?' satisfies Schema
209
+
210
+ const staticDetailedNumber = {
211
+ type: 'number',
212
+ optional: true,
213
+ default: 1,
214
+ brand: ['x', 'y'],
215
+ min: 1,
216
+ max: 1,
217
+ description: 'x',
218
+ } as const satisfies Schema
219
+
220
+ const programmaticNumber = number()
221
+ .optional()
222
+ .default(1)
223
+ .min(1)
224
+ .max(1)
225
+ .brand('x', 'y')
226
+ .description('y')
227
+
228
+ // boolean
229
+
230
+ const staticShortBooleanRequired = 'boolean' satisfies Schema
231
+ const staticShortBooleanOptional = 'boolean?' satisfies Schema
232
+
233
+ const staticDetailedBoolean = {
234
+ type: 'boolean',
235
+ optional: true,
236
+ default: false,
237
+ brand: ['x', 'y'],
238
+ description: 'x',
239
+ } as const satisfies Schema
240
+
241
+ const programmaticBoolean = boolean()
242
+ .optional()
243
+ .default(false)
244
+ .brand('x', 'y')
245
+ .description('y')
246
+
247
+ // buffer
248
+ // The default value is not supported because Buffer is not compatible with JSON
249
+
250
+ const staticShortBufferRequired = 'buffer' satisfies Schema
251
+ const staticShortBufferOptional = 'buffer?' satisfies Schema
252
+
253
+ const staticDetailedBuffer = {
254
+ type: 'buffer',
255
+ optional: true,
256
+ brand: ['x', 'y'],
257
+ minLength: 1,
258
+ maxLength: 1,
259
+ description: 'x',
260
+ } as const satisfies Schema
261
+
262
+ const programmaticBuffer = buffer()
263
+ .optional()
264
+ .minLength(1)
265
+ .maxLength(1)
266
+ .brand('x', 'y')
267
+ .description('y')
268
+
269
+ // stringUnion
270
+
271
+ const staticStringUnion = {
272
+ type: 'stringUnion',
273
+ of: ['x', 'y', 'z'],
274
+ optional: true,
275
+ brand: ['x', 'y'],
276
+ description: 'x',
277
+ } as const satisfies Schema
278
+
279
+ const programmaticStringUnion = stringUnion('x', 'y', 'z')
280
+ .optional()
281
+ .brand('x', 'y')
282
+ .description('y')
283
+
284
+ // stringUnion
285
+
286
+ const staticNumberUnion = {
287
+ type: 'numberUnion',
288
+ of: [0, 1, 2],
289
+ optional: true,
290
+ brand: ['x', 'y'],
291
+ description: 'x',
292
+ } as const satisfies Schema
293
+
294
+ const programmaticNumberUnion = numberUnion(0, 1, 2)
295
+ .optional()
296
+ .brand('x', 'y')
297
+ .description('y')
298
+
299
+ /* Compound schema */
300
+
301
+ // object
302
+
303
+ const staticObject = {
304
+ type: 'object',
305
+ of: {
306
+ x: 'string',
307
+ y: 'number?',
308
+ },
309
+ } as const satisfies Schema
310
+
311
+ const programmaticObject = object({
312
+ x: string(),
313
+ y: number().optional(),
314
+ })
315
+
316
+ // array
317
+
318
+ const staticArray = {
319
+ type: 'array',
320
+ of: 'string',
321
+ } as const satisfies Schema
322
+
323
+ const programmaticArray = array(string())
324
+ ```
325
+
326
+ ## Validation/parse error shape
327
+
328
+ ```typescript
329
+ import { x, object, array, string } from 'schematox'
330
+
331
+ const schemaX = x(
332
+ object({
333
+ x: object({
334
+ y: array(
335
+ object({
336
+ z: string(),
337
+ })
338
+ ),
339
+ }),
340
+ })
341
+ )
342
+
343
+ const result = schemaX.parse({ x: { y: [{ z: 0 }] } })
344
+
345
+ console.log(result.error)
346
+ ```
347
+
348
+ The console.log will output:
349
+
350
+ ```json
351
+ [
352
+ {
353
+ "code": "INVALID_TYPE",
354
+ "schema": { "type": "string" },
355
+ "subject": 0,
356
+ "path": ["x", "y", 0, "z"]
357
+ }
358
+ ]
359
+ ```
360
+
361
+ ## Parser details
362
+
363
+ The parser provides a new object/primitive without references to the evaluated subject, except in the case of `Buffer` for better performance. This functionality is responsible for clearing the `array` schema result value from `undefined` optional schema values.
364
+
365
+ Moreover, the parser manages the `null` value as `undefined` and subsequently replaces it with `undefined`. It also swaps `optional` values with the `default` value, provided that the default values are explicitly stated in the schema.
366
+
367
+ Particularly for this last function, the parser uses a separate type inference flow. This flow is accessible for the library user via the type generic `XParsed<T extends Schema>`. By leveraging this mechanism, the user can simplify the process and enhance efficiency.
368
+
369
+ ## Validator details
370
+
371
+ The validator operates by returning a reference to the original object, rather than applying any form of mutations to it. This is a significant feature because it disregards the schema’s default values. This particular trait of the validator ensures the object's integrity and independence from the schema’s default stipulations.
372
+
373
+ Furthermore, the validator incorporates a type inference flow distinct from the parser's. This separate flow is aimed at providing the library user with the flexibility and enhanced control needed for effective execution. Available for utilization through the type generic `XValidated<T extends Schema>`
@@ -0,0 +1,6 @@
1
+ /// <reference types="node" />
2
+ import type { EitherError } from './utils/fp';
3
+ import type { BaseSchema, Con_Schema_SubjT_P } from './types/compound-schema-types';
4
+ import type { BaseSchemaParseError } from './error';
5
+ export type BaseSchemaSubjectType = string | number | boolean | Buffer | undefined;
6
+ export declare function parseBaseSchemaSubject<T extends BaseSchema>(schema: T, schemaSubject: unknown): EitherError<BaseSchemaParseError, Con_Schema_SubjT_P<T>>;