@01-edu/shared 1.0.6 → 1.0.12

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 CHANGED
@@ -12,12 +12,17 @@ const typeCheckers = {
12
12
  array: Array.isArray,
13
13
  }
14
14
 
15
+ const determinType = value => {
16
+ if (Array.isArray(value)) return 'array'
17
+ if (typeof value === 'object' && value !== null) return value.type
18
+ return typeof value
19
+ }
15
20
  const typeChecker = (defs, value, object, key) => {
16
21
  const { type, check, options } = defs
17
22
 
18
23
  if (value == null) {
19
- if (!defs.required) return true
20
- // if no value for required attribute, reject
24
+ if (!defs.required || defs.value !== undefined) return true
25
+ // if no value for required attribute without a default value, reject
21
26
  throw Error(`missing value for required attribute ${key}`)
22
27
  }
23
28
 
@@ -33,11 +38,17 @@ const typeChecker = (defs, value, object, key) => {
33
38
 
34
39
  if (options) {
35
40
  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
+ if (opts.length === 1) {
42
+ if (opts[0] !== value) {
43
+ throw Error(`${key} must be ${opts[0]} but was ${value}`)
44
+ }
45
+ } else {
46
+ const isAnOption = opts.includes(value)
47
+ if (!isAnOption) {
48
+ throw Error(
49
+ `invalid option for ${key}: should be included in ${opts.join(', ')}`,
50
+ )
51
+ }
41
52
  }
42
53
  }
43
54
 
@@ -55,16 +66,23 @@ const typeChecker = (defs, value, object, key) => {
55
66
  // every value have to match one of the type definition
56
67
 
57
68
  const uniqueDef = type.length === 1 && type[0]
69
+ // convert array type into object for better accessibility
58
70
  const types =
59
71
  !uniqueDef &&
60
- Object.fromEntries(type.map(t => [t.type?.type?.value || t.type, t]))
72
+ Object.fromEntries(
73
+ type.map(t => [
74
+ Array.isArray(t.type) ? 'array' : t.type?.type?.value || t.type,
75
+ t,
76
+ ]),
77
+ )
78
+
61
79
  for (const [index, v] of value.entries()) {
62
80
  const err = Error('checks failed for all types')
63
81
  err.index = index
64
82
  err.key = key
65
83
  err.label = defs.label
66
- const subdefs =
67
- uniqueDef || (typeof v === 'object' ? types[v.type] : types[typeof v])
84
+ const subdefs = uniqueDef || types[determinType(v)]
85
+
68
86
  if (!subdefs) {
69
87
  err.details = {
70
88
  label: 'Unknown structure',
@@ -79,7 +97,6 @@ const typeChecker = (defs, value, object, key) => {
79
97
  label: subdefs.label || error.label,
80
98
  err: error,
81
99
  }
82
-
83
100
  throw err
84
101
  }
85
102
  }
@@ -111,7 +128,7 @@ const typeChecker = (defs, value, object, key) => {
111
128
  // same as attrs with the check & function by name generated and descriptions form markdown files
112
129
  export const attributes = mapEntries(attrs, ([attrKey, matches]) => [
113
130
  attrKey,
114
- mapValues(matches, (defs, type) => ({
131
+ mapValues(matches, (defs /* type */) => ({
115
132
  ...defs,
116
133
  check: (value, object) => typeChecker(defs, value, object, attrKey),
117
134
  })),
@@ -121,8 +138,8 @@ export const relationAttributes = mapEntries(
121
138
  relationAttrs,
122
139
  ([attrKey, byParent]) => [
123
140
  attrKey,
124
- mapValues(byParent, (matches, parentType) =>
125
- mapValues(matches, (defs, childType) => ({
141
+ mapValues(byParent, (matches /* parentType */) =>
142
+ mapValues(matches, (defs /* childType */) => ({
126
143
  ...defs,
127
144
  check: (value, object) => typeChecker(defs, value, object, attrKey),
128
145
  })),
@@ -130,6 +147,68 @@ export const relationAttributes = mapEntries(
130
147
  ],
131
148
  )
132
149
 
150
+ // white list of attributes that can be applied in bulk
151
+ const allowedBulkAttrs = {
152
+ codeEditor: { enabled: {} },
153
+ validations: {
154
+ // this is for the different options of validation,
155
+ // we know that raids for now is the only one that contains multiple validations
156
+ // and we **don't** want admin_selection
157
+ type: [
158
+ 'admin_audit',
159
+ 'tester',
160
+ 'dedicated_auditors_for_event',
161
+ 'user_audit',
162
+ ],
163
+ ratio: {},
164
+ required: {},
165
+ matchInfluence: {},
166
+ cooldown: {},
167
+ preQuestions: {},
168
+ postQuestions: {},
169
+ matchWhere: {},
170
+ },
171
+ }
172
+ export const getAllBulkAttrs = (parentType, childType) => {
173
+ const relationAttrs = getDefaultRelAttrs(parentType, childType)
174
+ const attrs = getDefaultAttrs({ type: childType })
175
+ const allAttrs = { ...relationAttrs, ...attrs }
176
+ return filterBulkAttrs(allAttrs, allowedBulkAttrs)
177
+ }
178
+ export const filterBulkAttrs = (attrs, allowedAttrs) => {
179
+ if (!attrs) return {}
180
+ if (Array.isArray(attrs.type)) {
181
+ const { type, ...rest } = attrs
182
+ // if it is type array and from the validation attribute we need to filter the validation type
183
+ const types = type
184
+ .map(a => filterBulkAttrs(a, allowedAttrs))
185
+ .filter(({ type }) => allowedAttrs.type.includes(type.type.value))
186
+ return { ...rest, type: types }
187
+ }
188
+ if (attrs.type && typeof attrs.type === 'object') {
189
+ const { type, ...rest } = attrs
190
+ const filtered = { type: {}, ...rest }
191
+ const typesKeys = Object.keys(type)
192
+ for (const t of typesKeys) {
193
+ if (allowedAttrs?.[t]) {
194
+ filtered.type[t] = type[t]
195
+ }
196
+ }
197
+ return filtered
198
+ }
199
+ if (typeof attrs === 'object' && !attrs.type) {
200
+ const filtered = {}
201
+ const keys = Object.keys(attrs)
202
+ for (const k of keys) {
203
+ if (allowedAttrs[k]) {
204
+ filtered[k] = filterBulkAttrs(attrs[k], allowedAttrs[k])
205
+ }
206
+ }
207
+ return filtered
208
+ }
209
+ return null
210
+ }
211
+
133
212
  // map from attrs[name][type] to attrs[type][name]
134
213
  export const attrsByType = {}
135
214
 
@@ -141,7 +220,7 @@ for (const [name, matches] of Object.entries(attributes)) {
141
220
  // handle translations: generate translation attrs and required status
142
221
  const { label, ...restDefs } = defs
143
222
  if (defs.functionsByName?.translate) {
144
- // perf measures done: inscrease from 0.671ms loadtime to 13.261ms
223
+ // perf measures done: increase from 0.671ms loadtime to 13.261ms
145
224
  // should not impact the perfs
146
225
  for (const [code, language] of languagesEntries) {
147
226
  const newLabel = `${label} - ${language}`
@@ -259,6 +338,14 @@ const expandAttr = (key, value, defs, object, getUser) => {
259
338
 
260
339
  export const expandAttrs = (object, getUser) => {
261
340
  if (!object.children) return (object.children = {})
341
+
342
+ for (const [key, defs] of getDefaultAttrsEntries(object)) {
343
+ if (object.attrs[key] === null) {
344
+ console.warn(`value is null for ${object.name} - ${key} - ${object.id}`)
345
+ }
346
+ expandAttr(key, object.attrs, defs, object, getUser)
347
+ }
348
+
262
349
  let prev
263
350
  for (const child of Object.values(object.children)) {
264
351
  child.parent = object
@@ -4,6 +4,7 @@ import { readFile, stat, watch } from 'node:fs/promises'
4
4
 
5
5
  import { checkAndBuildDefinitions } from '../definitions-checker.js'
6
6
 
7
+ const rootTypes = ['module', 'piscine', 'signup', 'onboarding']
7
8
  const isAudit = validation => validation.type.endsWith('_audit')
8
9
  const readDef = async key => {
9
10
  const path = key == null ? 'content/def.json' : `content/${key}/def.json`
@@ -11,6 +12,12 @@ const readDef = async key => {
11
12
  def.attrs || (def.attrs = {})
12
13
  def.referencePath = path
13
14
 
15
+ if (key == null && !rootTypes.includes(def.type)) {
16
+ throw Error(
17
+ `Root definition must be one of ${rootTypes.join(', ')}, found: ${def.type}`,
18
+ )
19
+ }
20
+
14
21
  switch (def.type) {
15
22
  case 'project':
16
23
  // biome-ignore lint/suspicious/noFallthroughSwitchClause: We want to fallthrough
@@ -54,6 +61,7 @@ if (process.argv.includes('--watch') || process.argv.includes('-w')) {
54
61
  await runChecks()
55
62
  for await (const event of watch('.', { recursive: true })) {
56
63
  console.clear()
64
+ if (!event.filename.endsWith('def.json')) continue
57
65
  console.log(event.eventType, 'on', event.filename, '\n')
58
66
  await runChecks()
59
67
  }
@@ -1,5 +1,5 @@
1
- import { childTypes, objectTypes } from './toolbox.js'
2
1
  import { attributes, relationAttributes } from './attrs.js'
2
+ import { childTypes, objectTypes } from './toolbox.js'
3
3
 
4
4
  const normalize = str =>
5
5
  str
@@ -9,16 +9,8 @@ const normalize = str =>
9
9
  .replaceAll(' ', '-')
10
10
 
11
11
  const assertDef = def => {
12
- const {
13
- type,
14
- name,
15
- attrs,
16
- children,
17
- childrenAttrs,
18
- refId,
19
- referencePath,
20
- ...rest
21
- } = def
12
+ const { type, name, attrs, children, childrenAttrs, referencePath, ...rest } =
13
+ def
22
14
  const [extra] = Object.keys(rest)
23
15
  if (extra) {
24
16
  throw Error(
@@ -27,11 +19,12 @@ const assertDef = def => {
27
19
  }
28
20
  if (!objectTypes.has(type)) throw Error(`Invalid type property`)
29
21
  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`)
22
+ if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs)) {
23
+ throw Error(`Invalid attrs property`)
24
+ }
32
25
  if (childrenAttrs) throw Error(`childrenAttrs is no longer supported`)
33
26
 
34
- for (const key of Object.keys(attrs)) {
27
+ for (const key of Object.keys(attrs || {})) {
35
28
  if (relationAttributes[key]) {
36
29
  throw Error(
37
30
  `Attr ${key} should be defined in the relation with its parent, not on the object itself.`,
@@ -52,16 +45,19 @@ const assertDef = def => {
52
45
  }
53
46
  }
54
47
 
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) => {
48
+ const buildTree = (
49
+ { name, type, attrs, children, referencePath },
50
+ parent,
51
+ depth,
52
+ ) => {
53
+ // As we check for the parent / child type structure, this should never happen
54
+ // so we did not bother to make a more descriptive error for this case
55
+ if (depth > 100) throw Error('Unexpected very deep tree, maybe circular ?')
60
56
  const object = { name, type, attrs, children: {}, referencePath, parent }
61
57
  if (!children) return object
62
58
  let prev
63
59
  for (const [key, { ref, ...rest }] of Object.entries(children)) {
64
- const child = buildTree(ref, object)
60
+ const child = buildTree(ref, object, (depth || 0) + 1)
65
61
  child.attrs = { ...child.attrs, ...rest }
66
62
  prev && (prev.next = child)
67
63
  child.prev = prev
@@ -73,7 +69,9 @@ const buildTree = ({ name, type, attrs, children, referencePath }, parent) => {
73
69
  const assertRelation = (parent, key, relation) => {
74
70
  if (!/^[a-z0-9-]*$/.test(key)) {
75
71
  throw Error(
76
- `Invalid key for child ${key} Kebab-case key suggestion: ${normalize(key)}`,
72
+ `Invalid key for child ${key} Kebab-case key suggestion: ${normalize(
73
+ key,
74
+ )}`,
77
75
  )
78
76
  }
79
77
 
@@ -176,7 +174,10 @@ export const checkAndBuildDefinitions = async readDef => {
176
174
  const getDefs = async ([key, relation]) => {
177
175
  let def
178
176
  try {
179
- def = cache[key] || (cache[key] = await readDef(key))
177
+ const cached = cache[key]
178
+ def = await (cached || (cache[key] = readDef(key)))
179
+ relation && (relation.ref = def)
180
+ if (cached) return [] // skip assert and children checks if already cached
180
181
  } catch (err) {
181
182
  err.name = key
182
183
  throw err
@@ -188,11 +189,12 @@ export const checkAndBuildDefinitions = async readDef => {
188
189
  err.referencePath = def.referencePath
189
190
  throw err
190
191
  }
191
- relation && (relation.ref = def)
192
192
  const relations = Object.entries(def.children || {}).map(getDefs)
193
193
  try {
194
194
  return [def, ...(await Promise.all(relations)).flat()]
195
195
  } catch (err) {
196
+ // Some content may be used at multiple place (ex: exam exercises)
197
+ // so we may have multiple parents
196
198
  key && (err.parents || (err.parents = [])).push(key)
197
199
  throw err
198
200
  }
package/onboarding.js CHANGED
@@ -9,6 +9,7 @@ export const onboardingTypes = new Set([
9
9
  'sign-step',
10
10
  'upload-step',
11
11
  'contact-validation-step',
12
+ 'avatar-step',
12
13
  ])
13
14
 
14
15
  export const prevValidated = (key, object) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01-edu/shared",
3
- "version": "1.0.6",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "./skill-definitions.js",
22
22
  "./toolbox.js"
23
23
  ],
24
- "license": "MIT",
24
+ "license": "Fair",
25
25
  "bin": {
26
26
  "check-defs": "./bin/check-definitions.js"
27
27
  },
package/path.js CHANGED
@@ -39,15 +39,23 @@ const walk = (object, segment) => {
39
39
  }
40
40
 
41
41
  /**
42
- * @returns {Object} the object in the relative path
43
- * @argument relativePath {string} a relative path
44
- * @argument currentObj {Object} the object from where the relative path starts
42
+ * @param {string} relativePath - a relative path
43
+ * @param {Object} currentObj - the object from where the relative path starts
44
+ * @param {Object} [opts = { throwError: true } ] - options object with boolean property `throwError` to throw an error in case the path is invalid or no object is found
45
+ * @returns {Object} - the object in the relative path
45
46
  */
46
- export const getObjectFromRelativePath = (relativePath, currentObj) => {
47
+ export const getObjectFromRelativePath = (
48
+ relativePath,
49
+ currentObj,
50
+ opts = { throwError: true },
51
+ ) => {
47
52
  const currentPath = currentObj.path
48
53
  if (isAbsolutePath(relativePath)) {
49
- throw Error("'relativePath' must be a relative path")
54
+ if (opts.throwError) throw Error("'relativePath' must be a relative path")
55
+ console.error("'relativePath' must be a relative path")
56
+ return undefined
50
57
  }
58
+
51
59
  // Ignore trailing `/` in the relative path
52
60
  if (relativePath[relativePath.length - 1] === '/') {
53
61
  relativePath = relativePath.slice(0, -1)
@@ -55,9 +63,11 @@ export const getObjectFromRelativePath = (relativePath, currentObj) => {
55
63
 
56
64
  const matchingObject = relativePath.split('/').reduce(walk, currentObj)
57
65
  if (matchingObject) return matchingObject
58
- throw Error(
59
- `Incorrect relative path '${relativePath}': no object found — current path = '${currentPath}'`,
60
- )
66
+
67
+ const errorMessage = `Incorrect relative path '${relativePath}': no object found — current path = '${currentPath}'`
68
+ if (opts.throwError) throw Error(errorMessage)
69
+ console.error(errorMessage)
70
+ return undefined
61
71
  }
62
72
 
63
73
  const isAbsolutePath = path => path.startsWith('/')
package/toolbox.js CHANGED
@@ -27,7 +27,13 @@ export const objectTypes = new Set([...contentObjects, ...onboardingTypes])
27
27
 
28
28
  export const childTypes = {
29
29
  campus: ['signup', 'onboarding', 'piscine', 'module'],
30
- signup: ['form-step', 'sign-step', 'upload-step', 'contact-validation-step'],
30
+ signup: [
31
+ 'form-step',
32
+ 'sign-step',
33
+ 'upload-step',
34
+ 'contact-validation-step',
35
+ 'avatar-step',
36
+ ],
31
37
  onboarding: [
32
38
  'games',
33
39
  'administration',
@@ -40,6 +46,7 @@ export const childTypes = {
40
46
  'sign-step',
41
47
  'upload-step',
42
48
  'contact-validation-step',
49
+ 'avatar-step',
43
50
  ],
44
51
  piscine: ['quest', 'exam', 'raid', 'project'],
45
52
  exam: ['exercise'],
@@ -182,15 +189,17 @@ export const formatedDuration = (seconds, { noSeconds } = {}) => {
182
189
  }
183
190
 
184
191
  const toDate = date => (date instanceof Date ? date : new Date(date))
185
- export const toDateFormat = d => {
192
+ export const toDateFormat = (d, isISO = true) => {
186
193
  const date = toDate(d)
187
194
  const month = (date.getMonth() + 1).toString().padStart(2, '0')
188
195
  const day = date.getDate().toString().padStart(2, '0')
189
196
 
190
- return `${date.getFullYear()}-${month}-${day}`
197
+ return isISO
198
+ ? `${date.getFullYear()}-${month}-${day}`
199
+ : `${day}/${month}/${date.getFullYear()}`
191
200
  }
192
201
 
193
- export const toISOStringWithTimeZone = d => toDate(d).toISOString().slice(0, -1)
202
+ export const toISOStringWithTimeZone = d => toDate(d).toISOString()
194
203
 
195
204
  export const toDateFormatWithTime = (d, separator = 'T') => {
196
205
  const date = toDate(d)
@@ -220,7 +229,7 @@ export const monthNames = [
220
229
  'Dec',
221
230
  ]
222
231
 
223
- const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
232
+ export const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
224
233
 
225
234
  const hour = hour => {
226
235
  if (hour === 0 || hour === 12) return 12
@@ -256,12 +265,12 @@ const getDateElems = date => {
256
265
  return { day, jj, mm, yyyy, hh, min }
257
266
  }
258
267
 
259
- export const formatedDateTime = date => {
268
+ export const formatedDateTime = (date, splitter = 'at') => {
260
269
  if (!date) return '-'
261
270
  if (dateIsPermanent(date)) return '∞'
262
271
  const { jj, mm, yyyy, hh, min } = getDateElems(date)
263
272
 
264
- return `${monthNames[mm]} ${jj}, ${yyyy} at ${hour(hh)}:${minPad(
273
+ return `${monthNames[mm]} ${jj}, ${yyyy} ${splitter} ${hour(hh)}:${minPad(
265
274
  min,
266
275
  )} ${suffix(hh)}`
267
276
  }
@@ -337,13 +346,12 @@ const toSnakeCase = str =>
337
346
  // id card > id-card
338
347
  // should be enough for unique usage we have of it in check-refs
339
348
  // checked the perfs and it is faster + more secure than using replace(s) + toLowerCase
340
- const toLowerCase = x => x.toLowerCase()
341
349
  export const toKebabCase = str =>
342
350
  str
343
351
  .match(
344
352
  /[A-Z](?=[A-Z][a-z]+[0-9]*[a-z]|\b)|[A-Z]?[a-z]+[0-9]+[a-z]?[A-Z]*|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g,
345
353
  )
346
- .map(toLowerCase)
354
+ .map(x => x.toLowerCase())
347
355
  .join('-')
348
356
 
349
357
  const spaceJoin = (_, a, b) => `${a} ${b}`
@@ -481,3 +489,18 @@ export const base64Encode = str => {
481
489
 
482
490
  export const timePlusDelay = (time, delay) =>
483
491
  new Date(new Date(time).getTime() + delay).toISOString()
492
+
493
+ export const getRecordStatus = record => {
494
+ if (!isFinished(record.startAt)) return 'starting soon'
495
+ if (record.endAt && isFinished(record.endAt)) {
496
+ return 'finished'
497
+ }
498
+ if (record.type.isPermanent) return 'permanent'
499
+ return record.endAt ? 'in progress' : 'unblock required'
500
+ }
501
+
502
+ export const createFrequencyMap = arr =>
503
+ arr.reduce((map, item) => {
504
+ map[item] = (map[item] || 0) + 1
505
+ return map
506
+ }, {})