• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CSCfi / metadata-submitter-frontend / 19494873348

19 Nov 2025 08:31AM UTC coverage: 59.668% (+1.3%) from 58.339%
19494873348

push

github

Hang Le
Add JSON schemas to frontend, refactor the codes, add more localization configs (merge commit)

Merge branch 'feature/move-json-schemas-to-frontend' into 'main'
* Fix submissionDetails objectType not fetchable

* Add JSON schemas to frontend and refactor the codes

Closes #1092, #1097, #1108, #1109, #1111, #1113, and #1114
See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1183

Approved-by: Monika Radaviciute <mradavic@csc.fi>
Merged by Hang Le <lhang@csc.fi>

639 of 831 branches covered (76.9%)

Branch coverage included in aggregate %.

400 of 578 new or added lines in 36 files covered. (69.2%)

62 existing lines in 12 files now uncovered.

5400 of 9290 relevant lines covered (58.13%)

4.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.26
/src/utils/JSONSchemaUtils.tsx
1
import $RefParser from "@apidevtools/json-schema-ref-parser"
1✔
2
import Ajv2020 from "ajv/dist/2020"
1✔
3
import { mergeWith } from "lodash"
1✔
4

5
import { FormObject } from "types"
6

7
/*
8
 * Solve $ref -references in schema, return new schema instead of modifying passed in-place.
9
 */
10
export const dereferenceSchema = async (schema: FormObject) => {
1✔
11
  const dereferenced = JSON.parse(JSON.stringify(schema))
6✔
12
  await $RefParser.dereference(dereferenced)
6✔
13
  delete dereferenced["definitions"]
6✔
14
  return dereferenced
6✔
15
}
6✔
16

17
/*
18
 * Translate array of path object levels (such as ["descriptor", "studyType"]) to unique name ("descriptor.studyType")
19
 */
20
export const pathToName = (path: string[]): string => path.join(".")
1✔
21

22
/*
23
 * Get pathName for a specific field based on the pathName of another field (only different in the last element)
24
 */
25
export const getPathName = (path: Array<string>, field: string): string => {
1✔
26
  const pathName = path.slice(0, -1).join(".").concat(".", field)
×
27
  return pathName
×
28
}
×
29

30
/*
31
 * Parse initial values from given object
32
 */
33

34
type ValueType = { [key: string]: unknown }
35

36
export const traverseValues = (object: FormObject) => {
1✔
37
  if (object["oneOf"]) return object
44✔
38

39
  switch (object["type"]) {
36✔
40
    case "object": {
36✔
41
      const values: ValueType = {}
9✔
42
      const properties = object["properties"]
9✔
43
      for (const propertyKey in properties) {
9✔
44
        const property = properties[propertyKey] as FormObject
27✔
45
        values[propertyKey] = traverseValues(property)
27✔
46
      }
27✔
47

48
      return values
9✔
49
    }
9✔
50
    case "string": {
44✔
51
      return ""
18✔
52
    }
18✔
53
    case "integer": {
44!
54
      return ""
×
55
    }
×
56
    case "number": {
44!
57
      return 0
×
58
    }
×
59
    case "boolean": {
44!
60
      return false
×
61
    }
×
62
    case "array": {
44✔
63
      return []
9✔
64
    }
9✔
65
    case "null": {
44!
66
      return null
×
67
    }
×
68

69
    default: {
44!
70
      console.error(`
×
71
      No initial value parsing support for type ${object["type"]} yet.
×
72

73
      Pretty printed version of object with unsupported type:
74
      ${JSON.stringify(object, null, 2)}
×
75
      `)
×
76
      break
×
77
    }
×
78
  }
44✔
79
}
44✔
80

81
const rawSchemaModules = import.meta.glob("../schemas/**/*.json")
1✔
82
const rawDataModules = import.meta.glob("../data/**/*.json")
1✔
83

84
const getModules = (rawModules: Record<string, () => Promise<unknown>>) =>
1✔
85
  Object.fromEntries(Object.entries(rawModules).map(([k, v]) => [k.toLowerCase(), v]))
11✔
86

87
async function loadJSONFile<T>(
11✔
88
  rawModules: Record<string, () => Promise<unknown>>,
11✔
89
  basePath: string,
11✔
90
  filePath: string,
11✔
91
  errorMessage: string
11✔
92
): Promise<T> {
11✔
93
  const key = `${basePath}/${filePath}.json`.toLowerCase()
11✔
94
  const modules = getModules(rawModules)
11✔
95
  const loader = modules[key]
11✔
96
  if (!loader) {
11!
NEW
97
    throw new Error(`${errorMessage}: ${basePath}/${filePath}.json`)
×
NEW
98
  }
×
99
  const module = (await loader()) as { default: T }
11✔
100
  return module.default
10✔
101
}
10✔
102

103
export async function loadJSONSchema(schemaPath: string): Promise<FormObject> {
9✔
104
  return loadJSONFile<FormObject>(
9✔
105
    rawSchemaModules,
9✔
106
    "../schemas",
9✔
107
    schemaPath,
9✔
108
    "JSON Schema not found"
9✔
109
  )
9✔
110
}
9✔
111

112
export async function loadJSONData<T>(dataPath: string): Promise<T> {
2✔
113
  return loadJSONFile<T>(rawDataModules, "../data", dataPath, "JSON Data file not found")
2✔
114
}
2✔
115

116
export function validateJSONSchema(schema: FormObject) {
1✔
117
  const ajv = new Ajv2020()
6✔
118
  const isValid = ajv.validateSchema(schema)
6✔
119
  if (!isValid) console.error(`Invalid JSON schema: ${ajv.errorsText(ajv.errors)}`)
6!
120
}
6✔
121

122
export function validateJSONData<T>(schema: FormObject, data: T) {
1✔
123
  const ajv = new Ajv2020()
2✔
124
  const validate = ajv.compile(schema)
2✔
125
  if (!validate(data)) console.error("Invalid JSON data", data)
2!
126
}
2✔
127

128
export const localizeSchema = (translationKey, namespace, baseSchema, t) => {
1✔
129
  const translationMap = t(`${translationKey}`, { ns: namespace, returnObjects: true }) || {}
13!
130
  const customizer = (objValue, srcValue, key) => {
13✔
131
    // Common keys that would need translation, can be added more.
132
    const translatableKeys = ["title", "description", "enum"]
260✔
133
    if (translatableKeys.includes(key)) return srcValue
260✔
134
    return undefined
210✔
135
  }
260✔
136
  return mergeWith({}, baseSchema, translationMap, customizer)
13✔
137
}
13✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc