@quazardous/qdadm 2.4.4 → 2.5.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/README.md +2 -0
- package/package.json +1 -1
- package/src/components/editors/VanillaJsonEditor.vue +11 -5
- package/src/composables/useListPage.ts +38 -0
- package/src/composables/useListPage.types.ts +2 -0
- package/src/editors/index.ts +5 -0
- package/src/entity/EntityManager.interface.ts +2 -0
- package/src/entity/EntityManager.ts +3 -4
- package/src/entity/auth/EntityAuthAdapter.ts +10 -0
- package/src/entity/auth/SecurityChecker.ts +6 -13
- package/src/gen/connectors/OpenAPIConnector.test.js +253 -1
- package/src/gen/connectors/OpenAPIConnector.ts +110 -4
- package/src/gen/generateManagers.test.js +78 -0
- package/src/gen/generateManagers.ts +33 -7
- package/src/gen/index.ts +1 -0
- package/src/index.ts +26 -0
- package/src/kernel/Kernel.api.ts +5 -5
- package/src/kernel/Kernel.auth.ts +5 -5
- package/src/kernel/Kernel.i18n.ts +5 -5
- package/src/kernel/Kernel.modules.ts +5 -5
- package/src/kernel/Kernel.registries.ts +5 -5
- package/src/kernel/Kernel.routing.ts +5 -5
- package/src/kernel/Kernel.vue.ts +5 -5
- package/src/kernel/KernelContext.entities.ts +5 -5
- package/src/kernel/KernelContext.events.ts +5 -5
- package/src/kernel/KernelContext.i18n.ts +5 -5
- package/src/kernel/KernelContext.permissions.ts +5 -5
- package/src/kernel/KernelContext.routing.ts +5 -5
- package/src/kernel/KernelContext.types.ts +5 -0
- package/src/kernel/KernelContext.zones.ts +5 -5
- package/src/orchestrator/useOrchestrator.ts +20 -5
- package/src/utils/humanize.ts +26 -0
- package/src/utils/index.ts +3 -0
package/README.md
CHANGED
|
@@ -137,6 +137,8 @@ This is the canonical Vite/Webpack pattern for Vue plugin singletons. The `file:
|
|
|
137
137
|
|
|
138
138
|
Installing qdadm from npm (`@quazardous/qdadm@^1.19.3`) doesn't have this issue: the published tarball ships no `node_modules/`, so peers always resolve from the host. The trap is specific to `file:` / `workspace:` links onto a package that lives inside another monorepo.
|
|
139
139
|
|
|
140
|
+
One more caveat: qdadm ships TypeScript sources, so with a `file:` link your `vue-tsc` / lint / test counters measure qdadm's **working tree**, not a released version. Any local qdadm edit moves your numbers with zero change on your side — don't calibrate quality ratchets or CI thresholds against a linked checkout; pin them to the published package your CI actually installs.
|
|
141
|
+
|
|
140
142
|
## License
|
|
141
143
|
|
|
142
144
|
MIT
|
package/package.json
CHANGED
|
@@ -22,7 +22,13 @@ type JsonValue = Record<string, unknown> | unknown[] | string | null
|
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
modelValue?: JsonValue
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Editor mode. String literals are accepted (#1253) so consumers never
|
|
27
|
+
* need to import the `Mode` enum from vanilla-jsoneditor (which is not
|
|
28
|
+
* a direct dependency of theirs); `Mode` itself is re-exported from
|
|
29
|
+
* '@quazardous/qdadm/editors' for those who want it.
|
|
30
|
+
*/
|
|
31
|
+
mode?: Mode | 'tree' | 'text' | 'table'
|
|
26
32
|
height?: string
|
|
27
33
|
readOnly?: boolean
|
|
28
34
|
mainMenuBar?: boolean
|
|
@@ -40,7 +46,7 @@ interface Props {
|
|
|
40
46
|
|
|
41
47
|
const props = withDefaults(defineProps<Props>(), {
|
|
42
48
|
modelValue: () => ({}),
|
|
43
|
-
mode: 'tree'
|
|
49
|
+
mode: 'tree',
|
|
44
50
|
height: '400px',
|
|
45
51
|
readOnly: false,
|
|
46
52
|
mainMenuBar: true,
|
|
@@ -100,7 +106,7 @@ onMounted(() => {
|
|
|
100
106
|
target: containerRef.value,
|
|
101
107
|
props: {
|
|
102
108
|
content,
|
|
103
|
-
mode: props.mode,
|
|
109
|
+
mode: props.mode as Mode,
|
|
104
110
|
readOnly: props.readOnly,
|
|
105
111
|
mainMenuBar: props.mainMenuBar,
|
|
106
112
|
navigationBar: props.navigationBar,
|
|
@@ -161,9 +167,9 @@ watch(() => props.modelValue, (newVal: JsonValue | undefined) => {
|
|
|
161
167
|
}, { deep: true })
|
|
162
168
|
|
|
163
169
|
// Watch mode changes
|
|
164
|
-
watch(() => props.mode, (newMode
|
|
170
|
+
watch(() => props.mode, (newMode) => {
|
|
165
171
|
if (editor && newMode) {
|
|
166
|
-
editor.updateProps({ mode: newMode })
|
|
172
|
+
editor.updateProps({ mode: newMode as Mode })
|
|
167
173
|
}
|
|
168
174
|
})
|
|
169
175
|
|
|
@@ -35,6 +35,7 @@ import { useEntityItemPage, type ParentConfig, type UseEntityItemPageReturn } fr
|
|
|
35
35
|
import { useActiveStack } from '../chain/useActiveStack.js'
|
|
36
36
|
import { useI18n } from '../i18n/useI18n'
|
|
37
37
|
import { formatDateOnly } from '../utils/formatters'
|
|
38
|
+
import { humanizeFieldName } from '../utils/humanize'
|
|
38
39
|
|
|
39
40
|
// Import types from dedicated types file
|
|
40
41
|
import type {
|
|
@@ -294,6 +295,42 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
294
295
|
}
|
|
295
296
|
})
|
|
296
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Template-side column binding helper (#1255): spread onto a PrimeVue
|
|
300
|
+
* `<Column>` so `field` + `header` come from one source instead of being
|
|
301
|
+
* retyped in every list template:
|
|
302
|
+
*
|
|
303
|
+
* ```html
|
|
304
|
+
* <Column v-bind="list.column('botUuid')" sortable>
|
|
305
|
+
* <template #body="{ data }">...</template>
|
|
306
|
+
* </Column>
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* Header resolution: i18n key (`entities.{entity}.fields.{field}`) >
|
|
310
|
+
* `overrides.header` > inline header given to `addColumn` >
|
|
311
|
+
* `manager.fields[name].label` > humanized field name. Pure read — it
|
|
312
|
+
* never registers into columnsMap, so calling it during render is safe.
|
|
313
|
+
*/
|
|
314
|
+
function column(name: string, overrides: Partial<ColumnConfig> = {}): ColumnConfig {
|
|
315
|
+
// Depend on the locale so headers re-render on locale change.
|
|
316
|
+
void i18nLocale.value
|
|
317
|
+
|
|
318
|
+
let header: string | undefined
|
|
319
|
+
if (entity && kernelI18n) {
|
|
320
|
+
const trace = kernelI18n.resolve(`entities.${entity}.fields.${name}`)
|
|
321
|
+
if (trace.hit) header = trace.result
|
|
322
|
+
}
|
|
323
|
+
const fieldConfig = manager.getFieldConfig?.(name) as { label?: string } | null | undefined
|
|
324
|
+
header ??=
|
|
325
|
+
overrides.header ??
|
|
326
|
+
columnInlineHeaders.get(name) ??
|
|
327
|
+
fieldConfig?.label ??
|
|
328
|
+
humanizeFieldName(name)
|
|
329
|
+
|
|
330
|
+
const registered = columnsMap.value.get(name)
|
|
331
|
+
return { ...registered, ...overrides, field: name, header }
|
|
332
|
+
}
|
|
333
|
+
|
|
297
334
|
function removeColumn(field: string): void {
|
|
298
335
|
columnsMap.value.delete(field)
|
|
299
336
|
}
|
|
@@ -1111,6 +1148,7 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
1111
1148
|
|
|
1112
1149
|
// Columns
|
|
1113
1150
|
columns,
|
|
1151
|
+
column,
|
|
1114
1152
|
addColumn,
|
|
1115
1153
|
removeColumn,
|
|
1116
1154
|
updateColumn,
|
|
@@ -347,6 +347,8 @@ export interface UseListPageReturn<T = unknown> {
|
|
|
347
347
|
|
|
348
348
|
// Columns
|
|
349
349
|
columns: ComputedRef<ColumnConfig[]>
|
|
350
|
+
/** v-bind column binding helper (#1255): field + header from one source */
|
|
351
|
+
column: (name: string, overrides?: Partial<ColumnConfig>) => ColumnConfig
|
|
350
352
|
addColumn: (field: string, config?: Partial<ColumnConfig>) => void
|
|
351
353
|
removeColumn: (field: string) => void
|
|
352
354
|
updateColumn: (field: string, updates: Partial<ColumnConfig>) => void
|
package/src/editors/index.ts
CHANGED
|
@@ -10,3 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
export { default as VanillaJsonEditor } from '../components/editors/VanillaJsonEditor.vue'
|
|
12
12
|
export { default as JsonStructuredField } from '../components/editors/JsonStructuredField.vue'
|
|
13
|
+
|
|
14
|
+
// Re-export the mode enum so consumers who want it don't need to depend on
|
|
15
|
+
// vanilla-jsoneditor directly (#1253). String literals ('tree' | 'text' |
|
|
16
|
+
// 'table') are accepted by VanillaJsonEditor's `mode` prop anyway.
|
|
17
|
+
export { Mode } from 'vanilla-jsoneditor'
|
|
@@ -60,6 +60,8 @@ export interface EntityManagerRead<T = unknown> extends EntityManagerBase<T>, En
|
|
|
60
60
|
request: (method: string, path: string, options?: { data?: unknown }) => Promise<unknown>
|
|
61
61
|
invalidateCache?: () => void
|
|
62
62
|
localFilterThreshold?: number
|
|
63
|
+
/** Field config lookup — read-side too since list column binding (#1255) */
|
|
64
|
+
getFieldConfig?: (name: string) => unknown | null
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// ─── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -34,6 +34,8 @@ import type { NullSortMode } from '../query/clientFilter'
|
|
|
34
34
|
|
|
35
35
|
// Re-export public types from types file
|
|
36
36
|
export type {
|
|
37
|
+
StorageResolution,
|
|
38
|
+
ResolvedStorage,
|
|
37
39
|
RoutingContext,
|
|
38
40
|
PresaveContext,
|
|
39
41
|
PostsaveContext,
|
|
@@ -501,10 +503,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
501
503
|
* Check if SecurityChecker is configured via authAdapter
|
|
502
504
|
*/
|
|
503
505
|
protected _hasSecurityChecker(): boolean {
|
|
504
|
-
return (
|
|
505
|
-
(this.authAdapter as unknown as { _securityChecker?: unknown })
|
|
506
|
-
?._securityChecker != null
|
|
507
|
-
)
|
|
506
|
+
return this.authAdapter?.hasSecurityChecker?.() ?? false
|
|
508
507
|
}
|
|
509
508
|
|
|
510
509
|
/**
|
|
@@ -41,6 +41,16 @@ export class EntityAuthAdapter {
|
|
|
41
41
|
this._securityChecker = checker
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether a SecurityChecker is configured (#1196).
|
|
46
|
+
*
|
|
47
|
+
* Public capability probe — EntityManager used to reach into the
|
|
48
|
+
* private _securityChecker field through a cast.
|
|
49
|
+
*/
|
|
50
|
+
hasSecurityChecker(): boolean {
|
|
51
|
+
return this._securityChecker != null
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* Check if current user is granted an attribute (Symfony-like contract)
|
|
46
56
|
*
|
|
@@ -52,28 +52,24 @@ export interface SecurityCheckerConfig {
|
|
|
52
52
|
*/
|
|
53
53
|
export class SecurityChecker {
|
|
54
54
|
protected _rolesProvider: RoleProvider
|
|
55
|
-
protected _legacyRoleHierarchy: RoleHierarchy | null
|
|
56
55
|
readonly getCurrentUser: () => AuthUser | null
|
|
57
56
|
|
|
58
57
|
constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
|
|
59
|
-
// Support both new rolesProvider and legacy rolePermissions
|
|
60
58
|
if (rolesProvider) {
|
|
61
59
|
this._rolesProvider = rolesProvider
|
|
62
|
-
this._legacyRoleHierarchy = null
|
|
63
60
|
} else {
|
|
64
|
-
// Legacy
|
|
61
|
+
// Legacy options (roleHierarchy/rolePermissions) are normalized ONCE
|
|
62
|
+
// into a static provider (#1196) — the parallel _legacyRoleHierarchy
|
|
63
|
+
// branch is gone; only the provider path remains.
|
|
65
64
|
const hierarchyConfig =
|
|
66
|
-
roleHierarchy instanceof RoleHierarchy
|
|
65
|
+
roleHierarchy instanceof RoleHierarchy
|
|
66
|
+
? roleHierarchy.map
|
|
67
|
+
: (roleHierarchy as RoleHierarchyConfig) || {}
|
|
67
68
|
|
|
68
69
|
this._rolesProvider = new StaticRoleProvider({
|
|
69
70
|
role_hierarchy: hierarchyConfig,
|
|
70
71
|
role_permissions: rolePermissions || {},
|
|
71
72
|
})
|
|
72
|
-
|
|
73
|
-
this._legacyRoleHierarchy =
|
|
74
|
-
roleHierarchy instanceof RoleHierarchy
|
|
75
|
-
? roleHierarchy
|
|
76
|
-
: new RoleHierarchy(hierarchyConfig)
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
this.getCurrentUser = getCurrentUser
|
|
@@ -83,9 +79,6 @@ export class SecurityChecker {
|
|
|
83
79
|
* Get role hierarchy (dynamically resolved from rolesProvider)
|
|
84
80
|
*/
|
|
85
81
|
get roleHierarchy(): RoleHierarchy {
|
|
86
|
-
if (this._legacyRoleHierarchy) {
|
|
87
|
-
return this._legacyRoleHierarchy
|
|
88
|
-
}
|
|
89
82
|
return new RoleHierarchy(this._rolesProvider.getHierarchy())
|
|
90
83
|
}
|
|
91
84
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
2
|
import { OpenAPIConnector } from './OpenAPIConnector'
|
|
3
3
|
import sampleSpec from './__fixtures__/sample-openapi.json'
|
|
4
4
|
|
|
@@ -584,6 +584,258 @@ describe('OpenAPIConnector', () => {
|
|
|
584
584
|
|
|
585
585
|
expect(result.warnings.some(w => w.code === 'UNRESOLVED_REF')).toBe(true)
|
|
586
586
|
})
|
|
587
|
+
|
|
588
|
+
describe('EMPTY_OBJECT_SCHEMA detection (#1240)', () => {
|
|
589
|
+
// Build a spec with a single tasks entity whose `payload` field uses
|
|
590
|
+
// the given schema node.
|
|
591
|
+
function specWithPayload(payloadSchema) {
|
|
592
|
+
return {
|
|
593
|
+
openapi: '3.0.0',
|
|
594
|
+
paths: {
|
|
595
|
+
'/api/tasks/{id}': {
|
|
596
|
+
get: {
|
|
597
|
+
responses: {
|
|
598
|
+
'200': {
|
|
599
|
+
content: {
|
|
600
|
+
'application/json': {
|
|
601
|
+
schema: {
|
|
602
|
+
type: 'object',
|
|
603
|
+
properties: {
|
|
604
|
+
data: {
|
|
605
|
+
type: 'object',
|
|
606
|
+
properties: {
|
|
607
|
+
id: { type: 'string' },
|
|
608
|
+
payload: payloadSchema
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function warningsFor(payloadSchema) {
|
|
624
|
+
const connector = new OpenAPIConnector()
|
|
625
|
+
const result = connector.parseWithWarnings(specWithPayload(payloadSchema))
|
|
626
|
+
return result.warnings.filter(w => w.code === 'EMPTY_OBJECT_SCHEMA')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
it('warns on a bare object schema (no properties, no additionalProperties)', () => {
|
|
630
|
+
const warnings = warningsFor({ type: 'object' })
|
|
631
|
+
expect(warnings).toHaveLength(1)
|
|
632
|
+
expect(warnings[0].path).toBe('/api/tasks/{id}#payload')
|
|
633
|
+
expect(warnings[0].message).toContain('additionalProperties: true')
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('warns when properties is present but empty', () => {
|
|
637
|
+
expect(warningsFor({ type: 'object', properties: {} })).toHaveLength(1)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
it('does not warn when additionalProperties is true', () => {
|
|
641
|
+
expect(warningsFor({ type: 'object', additionalProperties: true })).toHaveLength(0)
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('does not warn when additionalProperties is a sub-schema', () => {
|
|
645
|
+
expect(
|
|
646
|
+
warningsFor({ type: 'object', additionalProperties: { type: 'string' } })
|
|
647
|
+
).toHaveLength(0)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('does not warn when the object declares properties', () => {
|
|
651
|
+
expect(
|
|
652
|
+
warningsFor({ type: 'object', properties: { targets: { type: 'array' } } })
|
|
653
|
+
).toHaveLength(0)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
it('does not warn on composition schemas (oneOf/anyOf/allOf)', () => {
|
|
657
|
+
expect(
|
|
658
|
+
warningsFor({ type: 'object', oneOf: [{ type: 'object', properties: { a: { type: 'string' } } }] })
|
|
659
|
+
).toHaveLength(0)
|
|
660
|
+
expect(
|
|
661
|
+
warningsFor({ type: 'object', allOf: [{ type: 'object', properties: { a: { type: 'string' } } }] })
|
|
662
|
+
).toHaveLength(0)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it('does not warn on non-object types', () => {
|
|
666
|
+
expect(warningsFor({ type: 'string' })).toHaveLength(0)
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
it('still generates the field as a usable object type', () => {
|
|
670
|
+
const connector = new OpenAPIConnector()
|
|
671
|
+
const { schemas } = connector.parseWithWarnings(specWithPayload({ type: 'object' }))
|
|
672
|
+
const tasks = schemas.find(e => e.name === 'tasks')
|
|
673
|
+
expect(tasks.fields.payload).toBeDefined()
|
|
674
|
+
expect(tasks.fields.payload.type).toBe('object')
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('parse() logs collected warnings to console.warn', () => {
|
|
678
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
679
|
+
try {
|
|
680
|
+
const connector = new OpenAPIConnector()
|
|
681
|
+
connector.parse(specWithPayload({ type: 'object' }))
|
|
682
|
+
const calls = warnSpy.mock.calls.map(args => args.join(' '))
|
|
683
|
+
expect(calls.some(line => line.includes('EMPTY') || line.includes('additionalProperties'))).toBe(true)
|
|
684
|
+
expect(calls.some(line => line.includes('/api/tasks/{id}#payload'))).toBe(true)
|
|
685
|
+
} finally {
|
|
686
|
+
warnSpy.mockRestore()
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
describe('inference options (#1255)', () => {
|
|
693
|
+
// /api/bots: GET item + POST with a partial body; lastSeen/uuid are
|
|
694
|
+
// response-only, name is writable, status declares readOnly in-schema.
|
|
695
|
+
const botsSpec = {
|
|
696
|
+
openapi: '3.0.0',
|
|
697
|
+
paths: {
|
|
698
|
+
'/api/bots/{id}': {
|
|
699
|
+
get: {
|
|
700
|
+
responses: {
|
|
701
|
+
'200': {
|
|
702
|
+
content: {
|
|
703
|
+
'application/json': {
|
|
704
|
+
schema: {
|
|
705
|
+
type: 'object',
|
|
706
|
+
properties: {
|
|
707
|
+
data: {
|
|
708
|
+
type: 'object',
|
|
709
|
+
properties: {
|
|
710
|
+
uuid: { type: 'string', format: 'uuid' },
|
|
711
|
+
name: { type: 'string' },
|
|
712
|
+
lastSeen: { type: 'string', format: 'date-time' },
|
|
713
|
+
status: { type: 'string', readOnly: true },
|
|
714
|
+
notes: { type: 'string', description: 'Operator notes' }
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
'/api/bots': {
|
|
726
|
+
post: {
|
|
727
|
+
requestBody: {
|
|
728
|
+
content: {
|
|
729
|
+
'application/json': {
|
|
730
|
+
schema: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
name: { type: 'string' },
|
|
734
|
+
notes: { type: 'string', description: 'Operator notes' }
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
responses: {
|
|
741
|
+
'200': {
|
|
742
|
+
content: {
|
|
743
|
+
'application/json': {
|
|
744
|
+
schema: { type: 'object', properties: { data: { type: 'object', properties: { uuid: { type: 'string' } } } } }
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Read-only entity: no write operation at all
|
|
755
|
+
const logsSpec = {
|
|
756
|
+
openapi: '3.0.0',
|
|
757
|
+
paths: {
|
|
758
|
+
'/api/logs': {
|
|
759
|
+
get: {
|
|
760
|
+
responses: {
|
|
761
|
+
'200': {
|
|
762
|
+
content: {
|
|
763
|
+
'application/json': {
|
|
764
|
+
schema: {
|
|
765
|
+
type: 'object',
|
|
766
|
+
properties: {
|
|
767
|
+
data: {
|
|
768
|
+
type: 'array',
|
|
769
|
+
items: {
|
|
770
|
+
type: 'object',
|
|
771
|
+
properties: {
|
|
772
|
+
id: { type: 'string' },
|
|
773
|
+
message: { type: 'string' }
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
describe('inferLabels', () => {
|
|
789
|
+
it('is off by default (label only from description)', () => {
|
|
790
|
+
const connector = new OpenAPIConnector()
|
|
791
|
+
const bots = connector.parse(botsSpec).find(e => e.name === 'bots')
|
|
792
|
+
|
|
793
|
+
expect(bots.fields.lastSeen.label).toBeUndefined()
|
|
794
|
+
expect(bots.fields.notes.label).toBe('Operator notes')
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('humanizes field names when enabled and description is absent', () => {
|
|
798
|
+
const connector = new OpenAPIConnector({ inferLabels: 'humanize' })
|
|
799
|
+
const bots = connector.parse(botsSpec).find(e => e.name === 'bots')
|
|
800
|
+
|
|
801
|
+
expect(bots.fields.lastSeen.label).toBe('Last Seen')
|
|
802
|
+
expect(bots.fields.uuid.label).toBe('Uuid')
|
|
803
|
+
// description still wins
|
|
804
|
+
expect(bots.fields.notes.label).toBe('Operator notes')
|
|
805
|
+
})
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
describe('inferReadOnly', () => {
|
|
809
|
+
it('is off by default (readOnly only from schema)', () => {
|
|
810
|
+
const connector = new OpenAPIConnector()
|
|
811
|
+
const bots = connector.parse(botsSpec).find(e => e.name === 'bots')
|
|
812
|
+
|
|
813
|
+
expect(bots.fields.lastSeen.readOnly).toBe(false)
|
|
814
|
+
expect(bots.fields.status.readOnly).toBe(true)
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('marks response-only fields readOnly when enabled', () => {
|
|
818
|
+
const connector = new OpenAPIConnector({ inferReadOnly: true })
|
|
819
|
+
const bots = connector.parse(botsSpec).find(e => e.name === 'bots')
|
|
820
|
+
|
|
821
|
+
// never in a request body → inferred readOnly
|
|
822
|
+
expect(bots.fields.uuid.readOnly).toBe(true)
|
|
823
|
+
expect(bots.fields.lastSeen.readOnly).toBe(true)
|
|
824
|
+
// present in the POST body → stays writable
|
|
825
|
+
expect(bots.fields.name.readOnly).toBe(false)
|
|
826
|
+
expect(bots.fields.notes.readOnly).toBe(false)
|
|
827
|
+
// schema-declared readOnly preserved
|
|
828
|
+
expect(bots.fields.status.readOnly).toBe(true)
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('marks every field readOnly for entities without write operations', () => {
|
|
832
|
+
const connector = new OpenAPIConnector({ inferReadOnly: true })
|
|
833
|
+
const logs = connector.parse(logsSpec).find(e => e.name === 'logs')
|
|
834
|
+
|
|
835
|
+
expect(logs.fields.id.readOnly).toBe(true)
|
|
836
|
+
expect(logs.fields.message.readOnly).toBe(true)
|
|
837
|
+
})
|
|
838
|
+
})
|
|
587
839
|
})
|
|
588
840
|
|
|
589
841
|
describe('extensions option', () => {
|