adapt-octopus 0.1.2 → 0.2.0
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/.github/workflows/new.yml +1 -1
- package/.github/workflows/tests.yml +15 -0
- package/package.json +4 -1
- package/tests/Octopus.spec.js +349 -0
- package/tests/SchemaNode.spec.js +514 -0
- package/tests/data/sample-schema.json +29 -0
- package/tests/stripObject.spec.js +65 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
on: push
|
|
3
|
+
jobs:
|
|
4
|
+
default:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@master
|
|
10
|
+
- uses: actions/setup-node@master
|
|
11
|
+
with:
|
|
12
|
+
node-version: 'lts/*'
|
|
13
|
+
cache: 'npm'
|
|
14
|
+
- run: npm ci
|
|
15
|
+
- run: npm test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-octopus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Convert old Adapt schema into conformant JSON schema",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/Octopus.js",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/adapt-security/adapt-octopus#readme",
|
|
16
16
|
"bin": "./bin/cli.js",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test 'tests/**/*.spec.js'"
|
|
19
|
+
},
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"@semantic-release/git": "^10.0.1",
|
|
19
22
|
"conventional-changelog-eslint": "^6.0.0",
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import Octopus from '../lib/Octopus.js'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const testDir = join(__dirname, 'temp-test-data')
|
|
10
|
+
|
|
11
|
+
describe('Octopus', () => {
|
|
12
|
+
before(() => {
|
|
13
|
+
// Create test directory
|
|
14
|
+
if (existsSync(testDir)) {
|
|
15
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
16
|
+
}
|
|
17
|
+
mkdirSync(testDir, { recursive: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
after(() => {
|
|
21
|
+
// Clean up test directory
|
|
22
|
+
if (existsSync(testDir)) {
|
|
23
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('constructor', () => {
|
|
28
|
+
it('should create an instance with required options', () => {
|
|
29
|
+
const octopus = new Octopus({
|
|
30
|
+
inputPath: 'properties.schema',
|
|
31
|
+
inputId: 'test-component',
|
|
32
|
+
cwd: testDir
|
|
33
|
+
})
|
|
34
|
+
assert.ok(octopus)
|
|
35
|
+
assert.equal(octopus.inputId, 'test-component')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should use console as default logger', () => {
|
|
39
|
+
const octopus = new Octopus({
|
|
40
|
+
inputPath: 'properties.schema',
|
|
41
|
+
inputId: 'test-component',
|
|
42
|
+
cwd: testDir
|
|
43
|
+
})
|
|
44
|
+
assert.equal(octopus.logger, console)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should accept custom logger', () => {
|
|
48
|
+
const customLogger = { log: () => {} }
|
|
49
|
+
const octopus = new Octopus({
|
|
50
|
+
inputPath: 'properties.schema',
|
|
51
|
+
inputId: 'test-component',
|
|
52
|
+
cwd: testDir,
|
|
53
|
+
logger: customLogger
|
|
54
|
+
})
|
|
55
|
+
assert.equal(octopus.logger, customLogger)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('#start()', () => {
|
|
60
|
+
it('should throw error if no input path specified', async () => {
|
|
61
|
+
const octopus = new Octopus({
|
|
62
|
+
inputId: 'test-component',
|
|
63
|
+
cwd: testDir
|
|
64
|
+
})
|
|
65
|
+
octopus.inputPath = null
|
|
66
|
+
await assert.rejects(
|
|
67
|
+
() => octopus.start(),
|
|
68
|
+
{ message: 'No input path specified' }
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should throw error if no ID specified', async () => {
|
|
73
|
+
const inputPath = join(testDir, 'test.schema')
|
|
74
|
+
writeFileSync(inputPath, JSON.stringify({ properties: {} }))
|
|
75
|
+
const octopus = new Octopus({
|
|
76
|
+
inputPath,
|
|
77
|
+
cwd: testDir
|
|
78
|
+
})
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
() => octopus.start(),
|
|
81
|
+
{ message: 'No ID specified' }
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should load and parse input schema', async () => {
|
|
86
|
+
const inputPath = join(testDir, 'test-load.schema')
|
|
87
|
+
const schema = {
|
|
88
|
+
properties: {
|
|
89
|
+
title: { type: 'string' }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
93
|
+
|
|
94
|
+
const logs = []
|
|
95
|
+
const octopus = new Octopus({
|
|
96
|
+
inputPath,
|
|
97
|
+
inputId: 'test-component',
|
|
98
|
+
cwd: testDir,
|
|
99
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await octopus.start()
|
|
103
|
+
assert.deepEqual(octopus.inputSchema, schema)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('#convert()', () => {
|
|
108
|
+
it('should handle component schema', async () => {
|
|
109
|
+
const inputPath = join(testDir, 'component.schema')
|
|
110
|
+
const schema = {
|
|
111
|
+
$ref: 'http://localhost/plugins/content/component/model.schema',
|
|
112
|
+
properties: {
|
|
113
|
+
title: { type: 'string' }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
117
|
+
|
|
118
|
+
const logs = []
|
|
119
|
+
const octopus = new Octopus({
|
|
120
|
+
inputPath,
|
|
121
|
+
inputId: 'test-component',
|
|
122
|
+
cwd: testDir,
|
|
123
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await octopus.start()
|
|
127
|
+
|
|
128
|
+
// Check that component schema was created (course schema won't be created with empty properties)
|
|
129
|
+
const componentSchemaPath = join(testDir, 'schema', 'component.schema.json')
|
|
130
|
+
assert.ok(existsSync(componentSchemaPath))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should handle theme schema', async () => {
|
|
134
|
+
const inputPath = join(testDir, 'theme.schema')
|
|
135
|
+
const schema = {
|
|
136
|
+
$ref: 'http://localhost/plugins/content/theme/model.schema',
|
|
137
|
+
properties: {
|
|
138
|
+
variables: {
|
|
139
|
+
primaryColor: { type: 'string' }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
144
|
+
|
|
145
|
+
const logs = []
|
|
146
|
+
const octopus = new Octopus({
|
|
147
|
+
inputPath,
|
|
148
|
+
inputId: 'test-theme',
|
|
149
|
+
cwd: testDir,
|
|
150
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await octopus.start()
|
|
154
|
+
|
|
155
|
+
const themeSchemaPath = join(testDir, 'schema', 'theme.schema.json')
|
|
156
|
+
assert.ok(existsSync(themeSchemaPath))
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('#construct()', () => {
|
|
161
|
+
it('should create output schema with SchemaNode', async () => {
|
|
162
|
+
const inputPath = join(testDir, 'construct-test.schema')
|
|
163
|
+
const schema = {
|
|
164
|
+
properties: {
|
|
165
|
+
title: { type: 'string', title: 'Title' },
|
|
166
|
+
body: { type: 'string', title: 'Body' }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
170
|
+
|
|
171
|
+
const logs = []
|
|
172
|
+
const octopus = new Octopus({
|
|
173
|
+
inputPath,
|
|
174
|
+
inputId: 'test-component',
|
|
175
|
+
cwd: testDir,
|
|
176
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
octopus.inputSchema = schema
|
|
180
|
+
await octopus.construct('component', schema)
|
|
181
|
+
|
|
182
|
+
assert.ok(octopus.outputSchema)
|
|
183
|
+
assert.equal(octopus.outputSchema.$anchor, 'test-component-component')
|
|
184
|
+
assert.equal(octopus.outputSchema.$schema, 'https://json-schema.org/draft/2020-12/schema')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should handle empty properties', async () => {
|
|
188
|
+
const inputPath = join(testDir, 'empty-test.schema')
|
|
189
|
+
const schema = { properties: {} }
|
|
190
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
191
|
+
|
|
192
|
+
const logs = []
|
|
193
|
+
const octopus = new Octopus({
|
|
194
|
+
inputPath,
|
|
195
|
+
inputId: 'test-component',
|
|
196
|
+
cwd: testDir,
|
|
197
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
octopus.inputSchema = schema
|
|
201
|
+
await octopus.construct('component', schema)
|
|
202
|
+
|
|
203
|
+
// Should not create output for empty properties
|
|
204
|
+
assert.equal(octopus.outputSchema, undefined)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should handle course type with globals', async () => {
|
|
208
|
+
const inputPath = join(testDir, 'globals-test.schema')
|
|
209
|
+
const schema = {
|
|
210
|
+
properties: {
|
|
211
|
+
_id: { type: 'string' }
|
|
212
|
+
},
|
|
213
|
+
globals: {
|
|
214
|
+
title: { type: 'string' }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
218
|
+
|
|
219
|
+
const logs = []
|
|
220
|
+
const octopus = new Octopus({
|
|
221
|
+
inputPath,
|
|
222
|
+
inputId: 'test-component',
|
|
223
|
+
cwd: testDir,
|
|
224
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
octopus.inputSchema = schema
|
|
228
|
+
await octopus.construct('course', schema)
|
|
229
|
+
|
|
230
|
+
assert.ok(octopus.outputSchema)
|
|
231
|
+
const properties = octopus.outputSchema.$merge?.with?.properties || octopus.outputSchema.$patch?.with?.properties
|
|
232
|
+
assert.ok(properties)
|
|
233
|
+
assert.ok(properties._globals)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('#write()', () => {
|
|
238
|
+
it('should write output schema to file', async () => {
|
|
239
|
+
const inputPath = join(testDir, 'write-test.schema')
|
|
240
|
+
const schema = {
|
|
241
|
+
properties: {
|
|
242
|
+
title: { type: 'string' }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
246
|
+
|
|
247
|
+
const logs = []
|
|
248
|
+
const octopus = new Octopus({
|
|
249
|
+
inputPath,
|
|
250
|
+
inputId: 'test-component',
|
|
251
|
+
cwd: testDir,
|
|
252
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
octopus.inputSchema = schema
|
|
256
|
+
await octopus.construct('component', schema)
|
|
257
|
+
await octopus.write()
|
|
258
|
+
|
|
259
|
+
const outputPath = join(testDir, 'schema', 'component.schema.json')
|
|
260
|
+
assert.ok(existsSync(outputPath))
|
|
261
|
+
|
|
262
|
+
const output = JSON.parse(readFileSync(outputPath, 'utf8'))
|
|
263
|
+
assert.equal(output.$anchor, 'test-component-component')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should not overwrite existing schema', async () => {
|
|
267
|
+
const inputPath = join(testDir, 'no-overwrite.schema')
|
|
268
|
+
const schema = {
|
|
269
|
+
properties: {
|
|
270
|
+
title: { type: 'string' }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
274
|
+
|
|
275
|
+
const outputPath = join(testDir, 'schema', 'no-overwrite.schema.json')
|
|
276
|
+
mkdirSync(dirname(outputPath), { recursive: true })
|
|
277
|
+
const existingContent = '{"existing": true}'
|
|
278
|
+
writeFileSync(outputPath, existingContent)
|
|
279
|
+
|
|
280
|
+
const logs = []
|
|
281
|
+
const octopus = new Octopus({
|
|
282
|
+
inputPath,
|
|
283
|
+
inputId: 'test-component',
|
|
284
|
+
cwd: testDir,
|
|
285
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
octopus.inputSchema = schema
|
|
289
|
+
await octopus.construct('no-overwrite', schema)
|
|
290
|
+
octopus.outputPath = outputPath
|
|
291
|
+
await octopus.write()
|
|
292
|
+
|
|
293
|
+
// Should keep existing content
|
|
294
|
+
const content = readFileSync(outputPath, 'utf8')
|
|
295
|
+
assert.equal(content, existingContent)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should log message when writing new schema', async () => {
|
|
299
|
+
const inputPath = join(testDir, 'log-test.schema')
|
|
300
|
+
const schema = {
|
|
301
|
+
properties: {
|
|
302
|
+
title: { type: 'string' }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
306
|
+
|
|
307
|
+
const logs = []
|
|
308
|
+
const octopus = new Octopus({
|
|
309
|
+
inputPath,
|
|
310
|
+
inputId: 'test-component',
|
|
311
|
+
cwd: testDir,
|
|
312
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
octopus.inputSchema = schema
|
|
316
|
+
await octopus.construct('log-test', schema)
|
|
317
|
+
await octopus.write()
|
|
318
|
+
|
|
319
|
+
assert.ok(logs.some(log => log.includes('converted JSON schema written to')))
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('.run()', () => {
|
|
324
|
+
it('should be a static method', () => {
|
|
325
|
+
assert.equal(typeof Octopus.run, 'function')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should create instance and start conversion', async () => {
|
|
329
|
+
const inputPath = join(testDir, 'component.model.schema')
|
|
330
|
+
const schema = {
|
|
331
|
+
properties: {
|
|
332
|
+
title: { type: 'string' }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
writeFileSync(inputPath, JSON.stringify(schema))
|
|
336
|
+
|
|
337
|
+
const logs = []
|
|
338
|
+
await Octopus.run({
|
|
339
|
+
inputPath,
|
|
340
|
+
inputId: 'test-component',
|
|
341
|
+
cwd: testDir,
|
|
342
|
+
logger: { log: (msg) => logs.push(msg) }
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const outputPath = join(testDir, 'schema', 'component.schema.json')
|
|
346
|
+
assert.ok(existsSync(outputPath))
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
})
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import SchemaNode from '../lib/SchemaNode.js'
|
|
4
|
+
|
|
5
|
+
describe('SchemaNode', () => {
|
|
6
|
+
describe('constructor with nodeType: root', () => {
|
|
7
|
+
let result
|
|
8
|
+
|
|
9
|
+
before(() => {
|
|
10
|
+
const options = {
|
|
11
|
+
nodeType: 'root',
|
|
12
|
+
schemaType: 'config',
|
|
13
|
+
inputId: 'adapt-test',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
properties: {
|
|
16
|
+
title: { type: 'string', required: true },
|
|
17
|
+
body: { type: 'string' }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
logger: { log: () => {} }
|
|
21
|
+
}
|
|
22
|
+
result = new SchemaNode(options)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should create a root schema with $anchor', () => {
|
|
26
|
+
assert.equal(result.$anchor, 'adapt-test-config')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should have $schema property', () => {
|
|
30
|
+
assert.equal(result.$schema, 'https://json-schema.org/draft/2020-12/schema')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should have type object', () => {
|
|
34
|
+
assert.equal(result.type, 'object')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should have $patch for extension schemas', () => {
|
|
38
|
+
assert.ok(result.$patch)
|
|
39
|
+
assert.equal(result.$patch.source.$ref, 'config')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should have properties in $patch.with', () => {
|
|
43
|
+
assert.ok(result.$patch.with.properties)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('constructor with nodeType: root (core component)', () => {
|
|
48
|
+
let result
|
|
49
|
+
|
|
50
|
+
before(() => {
|
|
51
|
+
const options = {
|
|
52
|
+
nodeType: 'root',
|
|
53
|
+
schemaType: 'component',
|
|
54
|
+
inputId: 'component',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
properties: {
|
|
57
|
+
title: { type: 'string' }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
logger: { log: () => {} }
|
|
61
|
+
}
|
|
62
|
+
result = new SchemaNode(options)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should use $merge for core components', () => {
|
|
66
|
+
assert.ok(result.$merge)
|
|
67
|
+
assert.equal(result.$merge.source.$ref, 'content')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should have correct $anchor for core component', () => {
|
|
71
|
+
assert.equal(result.$anchor, 'component')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('constructor with nodeType: properties', () => {
|
|
76
|
+
let result
|
|
77
|
+
|
|
78
|
+
before(() => {
|
|
79
|
+
const options = {
|
|
80
|
+
nodeType: 'properties',
|
|
81
|
+
key: 'title',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
title: 'Title',
|
|
85
|
+
help: 'The title text',
|
|
86
|
+
default: '',
|
|
87
|
+
translatable: true
|
|
88
|
+
},
|
|
89
|
+
logger: { log: () => {} }
|
|
90
|
+
}
|
|
91
|
+
result = new SchemaNode(options)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should have correct type', () => {
|
|
95
|
+
assert.equal(result.type, 'string')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should have title', () => {
|
|
99
|
+
assert.equal(result.title, 'Title')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should have description from help', () => {
|
|
103
|
+
assert.equal(result.description, 'The title text')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should have default value', () => {
|
|
107
|
+
assert.equal(result.default, '')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should have _adapt options', () => {
|
|
111
|
+
assert.deepEqual(result._adapt, { translatable: true })
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('#getType()', () => {
|
|
116
|
+
it('should return the type for regular types', () => {
|
|
117
|
+
const node = new SchemaNode({
|
|
118
|
+
nodeType: 'properties',
|
|
119
|
+
key: 'test',
|
|
120
|
+
inputSchema: { type: 'string' },
|
|
121
|
+
logger: { log: () => {} }
|
|
122
|
+
})
|
|
123
|
+
assert.equal(node.type, 'string')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should keep objectid type and set isObjectId flag', () => {
|
|
127
|
+
const node = new SchemaNode({
|
|
128
|
+
nodeType: 'properties',
|
|
129
|
+
key: 'test',
|
|
130
|
+
inputSchema: { type: 'objectid' },
|
|
131
|
+
logger: { log: () => {} }
|
|
132
|
+
})
|
|
133
|
+
assert.equal(node.type, 'objectid')
|
|
134
|
+
assert.equal(node.isObjectId, true)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('#getTitle()', () => {
|
|
139
|
+
it('should return title if provided', () => {
|
|
140
|
+
const node = new SchemaNode({
|
|
141
|
+
nodeType: 'properties',
|
|
142
|
+
key: 'test',
|
|
143
|
+
inputSchema: { type: 'string', title: 'Custom Title' },
|
|
144
|
+
logger: { log: () => {} }
|
|
145
|
+
})
|
|
146
|
+
assert.equal(node.title, 'Custom Title')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should return legend if title not provided', () => {
|
|
150
|
+
const node = new SchemaNode({
|
|
151
|
+
nodeType: 'properties',
|
|
152
|
+
key: 'test',
|
|
153
|
+
inputSchema: { type: 'string', legend: 'Legend Text' },
|
|
154
|
+
logger: { log: () => {} }
|
|
155
|
+
})
|
|
156
|
+
assert.equal(node.title, 'Legend Text')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should generate title from key', () => {
|
|
160
|
+
const node = new SchemaNode({
|
|
161
|
+
nodeType: 'properties',
|
|
162
|
+
key: 'displayTitle',
|
|
163
|
+
inputSchema: { type: 'string' },
|
|
164
|
+
logger: { log: () => {} }
|
|
165
|
+
})
|
|
166
|
+
assert.equal(node.title, 'Display title')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should handle underscores in key', () => {
|
|
170
|
+
const node = new SchemaNode({
|
|
171
|
+
nodeType: 'properties',
|
|
172
|
+
key: 'user_name',
|
|
173
|
+
inputSchema: { type: 'string' },
|
|
174
|
+
logger: { log: () => {} }
|
|
175
|
+
})
|
|
176
|
+
assert.equal(node.title, 'Username')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('#getDefault()', () => {
|
|
181
|
+
it('should return explicit default if provided', () => {
|
|
182
|
+
const node = new SchemaNode({
|
|
183
|
+
nodeType: 'properties',
|
|
184
|
+
key: 'test',
|
|
185
|
+
inputSchema: { type: 'string', default: 'custom' },
|
|
186
|
+
logger: { log: () => {} }
|
|
187
|
+
})
|
|
188
|
+
assert.equal(node.default, 'custom')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should return empty string for non-required string', () => {
|
|
192
|
+
const node = new SchemaNode({
|
|
193
|
+
nodeType: 'properties',
|
|
194
|
+
key: 'test',
|
|
195
|
+
inputSchema: { type: 'string' },
|
|
196
|
+
logger: { log: () => {} }
|
|
197
|
+
})
|
|
198
|
+
assert.equal(node.default, '')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should return 0 for non-required number', () => {
|
|
202
|
+
const node = new SchemaNode({
|
|
203
|
+
nodeType: 'properties',
|
|
204
|
+
key: 'test',
|
|
205
|
+
inputSchema: { type: 'number' },
|
|
206
|
+
logger: { log: () => {} }
|
|
207
|
+
})
|
|
208
|
+
assert.equal(node.default, 0)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should return false for non-required boolean', () => {
|
|
212
|
+
const node = new SchemaNode({
|
|
213
|
+
nodeType: 'properties',
|
|
214
|
+
key: 'test',
|
|
215
|
+
inputSchema: { type: 'boolean' },
|
|
216
|
+
logger: { log: () => {} }
|
|
217
|
+
})
|
|
218
|
+
assert.equal(node.default, false)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should return empty object for non-required object', () => {
|
|
222
|
+
const node = new SchemaNode({
|
|
223
|
+
nodeType: 'properties',
|
|
224
|
+
key: 'test',
|
|
225
|
+
inputSchema: { type: 'object' },
|
|
226
|
+
logger: { log: () => {} }
|
|
227
|
+
})
|
|
228
|
+
assert.deepEqual(node.default, {})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should return empty array for non-required array without items', () => {
|
|
232
|
+
const node = new SchemaNode({
|
|
233
|
+
nodeType: 'properties',
|
|
234
|
+
key: 'test',
|
|
235
|
+
inputSchema: { type: 'array' },
|
|
236
|
+
logger: { log: () => {} }
|
|
237
|
+
})
|
|
238
|
+
assert.deepEqual(node.default, [])
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should not return default for required fields', () => {
|
|
242
|
+
const node = new SchemaNode({
|
|
243
|
+
nodeType: 'properties',
|
|
244
|
+
key: 'test',
|
|
245
|
+
inputSchema: { type: 'string', required: true },
|
|
246
|
+
logger: { log: () => {} }
|
|
247
|
+
})
|
|
248
|
+
assert.equal(node.default, undefined)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should not return default for ObjectId fields', () => {
|
|
252
|
+
const node = new SchemaNode({
|
|
253
|
+
nodeType: 'properties',
|
|
254
|
+
key: 'test',
|
|
255
|
+
inputSchema: { type: 'objectid' },
|
|
256
|
+
logger: { log: () => {} }
|
|
257
|
+
})
|
|
258
|
+
assert.equal(node.default, undefined)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('#getIsObjectId()', () => {
|
|
263
|
+
it('should return true for objectid type', () => {
|
|
264
|
+
const node = new SchemaNode({
|
|
265
|
+
nodeType: 'properties',
|
|
266
|
+
key: 'test',
|
|
267
|
+
inputSchema: { type: 'objectid' },
|
|
268
|
+
logger: { log: () => {} }
|
|
269
|
+
})
|
|
270
|
+
assert.equal(node.isObjectId, true)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should return true for Asset inputType', () => {
|
|
274
|
+
const node = new SchemaNode({
|
|
275
|
+
nodeType: 'properties',
|
|
276
|
+
key: 'test',
|
|
277
|
+
inputSchema: { type: 'string', inputType: 'Asset:image' },
|
|
278
|
+
logger: { log: () => {} }
|
|
279
|
+
})
|
|
280
|
+
assert.equal(node.isObjectId, true)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should return undefined for regular types', () => {
|
|
284
|
+
const node = new SchemaNode({
|
|
285
|
+
nodeType: 'properties',
|
|
286
|
+
key: 'test',
|
|
287
|
+
inputSchema: { type: 'string' },
|
|
288
|
+
logger: { log: () => {} }
|
|
289
|
+
})
|
|
290
|
+
assert.equal(node.isObjectId, undefined)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('#getRequiredFields()', () => {
|
|
295
|
+
it('should return required field names', () => {
|
|
296
|
+
const node = new SchemaNode({
|
|
297
|
+
nodeType: 'root',
|
|
298
|
+
schemaType: 'test',
|
|
299
|
+
inputId: 'test',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
properties: {
|
|
302
|
+
title: { type: 'string', required: true },
|
|
303
|
+
body: { type: 'string' },
|
|
304
|
+
subtitle: { type: 'string', validators: ['required'] }
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
logger: { log: () => {} }
|
|
308
|
+
})
|
|
309
|
+
// For root nodes, required is in $merge.with.required (core) or $patch.with.required (extension)
|
|
310
|
+
const required = node.$merge?.with?.required || node.$patch?.with?.required
|
|
311
|
+
assert.deepEqual(required, ['title', 'subtitle'])
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should exclude fields with default values', () => {
|
|
315
|
+
const node = new SchemaNode({
|
|
316
|
+
nodeType: 'root',
|
|
317
|
+
schemaType: 'test',
|
|
318
|
+
inputId: 'test',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
properties: {
|
|
321
|
+
title: { type: 'string', required: true, default: 'test' },
|
|
322
|
+
body: { type: 'string', required: true }
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
logger: { log: () => {} }
|
|
326
|
+
})
|
|
327
|
+
const required = node.$merge?.with?.required || node.$patch?.with?.required
|
|
328
|
+
assert.deepEqual(required, ['body'])
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should return undefined if no required fields', () => {
|
|
332
|
+
const node = new SchemaNode({
|
|
333
|
+
nodeType: 'root',
|
|
334
|
+
schemaType: 'test',
|
|
335
|
+
inputId: 'test',
|
|
336
|
+
inputSchema: {
|
|
337
|
+
properties: {
|
|
338
|
+
title: { type: 'string' },
|
|
339
|
+
body: { type: 'string' }
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
logger: { log: () => {} }
|
|
343
|
+
})
|
|
344
|
+
const required = node.$merge?.with?.required || node.$patch?.with?.required
|
|
345
|
+
assert.equal(required, undefined)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('#getProperties()', () => {
|
|
350
|
+
it('should return undefined if no properties or globals', () => {
|
|
351
|
+
const node = new SchemaNode({
|
|
352
|
+
nodeType: 'root',
|
|
353
|
+
schemaType: 'test',
|
|
354
|
+
inputId: 'test',
|
|
355
|
+
inputSchema: {},
|
|
356
|
+
logger: { log: () => {} }
|
|
357
|
+
})
|
|
358
|
+
const properties = node.$merge?.with?.properties || node.$patch?.with?.properties || node.properties
|
|
359
|
+
assert.equal(properties, undefined)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should convert properties to SchemaNode instances', () => {
|
|
363
|
+
const node = new SchemaNode({
|
|
364
|
+
nodeType: 'root',
|
|
365
|
+
schemaType: 'test',
|
|
366
|
+
inputId: 'test',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
properties: {
|
|
369
|
+
title: { type: 'string' }
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
logger: { log: () => {} }
|
|
373
|
+
})
|
|
374
|
+
const properties = node.$merge?.with?.properties || node.$patch?.with?.properties
|
|
375
|
+
assert.ok(properties.title)
|
|
376
|
+
assert.equal(properties.title.type, 'string')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should handle globals', () => {
|
|
380
|
+
const node = new SchemaNode({
|
|
381
|
+
nodeType: 'root',
|
|
382
|
+
schemaType: 'course',
|
|
383
|
+
inputId: 'test-plugin',
|
|
384
|
+
inputSchema: {
|
|
385
|
+
globals: {
|
|
386
|
+
title: { type: 'string', default: 'Global Title' }
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
logger: { log: () => {} }
|
|
390
|
+
})
|
|
391
|
+
const properties = node.$merge?.with?.properties || node.$patch?.with?.properties
|
|
392
|
+
assert.ok(properties._globals)
|
|
393
|
+
assert.equal(properties._globals.type, 'object')
|
|
394
|
+
assert.ok(properties._globals.properties['_test-plugin'])
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
describe('#getEnumeratedValues()', () => {
|
|
399
|
+
it('should return originalEnum if provided', () => {
|
|
400
|
+
const node = new SchemaNode({
|
|
401
|
+
nodeType: 'properties',
|
|
402
|
+
key: 'test',
|
|
403
|
+
inputSchema: { type: 'string', originalEnum: ['a', 'b', 'c'] },
|
|
404
|
+
logger: { log: () => {} }
|
|
405
|
+
})
|
|
406
|
+
assert.deepEqual(node.enum, ['a', 'b', 'c'])
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should return Select options', () => {
|
|
410
|
+
const node = new SchemaNode({
|
|
411
|
+
nodeType: 'properties',
|
|
412
|
+
key: 'test',
|
|
413
|
+
inputSchema: { type: 'string', inputType: { type: 'Select', options: ['x', 'y'] } },
|
|
414
|
+
logger: { log: () => {} }
|
|
415
|
+
})
|
|
416
|
+
assert.deepEqual(node.enum, ['x', 'y'])
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
describe('#getAdaptOptions()', () => {
|
|
421
|
+
it('should return adapt options', () => {
|
|
422
|
+
const node = new SchemaNode({
|
|
423
|
+
nodeType: 'properties',
|
|
424
|
+
key: 'test',
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: 'string',
|
|
427
|
+
editorOnly: true,
|
|
428
|
+
isSetting: true,
|
|
429
|
+
translatable: true
|
|
430
|
+
},
|
|
431
|
+
logger: { log: () => {} }
|
|
432
|
+
})
|
|
433
|
+
assert.deepEqual(node._adapt, {
|
|
434
|
+
editorOnly: true,
|
|
435
|
+
isSetting: true,
|
|
436
|
+
translatable: true
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('should strip undefined values', () => {
|
|
441
|
+
const node = new SchemaNode({
|
|
442
|
+
nodeType: 'properties',
|
|
443
|
+
key: 'test',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
translatable: true
|
|
447
|
+
},
|
|
448
|
+
logger: { log: () => {} }
|
|
449
|
+
})
|
|
450
|
+
assert.deepEqual(node._adapt, { translatable: true })
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('constructor with nodeType: items', () => {
|
|
455
|
+
it('should handle items with properties', () => {
|
|
456
|
+
const node = new SchemaNode({
|
|
457
|
+
nodeType: 'items',
|
|
458
|
+
inputSchema: {
|
|
459
|
+
type: 'object',
|
|
460
|
+
properties: {
|
|
461
|
+
label: { type: 'string' },
|
|
462
|
+
value: { type: 'string' }
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
logger: { log: () => {} }
|
|
466
|
+
})
|
|
467
|
+
assert.equal(node.type, 'object')
|
|
468
|
+
assert.ok(node.properties)
|
|
469
|
+
assert.ok(node.properties.label)
|
|
470
|
+
assert.ok(node.properties.value)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('should handle items without properties', () => {
|
|
474
|
+
const node = new SchemaNode({
|
|
475
|
+
nodeType: 'items',
|
|
476
|
+
inputSchema: {},
|
|
477
|
+
logger: { log: () => {} }
|
|
478
|
+
})
|
|
479
|
+
assert.equal(node.type, 'object')
|
|
480
|
+
assert.equal(node.properties, undefined)
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe('#getItems()', () => {
|
|
485
|
+
it('should return items SchemaNode if items exist', () => {
|
|
486
|
+
const node = new SchemaNode({
|
|
487
|
+
nodeType: 'properties',
|
|
488
|
+
key: 'test',
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: 'array',
|
|
491
|
+
items: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
properties: {
|
|
494
|
+
label: { type: 'string' }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
logger: { log: () => {} }
|
|
499
|
+
})
|
|
500
|
+
assert.ok(node.items)
|
|
501
|
+
assert.equal(node.items.type, 'object')
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('should return undefined if no items', () => {
|
|
505
|
+
const node = new SchemaNode({
|
|
506
|
+
nodeType: 'properties',
|
|
507
|
+
key: 'test',
|
|
508
|
+
inputSchema: { type: 'array' },
|
|
509
|
+
logger: { log: () => {} }
|
|
510
|
+
})
|
|
511
|
+
assert.equal(node.items, undefined)
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"title": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"title": "Title",
|
|
7
|
+
"help": "The title of the component",
|
|
8
|
+
"default": "",
|
|
9
|
+
"translatable": true
|
|
10
|
+
},
|
|
11
|
+
"body": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"title": "Body",
|
|
14
|
+
"help": "The main body text",
|
|
15
|
+
"default": "",
|
|
16
|
+
"translatable": true
|
|
17
|
+
},
|
|
18
|
+
"isEnabled": {
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"title": "Is Enabled",
|
|
21
|
+
"default": true
|
|
22
|
+
},
|
|
23
|
+
"count": {
|
|
24
|
+
"type": "number",
|
|
25
|
+
"title": "Count",
|
|
26
|
+
"default": 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import stripObject from '../utils/stripObject.js'
|
|
4
|
+
|
|
5
|
+
describe('stripObject', () => {
|
|
6
|
+
describe('with undefined values', () => {
|
|
7
|
+
it('should remove undefined properties', () => {
|
|
8
|
+
const obj = { a: 1, b: undefined, c: 'test' }
|
|
9
|
+
const result = stripObject(obj)
|
|
10
|
+
assert.deepEqual(result, { a: 1, c: 'test' })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should return undefined if all properties are undefined', () => {
|
|
14
|
+
const obj = { a: undefined, b: undefined }
|
|
15
|
+
const result = stripObject(obj)
|
|
16
|
+
assert.equal(result, undefined)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return the object if no properties are undefined', () => {
|
|
20
|
+
const obj = { a: 1, b: 'test', c: true }
|
|
21
|
+
const result = stripObject(obj)
|
|
22
|
+
assert.deepEqual(result, { a: 1, b: 'test', c: true })
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('with null and falsy values', () => {
|
|
27
|
+
it('should not remove null values', () => {
|
|
28
|
+
const obj = { a: null, b: 1 }
|
|
29
|
+
const result = stripObject(obj)
|
|
30
|
+
assert.deepEqual(result, { a: null, b: 1 })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not remove false values', () => {
|
|
34
|
+
const obj = { a: false, b: 1 }
|
|
35
|
+
const result = stripObject(obj)
|
|
36
|
+
assert.deepEqual(result, { a: false, b: 1 })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should not remove zero values', () => {
|
|
40
|
+
const obj = { a: 0, b: 1 }
|
|
41
|
+
const result = stripObject(obj)
|
|
42
|
+
assert.deepEqual(result, { a: 0, b: 1 })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should not remove empty string values', () => {
|
|
46
|
+
const obj = { a: '', b: 1 }
|
|
47
|
+
const result = stripObject(obj)
|
|
48
|
+
assert.deepEqual(result, { a: '', b: 1 })
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('edge cases', () => {
|
|
53
|
+
it('should handle empty objects', () => {
|
|
54
|
+
const obj = {}
|
|
55
|
+
const result = stripObject(obj)
|
|
56
|
+
assert.equal(result, undefined)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should mutate the original object', () => {
|
|
60
|
+
const obj = { a: 1, b: undefined }
|
|
61
|
+
stripObject(obj)
|
|
62
|
+
assert.deepEqual(obj, { a: 1 })
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
})
|