adapt-schemas 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/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # adapt-schemas
2
+
3
+ A standalone JSON Schema library for the Adapt framework stack. Load schemas from plugin folders via glob patterns, validate JSON data, and extract defaults including `_globals` from course schemas.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install adapt-schemas
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import Schemas from 'adapt-schemas'
15
+
16
+ // Create and initialize the library
17
+ const library = new Schemas()
18
+ await library.init()
19
+
20
+ // Load schemas from plugin directories
21
+ await library.loadSchemas('**/schema/*.schema.json', {
22
+ cwd: './plugins',
23
+ ignore: ['**/node_modules/**']
24
+ })
25
+
26
+ // Validate data against a schema
27
+ const validatedData = await library.validate('course', {
28
+ title: 'My Course'
29
+ })
30
+
31
+ // Get _globals defaults from the course schema
32
+ const globals = await library.getGlobalsDefaults('course')
33
+ ```
34
+
35
+ ## API Reference
36
+
37
+ ### Schemas
38
+
39
+ #### Constructor Options
40
+
41
+ ```javascript
42
+ const library = new Schemas({
43
+ enableCache: true, // Enable schema build caching (default: true)
44
+ xssWhitelist: {}, // Custom XSS whitelist tags/attributes
45
+ xssWhitelistOverride: false, // Replace defaults instead of extending
46
+ formatOverrides: {}, // Custom string format RegExp patterns
47
+ directoryReplacements: { // Replacements for isDirectory keyword
48
+ '$ROOT': '/app',
49
+ '$DATA': '/app/data'
50
+ }
51
+ })
52
+ ```
53
+
54
+ #### Methods
55
+
56
+ ##### `init()`
57
+ Initializes the library and loads the base schema.
58
+
59
+ ```javascript
60
+ await library.init()
61
+ ```
62
+
63
+ ##### `loadSchemas(patterns, options)`
64
+ Loads schemas from directories matching glob patterns.
65
+
66
+ ```javascript
67
+ await library.loadSchemas('**/schema/*.schema.json', {
68
+ cwd: './plugins', // Base directory for patterns
69
+ ignore: ['**/excluded/**'] // Patterns to exclude
70
+ })
71
+
72
+ // Multiple patterns
73
+ await library.loadSchemas([
74
+ 'core/**/schema/*.schema.json',
75
+ 'plugins/**/schema/*.schema.json'
76
+ ], { ignore: ['**/node_modules/**'] })
77
+ ```
78
+
79
+ ##### `registerSchema(filePath, options)`
80
+ Registers a single schema file.
81
+
82
+ ```javascript
83
+ await library.registerSchema('/path/to/schema.json', {
84
+ replace: false // Replace existing schema with same name
85
+ })
86
+ ```
87
+
88
+ ##### `getSchema(schemaName, options)`
89
+ Retrieves and builds a schema by name.
90
+
91
+ ```javascript
92
+ const schema = await library.getSchema('course', {
93
+ useCache: true, // Use cached build if available
94
+ compile: true, // Compile the schema
95
+ applyExtensions: true // Apply $patch extensions
96
+ })
97
+ ```
98
+
99
+ ##### `getBuiltSchema(schemaName)`
100
+ Returns the built schema object.
101
+
102
+ ```javascript
103
+ const schemaObj = await library.getBuiltSchema('course')
104
+ console.log(schemaObj.properties)
105
+ ```
106
+
107
+ ##### `validate(schemaName, data, options)`
108
+ Validates data against a named schema.
109
+
110
+ ```javascript
111
+ const validated = await library.validate('course', inputData, {
112
+ useDefaults: true, // Apply schema defaults (default: true)
113
+ ignoreRequired: false // Ignore required field errors
114
+ })
115
+ ```
116
+
117
+ ##### `getSchemaDefaults(schemaName)`
118
+ Returns all defaults as a structured object.
119
+
120
+ ```javascript
121
+ const defaults = await library.getSchemaDefaults('course')
122
+ // { title: 'Untitled', _globals: { ... } }
123
+ ```
124
+
125
+ ##### `getGlobalsDefaults(schemaName)`
126
+ Extracts `_globals` defaults from a schema.
127
+
128
+ ```javascript
129
+ const globals = await library.getGlobalsDefaults('course')
130
+ // { _accessibility: { _isEnabled: true, ... }, _extensions: { ... } }
131
+ ```
132
+
133
+ ##### `getSchemaNames()`
134
+ Returns list of all registered schema names.
135
+
136
+ ```javascript
137
+ const names = library.getSchemaNames()
138
+ // ['base', 'course', 'content', 'component', ...]
139
+ ```
140
+
141
+ ##### `getSchemaInfo()`
142
+ Returns information about all registered schemas.
143
+
144
+ ```javascript
145
+ const info = library.getSchemaInfo()
146
+ // { course: { filePath: '...', extensions: [...], isPatch: false } }
147
+ ```
148
+
149
+ ##### `extendSchema(baseSchemaName, extSchemaName)`
150
+ Manually extends a schema with another.
151
+
152
+ ```javascript
153
+ library.extendSchema('course', 'my-course-extension')
154
+ ```
155
+
156
+ ##### `addKeyword(definition)`
157
+ Adds a custom AJV keyword.
158
+
159
+ ```javascript
160
+ library.addKeyword({
161
+ keyword: 'isPositive',
162
+ type: 'number',
163
+ validate: (schema, data) => data > 0
164
+ })
165
+ ```
166
+
167
+ ##### `addStringFormats(formats)`
168
+ Adds custom string format validators.
169
+
170
+ ```javascript
171
+ library.addStringFormats({
172
+ 'phone': /^\+?[\d\s-]+$/
173
+ })
174
+ ```
175
+
176
+ ### Events
177
+
178
+ The library extends EventEmitter and emits the following events:
179
+
180
+ ```javascript
181
+ library.on('initialized', () => { })
182
+ library.on('reset', () => { })
183
+ library.on('schemasLoaded', (schemaNames) => { })
184
+ library.on('schemaRegistered', (name, filePath) => { })
185
+ library.on('schemaDeregistered', (name) => { })
186
+ library.on('schemaExtended', (baseName, extName) => { })
187
+ library.on('warning', (message) => { })
188
+ ```
189
+
190
+ ## Schema Format
191
+
192
+ ### Basic Schema with Inheritance
193
+
194
+ Schemas use `$merge` to inherit from a parent schema:
195
+
196
+ ```json
197
+ {
198
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
199
+ "$anchor": "content",
200
+ "$merge": {
201
+ "source": { "$ref": "base" },
202
+ "with": {
203
+ "properties": {
204
+ "title": {
205
+ "type": "string",
206
+ "default": ""
207
+ },
208
+ "body": {
209
+ "type": "string",
210
+ "default": ""
211
+ }
212
+ },
213
+ "required": ["title"]
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ ### Patch Schema (Extensions)
220
+
221
+ Use `$patch` to extend an existing schema without creating a new one:
222
+
223
+ ```json
224
+ {
225
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
226
+ "$anchor": "course-trickle-extension",
227
+ "$patch": {
228
+ "source": { "$ref": "course" },
229
+ "with": {
230
+ "properties": {
231
+ "_globals": {
232
+ "type": "object",
233
+ "properties": {
234
+ "_trickle": {
235
+ "type": "object",
236
+ "properties": {
237
+ "incompleteContent": {
238
+ "type": "string",
239
+ "default": "There is incomplete content above"
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ ```
250
+
251
+ ## Custom Keywords
252
+
253
+ The library includes these custom AJV keywords:
254
+
255
+ | Keyword | Description | Example |
256
+ |---------|-------------|---------|
257
+ | `isBytes` | Parses byte strings | `"1MB"` → `1048576` |
258
+ | `isDate` | Parses date strings | `"2024-01-01"` → `Date` |
259
+ | `isTimeMs` | Parses duration strings | `"7d"` → `604800000` |
260
+ | `isDirectory` | Resolves path tokens | `"$ROOT/data"` → `"/app/data"` |
261
+ | `isObjectId` | Marks ObjectId fields | No transformation |
262
+
263
+ ## Error Handling
264
+
265
+ The library throws `SchemaError` with the following codes:
266
+
267
+ | Code | Description |
268
+ |------|-------------|
269
+ | `INVALID_PARAMS` | Invalid method parameters |
270
+ | `SCHEMA_EXISTS` | Schema with same name already registered |
271
+ | `SCHEMA_LOAD_FAILED` | Failed to read/parse schema file |
272
+ | `INVALID_SCHEMA` | Schema fails JSON Schema validation |
273
+ | `MISSING_SCHEMA` | Requested schema not found |
274
+ | `VALIDATION_FAILED` | Data fails schema validation |
275
+ | `MODIFY_PROTECTED_ATTR` | Attempt to modify internal/read-only field |
276
+
277
+ ```javascript
278
+ import { SchemaError } from 'adapt-schemas'
279
+
280
+ try {
281
+ await library.validate('course', data)
282
+ } catch (e) {
283
+ if (e instanceof SchemaError) {
284
+ console.log(e.code) // 'VALIDATION_FAILED'
285
+ console.log(e.data) // { schemaName, errors, data }
286
+ }
287
+ }
288
+ ```
289
+
290
+ ## License
291
+
292
+ MIT
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import Schemas, { SchemaError } from './lib/Schemas.js'
2
+ import Schema from './lib/Schema.js'
3
+ import Keywords from './lib/Keywords.js'
4
+ import XSSDefaults from './lib/XSSDefaults.js'
5
+
6
+ export { Schemas, Schema, SchemaError, Keywords, XSSDefaults }
7
+ export default Schemas
@@ -0,0 +1,98 @@
1
+ import bytes from 'bytes'
2
+ import ms from 'ms'
3
+ import path from 'path'
4
+
5
+ /**
6
+ * Custom JSON schema keywords for AJV
7
+ */
8
+ class Keywords {
9
+ /**
10
+ * Returns all custom keywords
11
+ * @param {Object} directoryReplacements Replacements for isDirectory (e.g. { '$ROOT': '/app' })
12
+ * @returns {Object[]} Array of AJV keyword definitions
13
+ */
14
+ static all(directoryReplacements = {}) {
15
+ const keywords = {
16
+ /**
17
+ * Parses byte string values (e.g., "1MB" -> 1048576)
18
+ */
19
+ isBytes: function () {
20
+ return (value, { parentData, parentDataProperty }) => {
21
+ try {
22
+ parentData[parentDataProperty] = bytes.parse(value)
23
+ return true
24
+ } catch (e) {
25
+ return false
26
+ }
27
+ }
28
+ },
29
+
30
+ /**
31
+ * Parses date string values into Date objects
32
+ */
33
+ isDate: function () {
34
+ return (value, { parentData, parentDataProperty }) => {
35
+ try {
36
+ parentData[parentDataProperty] = new Date(value)
37
+ return true
38
+ } catch (e) {
39
+ return false
40
+ }
41
+ }
42
+ },
43
+
44
+ /**
45
+ * Resolves directory path tokens ($ROOT, $DATA, $TEMP, etc.)
46
+ */
47
+ isDirectory: function () {
48
+ const doReplace = value => {
49
+ const replacements = Object.entries(directoryReplacements)
50
+ return replacements.reduce((m, [k, v]) => {
51
+ return m.startsWith(k) ? path.resolve(v, m.replace(k, '').slice(1)) : m
52
+ }, value)
53
+ }
54
+ return (value, { parentData, parentDataProperty }) => {
55
+ try {
56
+ parentData[parentDataProperty] = doReplace(value)
57
+ } catch (e) {
58
+ // Keep original value on error
59
+ }
60
+ return true
61
+ }
62
+ },
63
+
64
+ /**
65
+ * Parses time duration strings into milliseconds (e.g., "7d" -> 604800000)
66
+ */
67
+ isTimeMs: function () {
68
+ return (value, { parentData, parentDataProperty }) => {
69
+ try {
70
+ parentData[parentDataProperty] = ms(value)
71
+ return true
72
+ } catch (e) {
73
+ return false
74
+ }
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Marker for ObjectId fields (no transformation, just marks the field)
80
+ */
81
+ isObjectId: function () {
82
+ return () => true
83
+ }
84
+ }
85
+
86
+ return Object.entries(keywords).map(([keyword, compile]) => {
87
+ return {
88
+ keyword,
89
+ type: 'string',
90
+ modifying: true,
91
+ schemaType: 'boolean',
92
+ compile
93
+ }
94
+ })
95
+ }
96
+ }
97
+
98
+ export default Keywords