@01-edu/shared 1.0.2 → 1.0.4
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/attrs-defs.js +3604 -0
- package/attrs.js +291 -0
- package/definitions-checker.js +236 -0
- package/languages.js +147 -0
- package/package.json +5 -3
package/attrs.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { attrs, relationAttrs } from './attrs-defs.js'
|
|
2
|
+
import { languages } from './languages.js'
|
|
3
|
+
import { mapEntries, mapValues } from './toolbox.js'
|
|
4
|
+
|
|
5
|
+
// ⚡ description placeholder
|
|
6
|
+
|
|
7
|
+
const typeCheckers = {
|
|
8
|
+
boolean: b => typeof b === 'boolean',
|
|
9
|
+
number: n => typeof n === 'number',
|
|
10
|
+
string: s => typeof s === 'string',
|
|
11
|
+
object: o => typeof o === 'object' && o !== null,
|
|
12
|
+
array: Array.isArray,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const typeChecker = (defs, value, object, key) => {
|
|
16
|
+
const { type, check, options } = defs
|
|
17
|
+
|
|
18
|
+
if (value == null) {
|
|
19
|
+
if (!defs.required) return true
|
|
20
|
+
// if no value for required attribute, reject
|
|
21
|
+
throw Error(`missing value for required attribute ${key}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!type) throw Error(`attribute type definition is missing for ${key}`)
|
|
25
|
+
|
|
26
|
+
if (value?.type === 'function') {
|
|
27
|
+
if (defs.functionsByName[value.name]) return true
|
|
28
|
+
throw Error(`function associated not allowed for ${key}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// if a check is defined and don't pass, reject
|
|
32
|
+
check?.(value, object, key)
|
|
33
|
+
|
|
34
|
+
if (options) {
|
|
35
|
+
const opts = typeof options === 'function' ? options(object) : options
|
|
36
|
+
const isAnOption = opts.includes(value)
|
|
37
|
+
if (!isAnOption) {
|
|
38
|
+
throw Error(
|
|
39
|
+
`invalid option for ${key}: should be included in ${opts.join(', ')}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// basic case
|
|
45
|
+
if (typeCheckers[type]) {
|
|
46
|
+
if (!typeCheckers[type](value)) throw Error(`Expect ${type} for ${key}`)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// complex cases: array
|
|
51
|
+
if (Array.isArray(type)) {
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
throw Error(`invalid attribute value: expects an array for ${key}`)
|
|
54
|
+
}
|
|
55
|
+
// every value have to match one of the type definition
|
|
56
|
+
|
|
57
|
+
const uniqueDef = type.length === 1 && type[0]
|
|
58
|
+
const types =
|
|
59
|
+
!uniqueDef &&
|
|
60
|
+
Object.fromEntries(type.map(t => [t.type?.type?.value || t.type, t]))
|
|
61
|
+
for (const [index, v] of value.entries()) {
|
|
62
|
+
const err = Error('checks failed for all types')
|
|
63
|
+
err.index = index
|
|
64
|
+
err.key = key
|
|
65
|
+
err.label = defs.label
|
|
66
|
+
const subdefs =
|
|
67
|
+
uniqueDef || (typeof v === 'object' ? types[v.type] : types[typeof v])
|
|
68
|
+
if (!subdefs) {
|
|
69
|
+
err.details = {
|
|
70
|
+
label: 'Unknown structure',
|
|
71
|
+
err: Error('no type matches the value'),
|
|
72
|
+
}
|
|
73
|
+
throw err
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
typeChecker(subdefs, v, object, key)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
err.details = error.details || {
|
|
79
|
+
label: subdefs.label || error.label,
|
|
80
|
+
err: error,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO: check "one of" also one day - meaning there is no duplicate
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof type !== 'object') throw Error('invalid attribute type definition')
|
|
93
|
+
|
|
94
|
+
if (typeof value !== 'object' || value === null) {
|
|
95
|
+
throw Error('invalid attribute value: expects an object')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// then is necessarily an object. Let's check!
|
|
99
|
+
const typeEntries = Object.entries(type)
|
|
100
|
+
|
|
101
|
+
// check that every key fulfilled is defined in the type definitions
|
|
102
|
+
const invalidKey = Object.keys(value).find(key => !type[key])
|
|
103
|
+
if (invalidKey) throw Error(`${invalidKey} is invalid.`)
|
|
104
|
+
|
|
105
|
+
for (const [key, defs] of typeEntries) {
|
|
106
|
+
typeChecker(defs, value[key], object, key)
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// same as attrs with the check & function by name generated and descriptions form markdown files
|
|
112
|
+
export const attributes = mapEntries(attrs, ([attrKey, matches]) => [
|
|
113
|
+
attrKey,
|
|
114
|
+
mapValues(matches, (defs, type) => ({
|
|
115
|
+
...defs,
|
|
116
|
+
check: (value, object) => typeChecker(defs, value, object, attrKey),
|
|
117
|
+
})),
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
export const relationAttributes = mapEntries(
|
|
121
|
+
relationAttrs,
|
|
122
|
+
([attrKey, byParent]) => [
|
|
123
|
+
attrKey,
|
|
124
|
+
mapValues(byParent, (matches, parentType) =>
|
|
125
|
+
mapValues(matches, (defs, childType) => ({
|
|
126
|
+
...defs,
|
|
127
|
+
check: (value, object) => typeChecker(defs, value, object, attrKey),
|
|
128
|
+
})),
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// map from attrs[name][type] to attrs[type][name]
|
|
134
|
+
export const attrsByType = {}
|
|
135
|
+
|
|
136
|
+
const languagesEntries = Object.entries(languages)
|
|
137
|
+
for (const [name, matches] of Object.entries(attributes)) {
|
|
138
|
+
for (const [type, defs] of Object.entries(matches)) {
|
|
139
|
+
const ofType = attrsByType[type] || (attrsByType[type] = {})
|
|
140
|
+
ofType[name] = defs
|
|
141
|
+
// handle translations: generate translation attrs and required status
|
|
142
|
+
const { label, ...restDefs } = defs
|
|
143
|
+
if (defs.functionsByName?.translate) {
|
|
144
|
+
// perf measures done: inscrease from 0.671ms loadtime to 13.261ms
|
|
145
|
+
// should not impact the perfs
|
|
146
|
+
for (const [code, language] of languagesEntries) {
|
|
147
|
+
const newLabel = `${label} - ${language}`
|
|
148
|
+
ofType[`${name}-${code}`] = {
|
|
149
|
+
...restDefs,
|
|
150
|
+
label: newLabel,
|
|
151
|
+
lgCode: code,
|
|
152
|
+
required: false,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// map from attrs[name][parentType][childType]
|
|
160
|
+
// to attrs[parentType][childType][name]
|
|
161
|
+
export const relationAttrsByParentType = {}
|
|
162
|
+
for (const [name, byParent] of Object.entries(relationAttributes)) {
|
|
163
|
+
for (const [parentType, byChild] of Object.entries(byParent)) {
|
|
164
|
+
for (const [childType, defs] of Object.entries(byChild)) {
|
|
165
|
+
const ofParentType =
|
|
166
|
+
relationAttrsByParentType[parentType] ||
|
|
167
|
+
(relationAttrsByParentType[parentType] = {})
|
|
168
|
+
const ofParentChildType =
|
|
169
|
+
ofParentType[childType] || (ofParentType[childType] = {})
|
|
170
|
+
ofParentChildType[name] = defs
|
|
171
|
+
// for now, there is no relation attr that is translatable
|
|
172
|
+
// if it is created one day, add handling of translations here
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const attrsEntriesByType = mapValues(attrsByType, Object.entries)
|
|
177
|
+
const relAttrsEntriesByParentType = mapValues(
|
|
178
|
+
relationAttrsByParentType,
|
|
179
|
+
byParent => mapValues(byParent, Object.entries),
|
|
180
|
+
)
|
|
181
|
+
export const getDefaultAttrsEntries = object =>
|
|
182
|
+
attrsEntriesByType[object.type] || []
|
|
183
|
+
export const getDefaultRelAttrsEntries = (parentType, childType) =>
|
|
184
|
+
relAttrsEntriesByParentType[parentType]?.[childType] || []
|
|
185
|
+
|
|
186
|
+
export const getDefaultAttrs = object => attrsByType[object.type]
|
|
187
|
+
export const getDefaultRelAttrs = (parentType, childType) =>
|
|
188
|
+
relationAttrsByParentType[parentType]?.[childType]
|
|
189
|
+
|
|
190
|
+
const findDefaultOrNamedFunction = (value, defs) => {
|
|
191
|
+
// Should be true only for required impure default value functions
|
|
192
|
+
if (value === undefined || value === null) return defs.required && defs.value
|
|
193
|
+
if (value.type !== 'function') return
|
|
194
|
+
// Otherwise we only care about when the default function was overridden
|
|
195
|
+
// by another impure function
|
|
196
|
+
|
|
197
|
+
return defs.functionsByName?.[value.name]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const applyDefs = (key, value, defs, object, getUser) => {
|
|
201
|
+
// xp, child.attrs.rewards, defs.type.xp, object
|
|
202
|
+
// reward, child.attrs, defs, object
|
|
203
|
+
if (value === undefined || value === null) return
|
|
204
|
+
const fn = findDefaultOrNamedFunction(value[key], defs)
|
|
205
|
+
if (typeof fn === 'function') {
|
|
206
|
+
if (getUser) {
|
|
207
|
+
Object.defineProperty(value, key, {
|
|
208
|
+
get: () => fn(object, getUser(), key),
|
|
209
|
+
enumerable: true,
|
|
210
|
+
})
|
|
211
|
+
} else if (fn.name === 'translate') {
|
|
212
|
+
// if we don't have a user, assume english language
|
|
213
|
+
value[key] = value[`${key}-en`]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const expandAttr = (key, value, defs, object, getUser) => {
|
|
219
|
+
const isFunction = value[key]?.type === 'function'
|
|
220
|
+
if (Array.isArray(defs.type) && value[key] && !isFunction) {
|
|
221
|
+
// handle multiple sub-types defs for array items
|
|
222
|
+
// there can be different subtypes defs of type object,
|
|
223
|
+
// as long as they all have a type property (mandatory - to distinguish def to check)
|
|
224
|
+
// and one subtype def for each primary data type
|
|
225
|
+
const types =
|
|
226
|
+
defs.type.length > 1 &&
|
|
227
|
+
Object.fromEntries(defs.type.map(t => [t.type.type?.value || t.type, t]))
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < value[key].length; i++) {
|
|
230
|
+
let def
|
|
231
|
+
if (types) {
|
|
232
|
+
const isObject = typeof value[key][i] === 'object'
|
|
233
|
+
const invalidType =
|
|
234
|
+
!value[key][i].type || typeof value[key][i].type !== 'string'
|
|
235
|
+
const allowedTypes = Object.keys(types).join(',').slice(0, -1)
|
|
236
|
+
if (isObject && invalidType) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`Type not allowed. Object item for ${key} array (#${i} item) must have one of these "type" property: ${allowedTypes}.`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
def = isObject ? types[value[key][i].type] : types[typeof value[key][i]]
|
|
242
|
+
if (!def) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`Missing type definition. Object item for ${key} array (#${i} item) must have one of these "type" property: ${allowedTypes}.`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
expandAttr(i, value[key], def || defs.type[0], object, getUser)
|
|
250
|
+
}
|
|
251
|
+
} else if (typeof defs.type === 'object' && value[key] && !isFunction) {
|
|
252
|
+
for (const [childKey, subDefs] of Object.entries(defs.type)) {
|
|
253
|
+
expandAttr(childKey, value[key], subDefs, object, getUser)
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
applyDefs(key, value, defs, object, getUser)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const expandAttrs = (object, getUser) => {
|
|
261
|
+
if (!object.children) return (object.children = {})
|
|
262
|
+
let prev
|
|
263
|
+
for (const child of Object.values(object.children)) {
|
|
264
|
+
child.parent = object
|
|
265
|
+
prev && (prev.next = child) && (child.prev = prev)
|
|
266
|
+
prev = child
|
|
267
|
+
|
|
268
|
+
// apply missing default functions
|
|
269
|
+
// a function can only be missing if it require the user,
|
|
270
|
+
// the event or current time
|
|
271
|
+
for (const [key, defs] of getDefaultAttrsEntries(child)) {
|
|
272
|
+
if (child.attrs[key] === null) {
|
|
273
|
+
console.warn(`value is null for ${child.name} - ${key} - ${child.id}`)
|
|
274
|
+
}
|
|
275
|
+
expandAttr(key, child.attrs, defs, child, getUser)
|
|
276
|
+
}
|
|
277
|
+
for (const [key, defs] of getDefaultRelAttrsEntries(
|
|
278
|
+
object.type,
|
|
279
|
+
child.type,
|
|
280
|
+
)) {
|
|
281
|
+
if (child.attrs[key] === null) {
|
|
282
|
+
console.warn(`value is null for ${child.name} - ${key} - ${child.id}`)
|
|
283
|
+
}
|
|
284
|
+
expandAttr(key, child.attrs, defs, child, getUser)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
expandAttrs(child, getUser)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { attrs, relationAttrs }
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { childTypes, objectTypes } from './toolbox.js'
|
|
2
|
+
import { attributes, relationAttributes } from './attrs.js'
|
|
3
|
+
|
|
4
|
+
const normalize = str =>
|
|
5
|
+
str
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replaceAll(/[^a-z0-9]+/g, ' ')
|
|
8
|
+
.trim()
|
|
9
|
+
.replaceAll(' ', '-')
|
|
10
|
+
|
|
11
|
+
const assertDef = def => {
|
|
12
|
+
const {
|
|
13
|
+
type,
|
|
14
|
+
name,
|
|
15
|
+
attrs,
|
|
16
|
+
children,
|
|
17
|
+
childrenAttrs,
|
|
18
|
+
refId,
|
|
19
|
+
referencePath,
|
|
20
|
+
...rest
|
|
21
|
+
} = def
|
|
22
|
+
const [extra] = Object.keys(rest)
|
|
23
|
+
if (extra) {
|
|
24
|
+
throw Error(
|
|
25
|
+
`Unexpected property ${extra}, did you mean to define an attribute ?`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (!objectTypes.has(type)) throw Error(`Invalid type property`)
|
|
29
|
+
if (!name || typeof name !== 'string') throw Error(`Invalid name property`)
|
|
30
|
+
if (attrs && typeof attrs !== 'object') throw Error(`Invalid attrs property`)
|
|
31
|
+
if (refId && typeof refId !== 'number') throw Error(`Invalid refId property`)
|
|
32
|
+
if (childrenAttrs) throw Error(`childrenAttrs is no longer supported`)
|
|
33
|
+
|
|
34
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
35
|
+
if (relationAttributes[key]) {
|
|
36
|
+
throw Error(
|
|
37
|
+
`Attr ${key} should be defined in the relation with its parent, not on the object itself.`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const matches = attributes[key]
|
|
42
|
+
if (!matches) throw Error(`Undefined attr ${key}`)
|
|
43
|
+
const attrDefs = matches[type]
|
|
44
|
+
if (!attrDefs) {
|
|
45
|
+
const types = Object.keys(matches).join(', ')
|
|
46
|
+
throw Error(`${type} does not match any of ${types} for attr ${key}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (attrDefs.deprecated) {
|
|
50
|
+
throw Error(`attr ${key} deprecated: ${attrDefs.deprecated}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// TODO: maybe check for circularity here
|
|
56
|
+
// I skipped this check for simplicity and performance
|
|
57
|
+
// because since we already have check that we have a correct parent / child type
|
|
58
|
+
// structure, a circular relation should not be possible
|
|
59
|
+
const buildTree = ({ name, type, attrs, children, referencePath }, parent) => {
|
|
60
|
+
const object = { name, type, attrs, children: {}, referencePath, parent }
|
|
61
|
+
if (!children) return object
|
|
62
|
+
let prev
|
|
63
|
+
for (const [key, { ref, ...rest }] of Object.entries(children)) {
|
|
64
|
+
const child = buildTree(ref, object)
|
|
65
|
+
child.attrs = { ...child.attrs, ...rest }
|
|
66
|
+
prev && (prev.next = child)
|
|
67
|
+
child.prev = prev
|
|
68
|
+
object.children[key] = child
|
|
69
|
+
}
|
|
70
|
+
return object
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const assertRelation = (parent, key, relation) => {
|
|
74
|
+
if (!/^[a-z0-9-]*$/.test(key)) {
|
|
75
|
+
throw Error(
|
|
76
|
+
`Invalid key for child ${key} Kebab-case key suggestion: ${normalize(key)}`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const child = relation.ref
|
|
81
|
+
const { type } = child
|
|
82
|
+
const allowedTypes = childTypes[parent.type]
|
|
83
|
+
if (!allowedTypes.includes(type)) {
|
|
84
|
+
throw Error(`Type ${type} ${key} is not a possible child`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Should never happen
|
|
88
|
+
if (parent.referencePath === child.referencePath) {
|
|
89
|
+
throw Error(`Self reference in child ${key}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [key, value] of Object.entries(relation)) {
|
|
93
|
+
if (key === 'ref') continue
|
|
94
|
+
const matches = attributes[key]
|
|
95
|
+
if (matches?.[type]?.restrictive) {
|
|
96
|
+
throw Error(
|
|
97
|
+
`Attr ${key} should be defined on the object itself, not in the relation with its parent.`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
const relMatches = relationAttributes[key]
|
|
101
|
+
if (!matches && !relMatches) throw Error(`Undefined attr ${key}`)
|
|
102
|
+
const attrDefs = (relMatches?.[parent.type] || matches)[type]
|
|
103
|
+
if (!attrDefs) {
|
|
104
|
+
const types = Object.keys(matches).join(', ')
|
|
105
|
+
throw Error(`${type} does not match any of ${types} for attr ${key}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (attrDefs.deprecated) {
|
|
109
|
+
throw Error(`attr ${key} deprecated: ${attrDefs.deprecated}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const checkAttrs = object => {
|
|
115
|
+
for (const [key, value] of Object.entries(object.attrs)) {
|
|
116
|
+
try {
|
|
117
|
+
const attrDefs =
|
|
118
|
+
relationAttributes[key]?.[object.parent?.type]?.[object.type] ||
|
|
119
|
+
attributes[key][object.type]
|
|
120
|
+
|
|
121
|
+
if (value?.type === 'function') {
|
|
122
|
+
const fn = attrDefs.functionsByName?.[value.name]
|
|
123
|
+
if (!fn) throw Error(`function ${value.name} not found for attr ${key}`)
|
|
124
|
+
} else if (!attrDefs.check(value, object)) {
|
|
125
|
+
throw Error(`wrong type for attr ${key}`)
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
err.parentRef = object.parent?.referencePath
|
|
129
|
+
err.childRef = object.referencePath
|
|
130
|
+
err.value = value
|
|
131
|
+
err.key = key
|
|
132
|
+
throw err
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const child of Object.values(object.children)) {
|
|
137
|
+
checkAttrs(child)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// TODO: replace by Set.intersection once available in node
|
|
142
|
+
const intersection = (a, b) => {
|
|
143
|
+
const result = new Set()
|
|
144
|
+
for (const element of a) {
|
|
145
|
+
b.has(element) && result.add(element)
|
|
146
|
+
}
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const generatePairs = arr => {
|
|
151
|
+
const result = []
|
|
152
|
+
let i = -1
|
|
153
|
+
// Only go up to the second to last element
|
|
154
|
+
while (++i < arr.length - 1) {
|
|
155
|
+
let j = i
|
|
156
|
+
// Start from the next element to avoid duplicates
|
|
157
|
+
while (++j < arr.length) {
|
|
158
|
+
result.push([arr[i], arr[j]])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isExam = def => def.type === 'exam'
|
|
165
|
+
const assertExams = definitions => {
|
|
166
|
+
const groups = {}
|
|
167
|
+
for (const exam of definitions.filter(isExam)) {
|
|
168
|
+
let prevExamGroup = 0
|
|
169
|
+
for (const [key, exercise] of Object.entries(exam.children)) {
|
|
170
|
+
try {
|
|
171
|
+
const { group } = exercise
|
|
172
|
+
if (!group) throw Error('missing exam group')
|
|
173
|
+
|
|
174
|
+
const groupDiff = group - prevExamGroup
|
|
175
|
+
prevExamGroup = group
|
|
176
|
+
if (groupDiff > 1) throw Error('exam must not have gaps in groups')
|
|
177
|
+
if (groupDiff < 0) {
|
|
178
|
+
throw Error('exercise must be sorted by group from lower to greater')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// exam exercise group should stay consistent across all exams
|
|
182
|
+
const prevGroup = groups[key]
|
|
183
|
+
if (prevGroup == null) {
|
|
184
|
+
groups[key] = group
|
|
185
|
+
} else if (prevGroup !== group) {
|
|
186
|
+
throw Error('Exercise used in different exam groups')
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
err.exam = exam.referencePath
|
|
190
|
+
err.exercise = exercise.ref.referencePath
|
|
191
|
+
throw err
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const checkAndBuildDefinitions = async readDef => {
|
|
198
|
+
const cache = {}
|
|
199
|
+
const getDefs = async ([key, relation]) => {
|
|
200
|
+
const def = cache[key] || (cache[key] = await readDef(key))
|
|
201
|
+
try {
|
|
202
|
+
assertDef(def)
|
|
203
|
+
} catch (err) {
|
|
204
|
+
err.type = def.type
|
|
205
|
+
err.referencePath = def.referencePath
|
|
206
|
+
throw err
|
|
207
|
+
}
|
|
208
|
+
relation && (relation.ref = def)
|
|
209
|
+
const relations = Object.entries(def.children || {}).map(getDefs)
|
|
210
|
+
return [def, ...(await Promise.all(relations)).flat()]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const definitions = await getDefs([])
|
|
214
|
+
|
|
215
|
+
// Assert all relations looks ok, possible child / parent, allowed attributes
|
|
216
|
+
for (const def of definitions) {
|
|
217
|
+
if (!def.children) continue
|
|
218
|
+
const allowedTypes = childTypes[def.type]
|
|
219
|
+
try {
|
|
220
|
+
for (const entry of Object.entries(def.children)) {
|
|
221
|
+
assertRelation(def, entry[0], entry[1])
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
err.type = def.type
|
|
225
|
+
err.referencePath = def.referencePath
|
|
226
|
+
throw err
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check all attributes in context
|
|
231
|
+
const root = buildTree(definitions[0])
|
|
232
|
+
checkAttrs(root)
|
|
233
|
+
assertExams(definitions)
|
|
234
|
+
|
|
235
|
+
return { definitions, root }
|
|
236
|
+
}
|
package/languages.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const languages = {
|
|
2
|
+
ab: 'Abkhazian',
|
|
3
|
+
aa: 'Afar',
|
|
4
|
+
af: 'Afrikaans',
|
|
5
|
+
sq: 'Albanian',
|
|
6
|
+
am: 'Amharic',
|
|
7
|
+
ar: 'Arabic',
|
|
8
|
+
hy: 'Armenian',
|
|
9
|
+
as: 'Assamese',
|
|
10
|
+
ay: 'Aymara',
|
|
11
|
+
az: 'Azerbaijani',
|
|
12
|
+
ba: 'Bashkir',
|
|
13
|
+
eu: 'Basque',
|
|
14
|
+
bn: 'Bengali (Bangla)',
|
|
15
|
+
dz: 'Bhutani',
|
|
16
|
+
bh: 'Bihari',
|
|
17
|
+
bi: 'Bislama',
|
|
18
|
+
br: 'Breton',
|
|
19
|
+
bg: 'Bulgarian',
|
|
20
|
+
my: 'Burmese',
|
|
21
|
+
be: 'Byelorussian (Belarusian)',
|
|
22
|
+
km: 'Cambodian',
|
|
23
|
+
ca: 'Catalan',
|
|
24
|
+
zh: 'Chinese (Simplified)',
|
|
25
|
+
// zh: 'Chinese (Traditional)',
|
|
26
|
+
co: 'Corsican',
|
|
27
|
+
hr: 'Croatian',
|
|
28
|
+
cs: 'Czech',
|
|
29
|
+
da: 'Danish',
|
|
30
|
+
nl: 'Dutch',
|
|
31
|
+
en: 'English',
|
|
32
|
+
eo: 'Esperanto',
|
|
33
|
+
et: 'Estonian',
|
|
34
|
+
fo: 'Faeroese',
|
|
35
|
+
fa: 'Farsi',
|
|
36
|
+
fj: 'Fiji',
|
|
37
|
+
fi: 'Finnish',
|
|
38
|
+
fr: 'French',
|
|
39
|
+
fy: 'Frisian',
|
|
40
|
+
gl: 'Galician',
|
|
41
|
+
gd: 'Gaelic (Scottish)',
|
|
42
|
+
gv: 'Gaelic (Manx)',
|
|
43
|
+
ka: 'Georgian',
|
|
44
|
+
de: 'German',
|
|
45
|
+
el: 'Greek',
|
|
46
|
+
kl: 'Greenlandic',
|
|
47
|
+
gn: 'Guarani',
|
|
48
|
+
gu: 'Gujarati',
|
|
49
|
+
ha: 'Hausa',
|
|
50
|
+
he: 'Hebrew',
|
|
51
|
+
hi: 'Hindi',
|
|
52
|
+
hu: 'Hungarian',
|
|
53
|
+
is: 'Icelandic',
|
|
54
|
+
id: 'Indonesian',
|
|
55
|
+
ia: 'Interlingua',
|
|
56
|
+
ie: 'Interlingue',
|
|
57
|
+
iu: 'Inuktitut',
|
|
58
|
+
ik: 'Inupiak',
|
|
59
|
+
ga: 'Irish',
|
|
60
|
+
it: 'Italian',
|
|
61
|
+
ja: 'Japanese',
|
|
62
|
+
// jv: 'Javanese',
|
|
63
|
+
kn: 'Kannada',
|
|
64
|
+
ks: 'Kashmiri',
|
|
65
|
+
kk: 'Kazakh',
|
|
66
|
+
rw: 'Kinyarwanda (Ruanda)',
|
|
67
|
+
ky: 'Kirghiz',
|
|
68
|
+
rn: 'Kirundi (Rundi)',
|
|
69
|
+
ko: 'Korean',
|
|
70
|
+
ku: 'Kurdish',
|
|
71
|
+
lo: 'Laothian',
|
|
72
|
+
la: 'Latin',
|
|
73
|
+
lv: 'Latvian (Lettish)',
|
|
74
|
+
li: 'Limburgish ( Limburger)',
|
|
75
|
+
ln: 'Lingala',
|
|
76
|
+
lt: 'Lithuanian',
|
|
77
|
+
mk: 'Macedonian',
|
|
78
|
+
mg: 'Malagasy',
|
|
79
|
+
ms: 'Malay',
|
|
80
|
+
ml: 'Malayalam',
|
|
81
|
+
mt: 'Maltese',
|
|
82
|
+
mi: 'Maori',
|
|
83
|
+
mr: 'Marathi',
|
|
84
|
+
mo: 'Moldavian',
|
|
85
|
+
mn: 'Mongolian',
|
|
86
|
+
na: 'Nauru',
|
|
87
|
+
ne: 'Nepali',
|
|
88
|
+
no: 'Norwegian',
|
|
89
|
+
oc: 'Occitan',
|
|
90
|
+
or: 'Oriya',
|
|
91
|
+
om: 'Oromo (Afan, Galla)',
|
|
92
|
+
ps: 'Pashto (Pushto)',
|
|
93
|
+
pl: 'Polish',
|
|
94
|
+
pt: 'Portuguese',
|
|
95
|
+
pa: 'Punjabi',
|
|
96
|
+
qu: 'Quechua',
|
|
97
|
+
rm: 'Rhaeto-Romance',
|
|
98
|
+
ro: 'Romanian',
|
|
99
|
+
ru: 'Russian',
|
|
100
|
+
sm: 'Samoan',
|
|
101
|
+
sg: 'Sangro',
|
|
102
|
+
sa: 'Sanskrit',
|
|
103
|
+
sr: 'Serbian',
|
|
104
|
+
sh: 'Serbo-Croatian',
|
|
105
|
+
st: 'Sesotho',
|
|
106
|
+
tn: 'Setswana',
|
|
107
|
+
sn: 'Shona',
|
|
108
|
+
sd: 'Sindhi',
|
|
109
|
+
si: 'Sinhalese',
|
|
110
|
+
ss: 'Siswati',
|
|
111
|
+
sk: 'Slovak',
|
|
112
|
+
sl: 'Slovenian',
|
|
113
|
+
so: 'Somali',
|
|
114
|
+
es: 'Spanish',
|
|
115
|
+
su: 'Sundanese',
|
|
116
|
+
sw: 'Swahili (Kiswahili)',
|
|
117
|
+
sv: 'Swedish',
|
|
118
|
+
tl: 'Tagalog',
|
|
119
|
+
tg: 'Tajik',
|
|
120
|
+
ta: 'Tamil',
|
|
121
|
+
tt: 'Tatar',
|
|
122
|
+
te: 'Telugu',
|
|
123
|
+
th: 'Thai',
|
|
124
|
+
bo: 'Tibetan',
|
|
125
|
+
ti: 'Tigrinya',
|
|
126
|
+
to: 'Tonga',
|
|
127
|
+
ts: 'Tsonga',
|
|
128
|
+
tr: 'Turkish',
|
|
129
|
+
tk: 'Turkmen',
|
|
130
|
+
tw: 'Twi',
|
|
131
|
+
ug: 'Uighur',
|
|
132
|
+
uk: 'Ukrainian',
|
|
133
|
+
ur: 'Urdu',
|
|
134
|
+
uz: 'Uzbek',
|
|
135
|
+
vi: 'Vietnamese',
|
|
136
|
+
vo: 'Volapük',
|
|
137
|
+
cy: 'Welsh',
|
|
138
|
+
wo: 'Wolof',
|
|
139
|
+
xh: 'Xhosa',
|
|
140
|
+
yi: 'Yiddish',
|
|
141
|
+
yo: 'Yoruba',
|
|
142
|
+
zu: 'Zulu',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const languagesByName = Object.fromEntries(
|
|
146
|
+
Object.entries(languages).map(([code, language]) => [language, code]),
|
|
147
|
+
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@01-edu/shared",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"scripts": {
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"./bin/check-definitions.js",
|
|
11
11
|
"./event-utils.js",
|
|
12
|
-
"./event-utils.js",
|
|
13
12
|
"./lodash.deburr.js",
|
|
14
13
|
"./onboarding.js",
|
|
15
|
-
"./
|
|
14
|
+
"./attrs.js",
|
|
15
|
+
"./attrs-defs.js",
|
|
16
|
+
"./languages.js",
|
|
17
|
+
"./definitions-checker.js",
|
|
16
18
|
"./path.js",
|
|
17
19
|
"./programming-languages.js",
|
|
18
20
|
"./score.js",
|