@rip-lang/schema 0.2.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.
- package/README.md +1042 -0
- package/SCHEMA.md +1113 -0
- package/emit-sql.js +460 -0
- package/emit-types.js +366 -0
- package/generate.js +144 -0
- package/grammar.rip +504 -0
- package/index.js +39 -0
- package/lexer.js +438 -0
- package/orm.js +916 -0
- package/package.json +62 -0
- package/parser.js +246 -0
- package/runtime.js +494 -0
package/runtime.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// Schema Runtime - Lightweight validation and model factory
|
|
3
|
+
//
|
|
4
|
+
// Design principles:
|
|
5
|
+
// - Plain objects (no Proxy overhead)
|
|
6
|
+
// - Compiled validators (fast execution)
|
|
7
|
+
// - Validate on demand (not on every set)
|
|
8
|
+
// - Zero dependencies
|
|
9
|
+
//
|
|
10
|
+
// Author: Steve Shreeve <steve.shreeve@gmail.com>
|
|
11
|
+
// Date: January 2026
|
|
12
|
+
// ==============================================================================
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'fs'
|
|
15
|
+
import { parse as _rawParse } from './parser.js'
|
|
16
|
+
import { formatParseError } from './errors.js'
|
|
17
|
+
import { generateSQL } from './emit-sql.js'
|
|
18
|
+
import { generateTypes } from './emit-types.js'
|
|
19
|
+
import { generateZod } from './emit-zod.js'
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Built-in Type Validators
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
const Types = {
|
|
26
|
+
// Primitives
|
|
27
|
+
string: (v) => typeof v === 'string',
|
|
28
|
+
number: (v) => typeof v === 'number' && !Number.isNaN(v),
|
|
29
|
+
integer: (v) => Number.isInteger(v),
|
|
30
|
+
boolean: (v) => typeof v === 'boolean',
|
|
31
|
+
date: (v) => v instanceof Date && !Number.isNaN(v.getTime()),
|
|
32
|
+
datetime: (v) => v instanceof Date && !Number.isNaN(v.getTime()),
|
|
33
|
+
|
|
34
|
+
// String formats
|
|
35
|
+
email: (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
36
|
+
url: (v) => typeof v === 'string' && /^https?:\/\/.+/.test(v),
|
|
37
|
+
uuid: (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
|
|
38
|
+
phone: (v) => typeof v === 'string' && /^[\d\s\-+()]+$/.test(v),
|
|
39
|
+
|
|
40
|
+
// Special
|
|
41
|
+
text: (v) => typeof v === 'string',
|
|
42
|
+
json: (v) => v !== undefined,
|
|
43
|
+
any: () => true,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Constraint validators
|
|
47
|
+
const Constraints = {
|
|
48
|
+
min: (v, min, type) => {
|
|
49
|
+
if (type === 'string' || type === 'text') return v.length >= min
|
|
50
|
+
if (type === 'number' || type === 'integer') return v >= min
|
|
51
|
+
if (Array.isArray(v)) return v.length >= min
|
|
52
|
+
return true
|
|
53
|
+
},
|
|
54
|
+
max: (v, max, type) => {
|
|
55
|
+
if (type === 'string' || type === 'text') return v.length <= max
|
|
56
|
+
if (type === 'number' || type === 'integer') return v <= max
|
|
57
|
+
if (Array.isArray(v)) return v.length <= max
|
|
58
|
+
return true
|
|
59
|
+
},
|
|
60
|
+
pattern: (v, pattern) => {
|
|
61
|
+
if (typeof v !== 'string') return false
|
|
62
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern
|
|
63
|
+
return regex.test(v)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Schema Registry
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
export class Schema {
|
|
72
|
+
constructor(source) {
|
|
73
|
+
this.types = new Map() // @type definitions
|
|
74
|
+
this.models = new Map() // @model definitions
|
|
75
|
+
this.enums = new Map() // @enum definitions
|
|
76
|
+
this.validators = new Map() // Compiled validators per model
|
|
77
|
+
this._ast = null // Raw AST (retained for code generation)
|
|
78
|
+
|
|
79
|
+
if (source) this.register(this.constructor._parse(source))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static _parse(source, filename) {
|
|
83
|
+
try {
|
|
84
|
+
return _rawParse(source)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw formatParseError(err, source, filename)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Factory
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
static load(path, base) {
|
|
95
|
+
const resolved = base ? new URL(path, base) : path
|
|
96
|
+
const source = readFileSync(resolved, 'utf-8')
|
|
97
|
+
const filename = typeof resolved === 'string' ? resolved : resolved.pathname
|
|
98
|
+
const schema = new Schema()
|
|
99
|
+
schema.register(Schema._parse(source, filename))
|
|
100
|
+
return schema
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Registration
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Register schema definitions from parsed AST
|
|
109
|
+
*/
|
|
110
|
+
register(ast) {
|
|
111
|
+
if (!Array.isArray(ast) || ast[0] !== 'schema') {
|
|
112
|
+
throw new Error('Invalid schema AST')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._ast = ast
|
|
116
|
+
|
|
117
|
+
for (let i = 1; i < ast.length; i++) {
|
|
118
|
+
const def = ast[i]
|
|
119
|
+
if (!Array.isArray(def)) continue
|
|
120
|
+
|
|
121
|
+
switch (def[0]) {
|
|
122
|
+
case 'enum':
|
|
123
|
+
this._registerEnum(def)
|
|
124
|
+
break
|
|
125
|
+
case 'type':
|
|
126
|
+
this._registerType(def)
|
|
127
|
+
break
|
|
128
|
+
case 'model':
|
|
129
|
+
this._registerModel(def)
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Code generation convenience methods
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
toSQL() {
|
|
140
|
+
return generateSQL(this._ast)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toTypes() {
|
|
144
|
+
return generateTypes(this._ast)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
toZod() {
|
|
148
|
+
return generateZod(this._ast)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_registerEnum(def) {
|
|
152
|
+
// ["enum", name, values]
|
|
153
|
+
// Simple: ["admin", "user", "guest"]
|
|
154
|
+
// Valued: [["pending", 0], ["active", 1]]
|
|
155
|
+
const [, name, values] = def
|
|
156
|
+
const members = Array.isArray(values[0])
|
|
157
|
+
? values.map(v => v[0]) // Extract member names from valued enums
|
|
158
|
+
: values
|
|
159
|
+
this.enums.set(name, new Set(members))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_registerType(def) {
|
|
163
|
+
// ["type", name, parent, body]
|
|
164
|
+
const [, name, parent, body] = def
|
|
165
|
+
const fields = this._parseFields(body)
|
|
166
|
+
this.types.set(name, { name, parent, fields, directives: {} })
|
|
167
|
+
|
|
168
|
+
// Pre-compile validator for this type
|
|
169
|
+
this._compileValidator(name, 'type')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_registerModel(def) {
|
|
173
|
+
// ["model", name, parent, body]
|
|
174
|
+
const [, name, parent, body] = def
|
|
175
|
+
const fields = this._parseFields(body)
|
|
176
|
+
const directives = this._parseDirectives(body)
|
|
177
|
+
this.models.set(name, { name, parent, fields, directives })
|
|
178
|
+
|
|
179
|
+
// Pre-compile validator for this model
|
|
180
|
+
this._compileValidator(name)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_parseFields(body) {
|
|
184
|
+
const fields = new Map()
|
|
185
|
+
if (!Array.isArray(body)) return fields
|
|
186
|
+
|
|
187
|
+
for (const item of body) {
|
|
188
|
+
if (!Array.isArray(item)) continue
|
|
189
|
+
if (item[0] === 'field') {
|
|
190
|
+
// ["field", name, modifiers, type, constraints, attrs]
|
|
191
|
+
const [, name, modifiers, type, constraints, attrs] = item
|
|
192
|
+
fields.set(name, {
|
|
193
|
+
name,
|
|
194
|
+
required: modifiers?.includes('!') ?? false,
|
|
195
|
+
unique: modifiers?.includes('#') ?? false,
|
|
196
|
+
optional: modifiers?.includes('?') ?? false,
|
|
197
|
+
type: Array.isArray(type) && type[0] === 'array' ? { array: true, of: type[1] } : type,
|
|
198
|
+
constraints: this._parseConstraints(constraints, type),
|
|
199
|
+
attrs,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return fields
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_parseConstraints(constraints, type) {
|
|
207
|
+
if (!Array.isArray(constraints) || constraints.length === 0) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Constraints are: [min], [min, max], [default], [min, max, default]
|
|
212
|
+
const result = {}
|
|
213
|
+
|
|
214
|
+
if (constraints.length === 1) {
|
|
215
|
+
// Single value = default
|
|
216
|
+
result.default = constraints[0]
|
|
217
|
+
} else if (constraints.length === 2) {
|
|
218
|
+
// Two values = min, max
|
|
219
|
+
result.min = constraints[0]
|
|
220
|
+
result.max = constraints[1]
|
|
221
|
+
} else if (constraints.length >= 3) {
|
|
222
|
+
// Three values = min, max, default
|
|
223
|
+
result.min = constraints[0]
|
|
224
|
+
result.max = constraints[1]
|
|
225
|
+
result.default = constraints[2]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_parseDirectives(body) {
|
|
232
|
+
const directives = {}
|
|
233
|
+
if (!Array.isArray(body)) return directives
|
|
234
|
+
|
|
235
|
+
for (const item of body) {
|
|
236
|
+
if (!Array.isArray(item)) continue
|
|
237
|
+
switch (item[0]) {
|
|
238
|
+
case 'timestamps':
|
|
239
|
+
directives.timestamps = true
|
|
240
|
+
break
|
|
241
|
+
case 'softDelete':
|
|
242
|
+
directives.softDelete = true
|
|
243
|
+
break
|
|
244
|
+
case 'index':
|
|
245
|
+
directives.indexes = directives.indexes || []
|
|
246
|
+
directives.indexes.push({ fields: item[1], unique: item[2] })
|
|
247
|
+
break
|
|
248
|
+
case 'belongs_to':
|
|
249
|
+
directives.belongsTo = directives.belongsTo || []
|
|
250
|
+
directives.belongsTo.push({ model: item[1], options: item[2] })
|
|
251
|
+
break
|
|
252
|
+
case 'has_many':
|
|
253
|
+
directives.hasMany = directives.hasMany || []
|
|
254
|
+
directives.hasMany.push({ model: item[1], options: item[2] })
|
|
255
|
+
break
|
|
256
|
+
case 'has_one':
|
|
257
|
+
directives.hasOne = directives.hasOne || []
|
|
258
|
+
directives.hasOne.push({ model: item[1], options: item[2] })
|
|
259
|
+
break
|
|
260
|
+
case 'link':
|
|
261
|
+
directives.links = directives.links || []
|
|
262
|
+
directives.links.push({ role: item[1], model: item[2], options: item[3] })
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return directives
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Validator Compilation
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
_compileValidator(modelName, kind = 'model') {
|
|
274
|
+
const model = kind === 'type' ? this.types.get(modelName) : this.models.get(modelName)
|
|
275
|
+
if (!model) return
|
|
276
|
+
|
|
277
|
+
const checks = []
|
|
278
|
+
|
|
279
|
+
for (const [fieldName, field] of model.fields) {
|
|
280
|
+
checks.push(this._compileFieldValidator(fieldName, field))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Return a function that runs all checks and collects errors
|
|
284
|
+
const validator = (obj) => {
|
|
285
|
+
const errors = []
|
|
286
|
+
for (const check of checks) {
|
|
287
|
+
const error = check(obj)
|
|
288
|
+
if (error) errors.push(error)
|
|
289
|
+
}
|
|
290
|
+
return errors.length > 0 ? errors : null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.validators.set(modelName, validator)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_compileFieldValidator(fieldName, field) {
|
|
297
|
+
const { required, type, constraints } = field
|
|
298
|
+
const isArray = typeof type === 'object' && type.array
|
|
299
|
+
const baseType = isArray ? type.of : type
|
|
300
|
+
|
|
301
|
+
return (obj) => {
|
|
302
|
+
const value = obj[fieldName]
|
|
303
|
+
|
|
304
|
+
// Check required
|
|
305
|
+
if (value === undefined || value === null) {
|
|
306
|
+
if (required) {
|
|
307
|
+
return { field: fieldName, error: 'required', message: `${fieldName} is required` }
|
|
308
|
+
}
|
|
309
|
+
return null // Optional and missing is OK
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check array type
|
|
313
|
+
if (isArray) {
|
|
314
|
+
if (!Array.isArray(value)) {
|
|
315
|
+
return { field: fieldName, error: 'type', message: `${fieldName} must be an array` }
|
|
316
|
+
}
|
|
317
|
+
// Validate each item
|
|
318
|
+
for (let i = 0; i < value.length; i++) {
|
|
319
|
+
const itemError = this._validateValue(value[i], baseType, fieldName, constraints)
|
|
320
|
+
if (itemError) {
|
|
321
|
+
return { ...itemError, index: i }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check single value
|
|
328
|
+
return this._validateValue(value, baseType, fieldName, constraints)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_validateValue(value, type, fieldName, constraints) {
|
|
333
|
+
// Check built-in type
|
|
334
|
+
const typeValidator = Types[type]
|
|
335
|
+
if (typeValidator) {
|
|
336
|
+
if (!typeValidator(value)) {
|
|
337
|
+
return { field: fieldName, error: 'type', message: `${fieldName} must be a valid ${type}` }
|
|
338
|
+
}
|
|
339
|
+
} else if (this.enums.has(type)) {
|
|
340
|
+
// Check enum
|
|
341
|
+
const enumValues = this.enums.get(type)
|
|
342
|
+
if (!enumValues.has(value)) {
|
|
343
|
+
return { field: fieldName, error: 'enum', message: `${fieldName} must be one of: ${[...enumValues].join(', ')}` }
|
|
344
|
+
}
|
|
345
|
+
} else if (this.types.has(type)) {
|
|
346
|
+
// Nested type - validate recursively
|
|
347
|
+
const nestedErrors = this.validate(type, value)
|
|
348
|
+
if (nestedErrors) {
|
|
349
|
+
return { field: fieldName, error: 'nested', errors: nestedErrors }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Unknown type - allow (might be a forward reference or external type)
|
|
353
|
+
|
|
354
|
+
// Check constraints
|
|
355
|
+
if (constraints) {
|
|
356
|
+
if (constraints.min !== undefined && !Constraints.min(value, constraints.min, type)) {
|
|
357
|
+
return { field: fieldName, error: 'min', message: `${fieldName} must be at least ${constraints.min}` }
|
|
358
|
+
}
|
|
359
|
+
if (constraints.max !== undefined && !Constraints.max(value, constraints.max, type)) {
|
|
360
|
+
return { field: fieldName, error: 'max', message: `${fieldName} must be at most ${constraints.max}` }
|
|
361
|
+
}
|
|
362
|
+
if (constraints.pattern !== undefined && !Constraints.pattern(value, constraints.pattern)) {
|
|
363
|
+
return { field: fieldName, error: 'pattern', message: `${fieldName} has invalid format` }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Public API
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Create a new model instance with defaults applied
|
|
376
|
+
*/
|
|
377
|
+
create(modelName, data = {}) {
|
|
378
|
+
const model = this.models.get(modelName) || this.types.get(modelName)
|
|
379
|
+
if (!model) {
|
|
380
|
+
throw new Error(`Unknown model: ${modelName}`)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const instance = { ...data }
|
|
384
|
+
|
|
385
|
+
// Apply defaults
|
|
386
|
+
for (const [fieldName, field] of model.fields) {
|
|
387
|
+
if (instance[fieldName] === undefined && field.constraints?.default !== undefined) {
|
|
388
|
+
const def = field.constraints.default
|
|
389
|
+
instance[fieldName] = (typeof def === 'object' && def !== null) ? structuredClone(def) : def
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Add timestamps if configured
|
|
394
|
+
if (model.directives?.timestamps) {
|
|
395
|
+
const now = new Date()
|
|
396
|
+
instance.createdAt = instance.createdAt || now
|
|
397
|
+
instance.updatedAt = instance.updatedAt || now
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Attach metadata
|
|
401
|
+
Object.defineProperty(instance, '$model', { value: modelName, enumerable: false })
|
|
402
|
+
Object.defineProperty(instance, '$schema', { value: this, enumerable: false })
|
|
403
|
+
Object.defineProperty(instance, '$validate', {
|
|
404
|
+
value: () => this.validate(modelName, instance),
|
|
405
|
+
enumerable: false,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
return instance
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Validate an object against a model or type schema
|
|
413
|
+
* Returns null if valid, array of errors if invalid
|
|
414
|
+
*/
|
|
415
|
+
validate(modelName, obj) {
|
|
416
|
+
// Check for pre-compiled validator
|
|
417
|
+
let validator = this.validators.get(modelName)
|
|
418
|
+
|
|
419
|
+
// If not found, try to compile on-demand
|
|
420
|
+
if (!validator) {
|
|
421
|
+
if (this.types.has(modelName)) {
|
|
422
|
+
this._compileValidator(modelName, 'type')
|
|
423
|
+
} else if (this.models.has(modelName)) {
|
|
424
|
+
this._compileValidator(modelName, 'model')
|
|
425
|
+
}
|
|
426
|
+
validator = this.validators.get(modelName)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (validator) {
|
|
430
|
+
return validator(obj)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return null // Unknown model/type - no validation
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Check if a single value is valid for a field
|
|
438
|
+
*/
|
|
439
|
+
isValid(modelName, fieldName, value) {
|
|
440
|
+
const model = this.models.get(modelName) || this.types.get(modelName)
|
|
441
|
+
if (!model) return true
|
|
442
|
+
|
|
443
|
+
const field = model.fields.get(fieldName)
|
|
444
|
+
if (!field) return true
|
|
445
|
+
|
|
446
|
+
const tempObj = { [fieldName]: value }
|
|
447
|
+
const errors = this.validate(modelName, tempObj)
|
|
448
|
+
return !errors || !errors.some(e => e.field === fieldName)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get model definition
|
|
453
|
+
*/
|
|
454
|
+
getModel(name) {
|
|
455
|
+
return this.models.get(name)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get type definition
|
|
460
|
+
*/
|
|
461
|
+
getType(name) {
|
|
462
|
+
return this.types.get(name)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get enum values
|
|
467
|
+
*/
|
|
468
|
+
getEnum(name) {
|
|
469
|
+
return this.enums.get(name)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* List all registered models
|
|
474
|
+
*/
|
|
475
|
+
listModels() {
|
|
476
|
+
return [...this.models.keys()]
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* List all registered types
|
|
481
|
+
*/
|
|
482
|
+
listTypes() {
|
|
483
|
+
return [...this.types.keys()]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* List all registered enums
|
|
488
|
+
*/
|
|
489
|
+
listEnums() {
|
|
490
|
+
return [...this.enums.keys()]
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export default Schema
|