Skip to content

Commit ec5721a

Browse files
Add and export JSON and YAML file loaders
1 parent 119bcf2 commit ec5721a

3 files changed

Lines changed: 226 additions & 6 deletions

File tree

package-lock.json

Lines changed: 19 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"./file-upload.min.js": "./.public/javascripts/file-upload.min.js",
2222
"./file-upload.min.js.map": "./.public/javascripts/file-upload.min.js.map",
2323
"./application.min.css": "./.public/stylesheets/application.min.css",
24+
"./file-form-service.js": "./.server/server/utils/file-form-service.js",
2425
"./controllers/*": "./.server/server/plugins/engine/pageControllers/*",
2526
"./services/*": "./.server/server/plugins/engine/services/*",
2627
"./package.json": "./package.json"
@@ -103,7 +104,8 @@
103104
"pino": "^9.6.0",
104105
"pino-pretty": "^13.0.0",
105106
"proxy-agent": "^6.5.0",
106-
"resolve": "^1.22.10"
107+
"resolve": "^1.22.10",
108+
"yaml": "^2.7.1"
107109
},
108110
"devDependencies": {
109111
"@babel/cli": "^7.26.4",
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import fs from 'fs/promises'
2+
import crypto from 'node:crypto'
3+
import path from 'node:path'
4+
5+
import Boom from '@hapi/boom'
6+
import YAML from 'yaml'
7+
8+
/**
9+
* Create a deterministic UUID string
10+
* @param {string} seed - the seed string
11+
* @returns string
12+
*/
13+
function uuid(seed) {
14+
const hash = crypto
15+
.createHash('sha256')
16+
.update(seed.toString())
17+
.digest('hex')
18+
.substring(0, 36)
19+
const chars = hash.split('')
20+
21+
chars[8] = '-'
22+
chars[13] = '-'
23+
chars[14] = '4'
24+
chars[18] = '-'
25+
chars[19] = '8'
26+
chars[23] = '-'
27+
28+
return chars.join('')
29+
}
30+
31+
/**
32+
* FileFormService
33+
*/
34+
class FileFormService {
35+
/**
36+
* @type {string}
37+
*/
38+
#ext
39+
40+
/**
41+
* @type {PartialFormMetadata}
42+
*/
43+
#defaultMetadata
44+
45+
/**
46+
* @type {Map<string, FormMetadata>}
47+
*/
48+
#metadata = new Map()
49+
50+
/**
51+
* @type {Map<string, FormDefinition>}
52+
*/
53+
#definition = new Map()
54+
55+
/**
56+
* @param {string} ext - the file type extension
57+
* @param {PartialFormMetadata} metadata - the default partial form metadata to use for all forms
58+
*/
59+
constructor(ext, metadata) {
60+
this.#ext = ext.toLowerCase()
61+
this.#defaultMetadata = metadata
62+
}
63+
64+
/**
65+
* @param {string} dir
66+
* @param {PartialFormMetadata} metadata - the partial metadata to use for this form
67+
*/
68+
async addDir(dir, metadata = this.#defaultMetadata) {
69+
const dirents = await fs.readdir(dir, { withFileTypes: true })
70+
const fileEntries = dirents.filter(
71+
(entry) =>
72+
entry.isFile() &&
73+
path.extname(entry.name).toLowerCase() === `.${this.#ext}`
74+
)
75+
76+
// Read each file
77+
for (const entry of fileEntries) {
78+
await this.addForm(
79+
`${entry.parentPath}${path.sep}${entry.name}`,
80+
metadata
81+
)
82+
}
83+
}
84+
85+
/**
86+
* @param {string} filepath
87+
* @param {PartialFormMetadata} metadata - the metadata to use for this form
88+
*/
89+
async addForm(filepath, metadata = this.#defaultMetadata) {
90+
const definition = await this.readFormDefintion(filepath)
91+
const filename = path.basename(filepath)
92+
const slug = path.basename(filename, `.${this.#ext}`)
93+
const id = uuid(filename)
94+
const title = definition.name ?? slug
95+
const fullMetadata = { ...metadata, id, slug, title }
96+
97+
this.#metadata.set(slug, fullMetadata)
98+
this.#definition.set(id, definition)
99+
}
100+
101+
/**
102+
* @param {string} path
103+
* @returns {Promise<FormDefinition>}
104+
*/
105+
// eslint-disable-next-line @typescript-eslint/require-await
106+
async readFormDefintion(path) {
107+
throw new Error(
108+
`Error reading path '${path}'. 'readFormDefintion' not implemented in abstract class`
109+
)
110+
}
111+
112+
/**
113+
* Get the form metadata by slug
114+
* @param {string} slug
115+
* @returns {FormMetadata}
116+
*/
117+
getFormMetadata(slug) {
118+
const metadata = this.#metadata.get(slug)
119+
120+
if (!metadata) {
121+
throw Boom.notFound(`Form '${slug}' not found`)
122+
}
123+
124+
return metadata
125+
}
126+
127+
/**
128+
* Get the form defintion by id
129+
* @param {string} id
130+
* @returns {FormDefinition}
131+
*/
132+
getFormDefinition(id) {
133+
const definition = this.#definition.get(id)
134+
135+
if (!definition) {
136+
throw Boom.notFound(`Form '${id}' not found`)
137+
}
138+
139+
return definition
140+
}
141+
}
142+
143+
/**
144+
* JsonFileFormService class
145+
* @augments FileFormService
146+
*/
147+
export class JsonFileFormService extends FileFormService {
148+
/**
149+
* @param {FormMetadata} metadata - the default metadata to use for all forms
150+
*/
151+
constructor(metadata) {
152+
super('json', metadata)
153+
}
154+
155+
/**
156+
* @param {string} path
157+
* @returns {Promise<FormDefinition>}
158+
*/
159+
async readFormDefintion(path) {
160+
/**
161+
* @type {FormDefinition}
162+
*/
163+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
164+
const definition = JSON.parse(await fs.readFile(path, 'utf8'))
165+
166+
return definition
167+
}
168+
}
169+
170+
/**
171+
* YamlFileFormService class
172+
* @augments FileFormService
173+
*/
174+
export class YamlFileFormService extends FileFormService {
175+
/**
176+
* @param {FormMetadata} metadata - the default metadata to use for all forms
177+
*/
178+
constructor(metadata) {
179+
super('yaml', metadata)
180+
}
181+
182+
/**
183+
* @param {string} path
184+
* @returns {Promise<FormDefinition>}
185+
*/
186+
async readFormDefintion(path) {
187+
/**
188+
* @type {FormDefinition}
189+
*/
190+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
191+
const definition = YAML.parse(await fs.readFile(path, 'utf8'))
192+
193+
return definition
194+
}
195+
}
196+
197+
/**
198+
* @import { FormMetadata, FormDefinition } from '@defra/forms-model'
199+
*/
200+
201+
/**
202+
* Partial FormMetadata
203+
* @typedef {Omit<FormMetadata, "id" | "slug" | "title">} PartialFormMetadata
204+
*/

0 commit comments

Comments
 (0)