adapt-octopus 0.1.1

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/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = tab
5
+ tab_width = 2
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ node_modules
package/.eslintrc.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "node": true,
5
+ "commonjs": false
6
+ },
7
+ "extends": [
8
+ "standard"
9
+ ],
10
+ "parserOptions": {
11
+ "ecmaVersion": 2022
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "npm" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # 🐙 adapt-octopus
2
+
3
+ Utility for converting old Adapt schema from the non-conformant `properties.schema` style into the new conformant JSON schema format.
4
+
5
+ ## Command-line
6
+
7
+ The utility can be used directly from a command line. See below for details.
8
+
9
+ ### Installation
10
+
11
+ Note: requires [Node.js](http://nodejs.org) to be installed.
12
+
13
+ From the command line, run:
14
+
15
+ ```console
16
+ npm install -g adapt-security/adapt-octopus
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ To convert a single schema, simply run the following:
22
+ ```console
23
+ adapt-octopus <inputPath> <id>
24
+ ```
25
+ * ID should match the value of the _component/extension/menu/theme_ attribute in a plugin’s bower.json.
26
+
27
+ To convert all schemas in a framework source repository, run:
28
+ ```console
29
+ adapt-octopus <inputPath>
30
+ ```
31
+
32
+ ## Programmatic
33
+
34
+ The utility also exports a Node.js API for use programatically.
35
+
36
+ ```js
37
+ import octopus from 'adapt-security/adapt-octopus'
38
+ // to run for a single schema, use the following (returns a promise)
39
+ octopus.run(options);
40
+ // to run for multiple schemas, use the following (returns a promise)
41
+ octopus.runRecursive(options);
42
+ ```
43
+
44
+ ### Options
45
+ The following options can be passed to the run functions:
46
+ - **cwd**: the current working directory (used when searching and writing files)
47
+ - **inputPath**: _required when calling `run`_ should be the path to the input schema
48
+ - **inputId**: _required when calling `run`_ the type of the schema being converted (accepted values: `component`, `extension`, `menu`, `theme`)
49
+ - **logger**: an instance to a logger to be used when logging status messages (must export a `log` function)
package/bin/cli.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import Octopus from '../lib/Octopus.js'
4
+
5
+ async function run () {
6
+ const opts = { cwd: path.resolve(process.argv[2]), inputId: process.argv[3] }
7
+ process.argv.length === 4
8
+ ? await Octopus.run(opts)
9
+ : await Octopus.runRecursive(opts)
10
+ }
11
+
12
+ run()
package/lib/Octopus.js ADDED
@@ -0,0 +1,117 @@
1
+ import { EOL } from 'os'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import SchemaNode from './SchemaNode.js'
5
+
6
+ export default class Octopus {
7
+ static async run (opts) {
8
+ await new Octopus(opts).start()
9
+ }
10
+
11
+ static async runRecursive (opts) {
12
+ const _recurse = async pluginDir => {
13
+ const hasNewSchemas = (await fs.readdir(path.join(pluginDir, 'schema'))).some(f => f.endsWith('.schema.json'))
14
+ if(hasNewSchemas && opts.force !== true) {
15
+ return
16
+ }
17
+ const bowerJson = JSON.parse(await fs.readFile(path.join(pluginDir, 'bower.json')))
18
+ const inputId = bowerJson.component || bowerJson.extension || bowerJson.menu || bowerJson.theme
19
+ const octopus = new Octopus({ ...opts, cwd: pluginDir, inputId })
20
+ await octopus.start()
21
+ }
22
+ const { name } = JSON.parse(await fs.readFile(path.join(opts.cwd, 'package.json')))
23
+ if (name !== 'adapt_framework') {
24
+ return await _recurse(opts.cwd)
25
+ }
26
+ await Promise.all(['components', 'extensions', 'menu', 'theme'].map(async f => {
27
+ try {
28
+ const dir = path.join(opts.cwd, 'src', f)
29
+ const contents = await fs.readdir(dir)
30
+ return await Promise.all(contents.map(async c => _recurse(path.join(dir, c))))
31
+ } catch (e) {}
32
+ }))
33
+ }
34
+
35
+ logger
36
+ inputPath
37
+ outputPath
38
+ inputId
39
+ inputSchema
40
+ outputSchema
41
+
42
+ constructor ({ inputPath = 'properties.schema', inputId, cwd, logger = console }) {
43
+ this.cwd = cwd || path.basename(inputPath)
44
+ this.inputPath = path.resolve(cwd, inputPath)
45
+ this.inputId = inputId
46
+ this.logger = logger
47
+ }
48
+
49
+ async start () {
50
+ if (!this.inputPath) throw (new Error('No input path specified'))
51
+ if (!this.inputId) throw (new Error('No ID specified'))
52
+
53
+ this.inputSchema = JSON.parse(await fs.readFile(this.inputPath, 'utf8'))
54
+ await this.convert()
55
+ }
56
+
57
+ async convert () {
58
+ const properties = this.inputSchema.properties
59
+ if (this.inputSchema.$ref === 'http://localhost/plugins/content/component/model.schema') {
60
+ await this.construct('course', {})
61
+ await this.construct('component')
62
+ return
63
+ }
64
+ if (this.inputSchema.$ref === 'http://localhost/plugins/content/theme/model.schema') {
65
+ await this.construct('theme', { properties: properties.variables })
66
+ }
67
+ if (properties?.pluginLocations) return await this.iterateLocations()
68
+ await this.construct(path.basename(this.inputPath, '.model.schema'))
69
+ }
70
+
71
+ async iterateLocations () {
72
+ const locations = this.inputSchema.properties.pluginLocations.properties
73
+
74
+ for (const location of Object.entries(locations)) {
75
+ await this.construct(...location)
76
+ }
77
+
78
+ // ensure any globals are converted
79
+ if (!Object.keys(locations).includes('course')) {
80
+ await this.construct('course', {})
81
+ }
82
+ }
83
+
84
+ async construct (type, schema = this.inputSchema) {
85
+ const properties = schema.properties
86
+
87
+ if (type !== 'course' || !(schema.globals || (schema.globals = this.inputSchema.globals))) {
88
+ if (!properties || !Object.keys(properties).length) return
89
+ delete schema.globals
90
+ }
91
+
92
+ this.outputSchema = new SchemaNode({
93
+ nodeType: 'root',
94
+ schemaType: type,
95
+ inputId: this.inputId,
96
+ inputSchema: schema,
97
+ logger: this.logger
98
+ })
99
+
100
+ this.outputPath = path.resolve(this.cwd, `schema/${type}.schema.json`)
101
+ await this.write()
102
+ }
103
+
104
+ async write () {
105
+ try {
106
+ return await fs.readFile(this.outputPath)
107
+ // return this.logger.log(`JSON schema already exists at ${this.outputPath}, exiting`);
108
+ } catch (e) {
109
+ // carry on
110
+ }
111
+ const json = JSON.stringify(this.outputSchema, null, 2) + EOL
112
+
113
+ await fs.mkdir(path.dirname(this.outputPath), { recursive: true })
114
+ await fs.writeFile(this.outputPath, json)
115
+ this.logger.log(`converted JSON schema written to ${this.outputPath}`)
116
+ }
117
+ }
@@ -0,0 +1,328 @@
1
+ import stripObject from '../utils/stripObject.js'
2
+
3
+ export default class SchemaNode {
4
+ inputId
5
+ inputSchema
6
+
7
+ constructor (options) {
8
+ this.inputId = options.inputId
9
+ this.inputSchema = options.inputSchema
10
+ this.logger = options.logger
11
+
12
+ switch (options.nodeType) {
13
+ case 'root': {
14
+ const type = options.schemaType
15
+ const isCore = type === this.inputId
16
+ const isComponent = type === 'component'
17
+ const isExtension = (!isCore || type !== 'config') && type !== 'theme'
18
+
19
+ let schemaInner = {
20
+ required: this.getRequiredFields(),
21
+ properties: this.getProperties()
22
+ }
23
+
24
+ if (isExtension) {
25
+ schemaInner = {
26
+ [isCore || isComponent ? '$merge' : '$patch']: {
27
+ source: { $ref: isCore ? 'content' : type },
28
+ with: schemaInner
29
+ }
30
+ }
31
+ }
32
+ return {
33
+ $anchor: isCore ? type : `${this.inputId}-${type}`,
34
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
35
+ type: 'object',
36
+ ...schemaInner
37
+ }
38
+ }
39
+ case 'properties':
40
+ return {
41
+ type: this.inputSchema.type,
42
+ isObjectId: this.getIsObjectId(),
43
+ title: this.getTitle(options.key),
44
+ description: this.getDescription(),
45
+ default: this.getDefault(),
46
+ enum: this.getEnumeratedValues(),
47
+ required: this.getRequiredFields(),
48
+ items: this.getItems(),
49
+ properties: this.getProperties(),
50
+ _adapt: this.getAdaptOptions(),
51
+ _backboneForms: this.getBackboneFormsOptions(),
52
+ _unrecognisedFields: this.getUnrecognisedFields()
53
+ }
54
+ case 'items':{
55
+ const properties = this.getItemsProperties()
56
+
57
+ return {
58
+ type: properties ? this.getType() : 'object',
59
+ isObjectId: this.getIsObjectId(),
60
+ properties
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ getItemsProperties () {
67
+ const originalItems = this.inputSchema.properties
68
+
69
+ if (!originalItems) return
70
+
71
+ return Object.entries(originalItems).reduce((a, [key, inputSchema]) => {
72
+ a[key] = new SchemaNode({ nodeType: 'properties', key, inputSchema, logger: this.logger })
73
+
74
+ return a
75
+ }, {})
76
+ }
77
+
78
+ getProperties () {
79
+ const originalProperties = this.inputSchema.properties
80
+ const originalGlobals = this.inputSchema.globals
81
+
82
+ if (!originalProperties && !originalGlobals) return
83
+
84
+ const globals = {}
85
+ const properties = {}
86
+
87
+ if (originalGlobals) {
88
+ for (const [key, inputSchema] of Object.entries(originalGlobals)) {
89
+ globals[key] = new SchemaNode({
90
+ nodeType: 'properties',
91
+ key,
92
+ inputSchema,
93
+ logger: this.logger
94
+ })
95
+ }
96
+
97
+ properties._globals = {
98
+ type: 'object',
99
+ default: {},
100
+ properties: {
101
+ [`_${this.inputId}`]: {
102
+ type: 'object',
103
+ default: {},
104
+ properties: globals
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ if (originalProperties) {
111
+ for (const [key, inputSchema] of Object.entries(originalProperties)) {
112
+ properties[key] = new SchemaNode({
113
+ nodeType: 'properties',
114
+ key,
115
+ inputSchema,
116
+ logger: this.logger
117
+ })
118
+ }
119
+ }
120
+
121
+ return properties
122
+ }
123
+
124
+ getType () {
125
+ const type = this.inputSchema.type
126
+
127
+ return type === 'objectid' ? 'string' : type
128
+ }
129
+
130
+ getTitle (key) {
131
+ const { title, legend } = this.inputSchema
132
+
133
+ if (title) return title
134
+ if (legend) return legend
135
+ if (key === 'pluginLocations') return
136
+
137
+ key = key.replace(/_/g, '').replace(/[A-Z]/g, ' $&').toLowerCase()
138
+
139
+ return key.charAt(0).toUpperCase() + key.slice(1)
140
+ }
141
+
142
+ getDescription () {
143
+ const help = this.inputSchema.help
144
+
145
+ if (help) return help
146
+ }
147
+
148
+ getDefault () {
149
+ if (this.getIsObjectId()) return
150
+
151
+ const originalDefault = this.inputSchema.default
152
+
153
+ if (originalDefault !== undefined) return originalDefault
154
+ if (this.inputSchema.required || this.inputSchema.validators?.includes('required')) return
155
+
156
+ switch (this.inputSchema.type) {
157
+ case 'string':
158
+ return ''
159
+ case 'number':
160
+ return 0
161
+ case 'object':
162
+ return {}
163
+ case 'array':
164
+ if (!this.inputSchema.items) return []
165
+ break
166
+ case 'boolean':
167
+ return false
168
+ }
169
+ }
170
+
171
+ getEnumeratedValues () {
172
+ const { originalEnum, inputType } = this.inputSchema
173
+
174
+ if (originalEnum) return originalEnum
175
+ if (inputType?.type === 'Select') return inputType.options
176
+ }
177
+
178
+ getRequiredFields () {
179
+ const properties = this.inputSchema.properties
180
+
181
+ if (!properties) return
182
+
183
+ const requiredFields = Object.entries(properties).reduce((a, [key, value]) => {
184
+ if ((value.required === true || value.validators?.includes('required')) && value?.default === undefined) a.push(key)
185
+
186
+ return a
187
+ }, [])
188
+
189
+ if (!requiredFields.length) return
190
+
191
+ return requiredFields
192
+ }
193
+
194
+ getItems () {
195
+ const items = this.inputSchema.items
196
+
197
+ if (items) return new SchemaNode({ nodeType: 'items', inputSchema: items, logger: this.logger })
198
+ }
199
+
200
+ getIsObjectId () {
201
+ const inputType = this.inputSchema.inputType
202
+ const isAsset = (inputType?.type || inputType)?.startsWith('Asset')
203
+
204
+ if (isAsset || this.inputSchema.type === 'objectid') return true
205
+ }
206
+
207
+ getAdaptOptions () {
208
+ return stripObject({
209
+ editorOnly: this.inputSchema.editorOnly,
210
+ isSetting: this.inputSchema.isSetting,
211
+ translatable: this.inputSchema.translatable
212
+ })
213
+ }
214
+
215
+ getBackboneFormsOptions () {
216
+ const inputType = this.inputSchema.inputType
217
+
218
+ const getEditor = () => {
219
+ const type = this.inputSchema.type
220
+
221
+ const recognisedTypes = [
222
+ 'string',
223
+ 'number',
224
+ 'object',
225
+ 'array',
226
+ 'boolean',
227
+ 'objectid'
228
+ ]
229
+
230
+ const editor = options.type || inputType
231
+
232
+ if (!recognisedTypes.includes(type)) {
233
+ this.logger.log(`Unrecognised type => ${type}`)
234
+ }
235
+
236
+ if (editor === 'QuestionButton' ||
237
+ (type === 'string' && editor === 'Text') ||
238
+ (type === 'number' && editor === 'Number') ||
239
+ (type === 'boolean' && editor === 'Checkbox')) {
240
+ return
241
+ }
242
+
243
+ return editor
244
+ }
245
+
246
+ const getValidators = () => {
247
+ let validators = this.inputSchema.validators
248
+
249
+ if (!validators) return
250
+
251
+ validators = this.inputSchema.validators.filter(validator => {
252
+ return validator === 'number'
253
+ ? inputType !== 'Number'
254
+ : validator !== 'required'
255
+ })
256
+
257
+ if (!validators.length) return
258
+
259
+ return validators
260
+ }
261
+
262
+ let options = typeof inputType === 'object' ? inputType : {}
263
+
264
+ Object.assign(options, {
265
+ type: getEditor(),
266
+ titleHTML: this.inputSchema.titleHTML,
267
+ validators: getValidators(),
268
+ editorClass: this.inputSchema.editorClass,
269
+ editorAttrs: this.inputSchema.editorAttrs,
270
+ fieldClass: this.inputSchema.fieldClass,
271
+ fieldAttrs: this.inputSchema.fieldAttrs,
272
+ confirmDelete: this.inputSchema.confirmDelete
273
+ })
274
+
275
+ const splitTypes = options.type?.split(':')
276
+
277
+ switch (splitTypes?.length > 1 && splitTypes[0]) {
278
+ case 'Asset':
279
+ options.type = { type: 'Asset', media: splitTypes[1] }
280
+ break
281
+ case 'CodeEditor':
282
+ options.type = { type: 'CodeEditor', mode: splitTypes[1] }
283
+ }
284
+
285
+ if (options.type === 'Select') delete options.options
286
+
287
+ options = stripObject(options)
288
+
289
+ return (options && Object.keys(options).length === 1 && options.type) || options
290
+ }
291
+
292
+ getUnrecognisedFields () {
293
+ const recognisedKeys = [
294
+ 'confirmDelete',
295
+ 'default',
296
+ 'description',
297
+ 'editorAttrs',
298
+ 'editorClass',
299
+ 'editorOnly',
300
+ 'enum',
301
+ 'fieldAttrs',
302
+ 'fieldClass',
303
+ 'help',
304
+ 'inputType',
305
+ 'items',
306
+ 'isSetting',
307
+ 'legend',
308
+ 'properties',
309
+ 'required',
310
+ 'title',
311
+ 'titleHTML',
312
+ 'translatable',
313
+ 'type',
314
+ 'validators'
315
+ ]
316
+
317
+ const unrecognisedFields = {}
318
+
319
+ for (const [key, value] of Object.entries(this.inputSchema)) {
320
+ if (recognisedKeys.includes(key)) continue
321
+
322
+ this.logger.log(`Unrecognised field => "${key}": ${JSON.stringify(value)}`)
323
+ unrecognisedFields[key] = value
324
+ }
325
+
326
+ return stripObject(unrecognisedFields)
327
+ }
328
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "adapt-octopus",
3
+ "version": "0.1.1",
4
+ "description": "Convert old Adapt schema into conformant JSON schema",
5
+ "type": "module",
6
+ "main": "./lib/Octopus.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tomgreenfield/adapt-octopus.git"
10
+ },
11
+ "license": "GPL-3.0",
12
+ "bugs": {
13
+ "url": "https://github.com/tomgreenfield/adapt-octopus/issues"
14
+ },
15
+ "homepage": "https://github.com/tomgreenfield/adapt-octopus#readme",
16
+ "bin": "./bin/cli.js",
17
+ "devDependencies": {
18
+ "eslint": "^8.57.1",
19
+ "eslint-config-standard": "^17.1.0",
20
+ "eslint-plugin-import": "^2.31.0",
21
+ "eslint-plugin-node": "^11.1.0",
22
+ "eslint-plugin-promise": "^6.6.0"
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ export default function stripObject (object) {
2
+ Object.keys(object).forEach(i => object[i] === undefined && delete object[i])
3
+
4
+ if (Object.keys(object).length) return object
5
+ };