@01-edu/shared 1.0.3 → 1.0.5

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.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 }
@@ -5,24 +5,30 @@ import { readFile, stat } from 'node:fs/promises'
5
5
  import { checkAndBuildDefinitions } from '../definitions-checker.js'
6
6
 
7
7
  const auditValidation = validation => validation.type.endsWith('_audit')
8
- const result = await checkAndBuildDefinitions(async key => {
9
- const referencePath = `content${key ? `/${key}` : ''}/def.json`
10
- const def = JSON.parse(await readFile(referencePath, 'utf8'))
11
- def.attrs || (def.attrs = {})
12
- def.referencePath = referencePath
13
- switch (def.type) {
14
- case 'project':
15
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: We want to fallthrough
16
- case 'raid': {
17
- await stat(`content/${key}/audit/README.md`)
8
+ try {
9
+ const result = await checkAndBuildDefinitions(async key => {
10
+ const referencePath = `content${key ? `/${key}` : ''}/def.json`
11
+ const def = JSON.parse(await readFile(referencePath, 'utf8'))
12
+ def.attrs || (def.attrs = {})
13
+ def.referencePath = referencePath
14
+ switch (def.type) {
15
+ case 'project':
16
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We want to fallthrough
17
+ case 'raid': {
18
+ await stat(`content/${key}/audit/README.md`)
19
+ }
20
+ case 'exercise': {
21
+ await stat(`content/${key}/README.md`)
22
+ }
18
23
  }
19
- case 'exercise': {
20
- await stat(`content/${key}/README.md`)
21
- }
22
- }
23
24
 
24
- return def
25
- })
25
+ return def
26
+ })
27
+ } catch ({ message, ...props }) {
28
+ console.error(message, props)
29
+ process.exit(1)
30
+ }
26
31
 
32
+ console.log('All checks passed, no errors')
27
33
  // Extra checks:
28
34
  // - Orphan files (defs files not referenced anywhere)
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",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {
@@ -11,6 +11,9 @@
11
11
  "./event-utils.js",
12
12
  "./lodash.deburr.js",
13
13
  "./onboarding.js",
14
+ "./attrs.js",
15
+ "./attrs-defs.js",
16
+ "./languages.js",
14
17
  "./definitions-checker.js",
15
18
  "./path.js",
16
19
  "./programming-languages.js",