@quazardous/qdadm 1.19.3
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/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decorators Layer
|
|
3
|
+
*
|
|
4
|
+
* Applies per-entity field customizations to UnifiedEntitySchema.
|
|
5
|
+
* Decorators allow overriding field properties (hidden, label, readonly, order)
|
|
6
|
+
* without modifying the base schema from connectors.
|
|
7
|
+
*
|
|
8
|
+
* @module gen/decorators
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { UnifiedEntitySchema, UnifiedFieldSchema } from './schema'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Allowed decorator properties for field overrides.
|
|
15
|
+
* Unknown properties will trigger a console warning.
|
|
16
|
+
*/
|
|
17
|
+
const ALLOWED_FIELD_DECORATORS = ['hidden', 'label', 'readOnly', 'order'] as const
|
|
18
|
+
|
|
19
|
+
type AllowedFieldDecoratorKey = (typeof ALLOWED_FIELD_DECORATORS)[number]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Field Decorator Configuration
|
|
23
|
+
*
|
|
24
|
+
* Per-field overrides that can be applied to schema fields.
|
|
25
|
+
*/
|
|
26
|
+
export interface FieldDecorator {
|
|
27
|
+
/** Override field visibility */
|
|
28
|
+
hidden?: boolean
|
|
29
|
+
/** Override field label */
|
|
30
|
+
label?: string
|
|
31
|
+
/** Override field read-only state */
|
|
32
|
+
readOnly?: boolean
|
|
33
|
+
/** Override field display order */
|
|
34
|
+
order?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Entity Decorator Configuration
|
|
39
|
+
*
|
|
40
|
+
* Decorator configuration for a single entity.
|
|
41
|
+
*/
|
|
42
|
+
export interface EntityDecorator {
|
|
43
|
+
/** Field-level overrides keyed by field name */
|
|
44
|
+
fields?: Record<string, FieldDecorator>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Apply decorators to a UnifiedEntitySchema
|
|
49
|
+
*
|
|
50
|
+
* Takes a schema and decorator configuration, returns a new schema with
|
|
51
|
+
* decorator properties merged onto matching fields. Original schema is
|
|
52
|
+
* not mutated.
|
|
53
|
+
*
|
|
54
|
+
* @param schema - Original entity schema
|
|
55
|
+
* @param decoratorConfig - Decorator configuration
|
|
56
|
+
* @returns New schema with decorators applied
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Hide email field and rename created_at
|
|
60
|
+
* const decorated = applyDecorators(usersSchema, {
|
|
61
|
+
* fields: {
|
|
62
|
+
* email: { hidden: true },
|
|
63
|
+
* created_at: { label: 'Member Since', readOnly: true }
|
|
64
|
+
* }
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Set field display order
|
|
69
|
+
* const ordered = applyDecorators(postsSchema, {
|
|
70
|
+
* fields: {
|
|
71
|
+
* title: { order: 1 },
|
|
72
|
+
* body: { order: 2 },
|
|
73
|
+
* author_id: { order: 3 }
|
|
74
|
+
* }
|
|
75
|
+
* })
|
|
76
|
+
*/
|
|
77
|
+
export function applyDecorators(
|
|
78
|
+
schema: UnifiedEntitySchema,
|
|
79
|
+
decoratorConfig?: EntityDecorator
|
|
80
|
+
): UnifiedEntitySchema {
|
|
81
|
+
// No decorators - return schema as-is (still immutable reference)
|
|
82
|
+
if (!decoratorConfig || !decoratorConfig.fields) {
|
|
83
|
+
return schema
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { fields: fieldDecorators } = decoratorConfig
|
|
87
|
+
|
|
88
|
+
// Build new fields object with decorators applied
|
|
89
|
+
const decoratedFields: Record<string, UnifiedFieldSchema> = {}
|
|
90
|
+
|
|
91
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
92
|
+
const decorator = fieldDecorators[fieldName]
|
|
93
|
+
|
|
94
|
+
if (!decorator) {
|
|
95
|
+
// No decorator for this field - copy as-is
|
|
96
|
+
decoratedFields[fieldName] = { ...fieldSchema }
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate decorator properties
|
|
101
|
+
for (const key of Object.keys(decorator)) {
|
|
102
|
+
if (!(ALLOWED_FIELD_DECORATORS as readonly string[]).includes(key)) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[qdadm] Unknown decorator property "${key}" for field "${fieldName}" in entity "${schema.name}". Allowed: ${ALLOWED_FIELD_DECORATORS.join(', ')}`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Merge decorator properties onto field (only allowed properties)
|
|
110
|
+
const decoratedField: UnifiedFieldSchema = { ...fieldSchema }
|
|
111
|
+
for (const key of ALLOWED_FIELD_DECORATORS) {
|
|
112
|
+
if (key in decorator) {
|
|
113
|
+
;(decoratedField as Record<string, unknown>)[key] = decorator[key as AllowedFieldDecoratorKey]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
decoratedFields[fieldName] = decoratedField
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Warn about decorators for non-existent fields
|
|
121
|
+
for (const fieldName of Object.keys(fieldDecorators)) {
|
|
122
|
+
if (!(fieldName in schema.fields)) {
|
|
123
|
+
console.warn(
|
|
124
|
+
`[qdadm] Decorator defined for unknown field "${fieldName}" in entity "${schema.name}". Available fields: ${Object.keys(schema.fields).join(', ')}`
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Return new schema with decorated fields
|
|
130
|
+
return {
|
|
131
|
+
...schema,
|
|
132
|
+
fields: decoratedFields,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { generateManagers, fieldTypeToTsType, generateEntityInterface } from './generateManagers'
|
|
3
|
+
import { rm, readFile, stat } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
describe('generateManagers', () => {
|
|
8
|
+
let testOutputDir
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Create unique temp directory for each test
|
|
12
|
+
testOutputDir = join(tmpdir(), `qdadm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
// Clean up test output directory
|
|
17
|
+
try {
|
|
18
|
+
await rm(testOutputDir, { recursive: true, force: true })
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore cleanup errors
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('validation', () => {
|
|
25
|
+
it('throws if config is missing', async () => {
|
|
26
|
+
await expect(generateManagers()).rejects.toThrow('config is required')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('throws if config is not an object', async () => {
|
|
30
|
+
await expect(generateManagers('invalid')).rejects.toThrow('config is required')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('throws if entities is missing', async () => {
|
|
34
|
+
await expect(generateManagers({})).rejects.toThrow('config.entities is required')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('throws if entity config is not an object', async () => {
|
|
38
|
+
await expect(generateManagers({
|
|
39
|
+
output: testOutputDir,
|
|
40
|
+
entities: { users: null }
|
|
41
|
+
})).rejects.toThrow("entity 'users' config must be an object")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('throws if entity schema is missing', async () => {
|
|
45
|
+
await expect(generateManagers({
|
|
46
|
+
output: testOutputDir,
|
|
47
|
+
entities: {
|
|
48
|
+
users: {
|
|
49
|
+
endpoint: '/users',
|
|
50
|
+
storageImport: '@quazardous/qdadm',
|
|
51
|
+
storageClass: 'ApiStorage'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})).rejects.toThrow("entity 'users' requires 'schema' property")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws if entity endpoint is missing', async () => {
|
|
58
|
+
await expect(generateManagers({
|
|
59
|
+
output: testOutputDir,
|
|
60
|
+
entities: {
|
|
61
|
+
users: {
|
|
62
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
63
|
+
storageImport: '@quazardous/qdadm',
|
|
64
|
+
storageClass: 'ApiStorage'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})).rejects.toThrow("entity 'users' requires 'endpoint' property")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws if entity storageImport is missing', async () => {
|
|
71
|
+
await expect(generateManagers({
|
|
72
|
+
output: testOutputDir,
|
|
73
|
+
entities: {
|
|
74
|
+
users: {
|
|
75
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
76
|
+
endpoint: '/users',
|
|
77
|
+
storageClass: 'ApiStorage'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})).rejects.toThrow("entity 'users' requires 'storageImport' property")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('throws if entity storageClass is missing', async () => {
|
|
84
|
+
await expect(generateManagers({
|
|
85
|
+
output: testOutputDir,
|
|
86
|
+
entities: {
|
|
87
|
+
users: {
|
|
88
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
89
|
+
endpoint: '/users',
|
|
90
|
+
storageImport: '@quazardous/qdadm'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})).rejects.toThrow("entity 'users' requires 'storageClass' property")
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('file generation', () => {
|
|
98
|
+
it('creates output directory if it does not exist', async () => {
|
|
99
|
+
const files = await generateManagers({
|
|
100
|
+
output: testOutputDir,
|
|
101
|
+
entities: {
|
|
102
|
+
users: {
|
|
103
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
104
|
+
endpoint: '/users',
|
|
105
|
+
storageImport: '@quazardous/qdadm',
|
|
106
|
+
storageClass: 'ApiStorage'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(files).toHaveLength(1)
|
|
112
|
+
|
|
113
|
+
// Directory should exist
|
|
114
|
+
const dirStat = await stat(testOutputDir)
|
|
115
|
+
expect(dirStat.isDirectory()).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('generates .ts files with correct naming', async () => {
|
|
119
|
+
const files = await generateManagers({
|
|
120
|
+
output: testOutputDir,
|
|
121
|
+
entities: {
|
|
122
|
+
users: {
|
|
123
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
124
|
+
endpoint: '/api/users',
|
|
125
|
+
storageImport: '@quazardous/qdadm',
|
|
126
|
+
storageClass: 'ApiStorage'
|
|
127
|
+
},
|
|
128
|
+
blog_posts: {
|
|
129
|
+
schema: { name: 'blog_posts', endpoint: '/posts', fields: {} },
|
|
130
|
+
endpoint: '/api/posts',
|
|
131
|
+
storageImport: './storage',
|
|
132
|
+
storageClass: 'CustomStorage'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(files).toHaveLength(2)
|
|
138
|
+
expect(files).toContain(join(testOutputDir, 'usersManager.ts'))
|
|
139
|
+
expect(files).toContain(join(testOutputDir, 'blog_postsManager.ts'))
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns file paths in generation order', async () => {
|
|
143
|
+
const files = await generateManagers({
|
|
144
|
+
output: testOutputDir,
|
|
145
|
+
entities: {
|
|
146
|
+
alpha: {
|
|
147
|
+
schema: { name: 'alpha', endpoint: '/alpha', fields: {} },
|
|
148
|
+
endpoint: '/alpha',
|
|
149
|
+
storageImport: '@quazardous/qdadm',
|
|
150
|
+
storageClass: 'ApiStorage'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(files[0]).toBe(join(testOutputDir, 'alphaManager.ts'))
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('generated file content', () => {
|
|
160
|
+
it('generates valid TypeScript with imports and interface', async () => {
|
|
161
|
+
await generateManagers({
|
|
162
|
+
output: testOutputDir,
|
|
163
|
+
entities: {
|
|
164
|
+
users: {
|
|
165
|
+
schema: {
|
|
166
|
+
name: 'users',
|
|
167
|
+
endpoint: '/users',
|
|
168
|
+
fields: {
|
|
169
|
+
id: { name: 'id', type: 'number', readOnly: true },
|
|
170
|
+
email: { name: 'email', type: 'email', required: true }
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
endpoint: '/api/users',
|
|
174
|
+
storageImport: '@quazardous/qdadm',
|
|
175
|
+
storageClass: 'ApiStorage'
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
181
|
+
|
|
182
|
+
// Check imports
|
|
183
|
+
expect(content).toContain("import { EntityManager } from '@quazardous/qdadm'")
|
|
184
|
+
expect(content).toContain("import { ApiStorage } from '@quazardous/qdadm'")
|
|
185
|
+
expect(content).toContain("import type { EntityRecord } from '@quazardous/qdadm'")
|
|
186
|
+
|
|
187
|
+
// Check entity interface
|
|
188
|
+
expect(content).toContain('export interface UsersEntity extends EntityRecord')
|
|
189
|
+
expect(content).toContain('id: number')
|
|
190
|
+
expect(content).toContain('email: string')
|
|
191
|
+
|
|
192
|
+
// Check typed manager
|
|
193
|
+
expect(content).toContain('new EntityManager<UsersEntity>')
|
|
194
|
+
|
|
195
|
+
// Storage must be parameterized with the entity type — without this the
|
|
196
|
+
// generated file fails type-check at the consumer side because
|
|
197
|
+
// ApiStorage<EntityRecord> is not assignable to IStorage<UsersEntity>.
|
|
198
|
+
expect(content).toContain('new ApiStorage<UsersEntity>(storageOptions)')
|
|
199
|
+
|
|
200
|
+
// Check exports
|
|
201
|
+
expect(content).toContain('export const usersSchema')
|
|
202
|
+
expect(content).toContain('export const usersManager')
|
|
203
|
+
|
|
204
|
+
// Check schema content (uses double quotes from JSON.stringify)
|
|
205
|
+
expect(content).toContain('name: "users"')
|
|
206
|
+
expect(content).toContain('type: "number"')
|
|
207
|
+
expect(content).toContain('type: "email"')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('includes DO NOT EDIT warning', async () => {
|
|
211
|
+
await generateManagers({
|
|
212
|
+
output: testOutputDir,
|
|
213
|
+
entities: {
|
|
214
|
+
users: {
|
|
215
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
216
|
+
endpoint: '/api/users',
|
|
217
|
+
storageImport: '@quazardous/qdadm',
|
|
218
|
+
storageClass: 'ApiStorage'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
224
|
+
expect(content).toContain('DO NOT EDIT MANUALLY')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('includes endpoint in storage options', async () => {
|
|
228
|
+
await generateManagers({
|
|
229
|
+
output: testOutputDir,
|
|
230
|
+
entities: {
|
|
231
|
+
users: {
|
|
232
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
233
|
+
endpoint: '/api/v1/users',
|
|
234
|
+
storageImport: '@quazardous/qdadm',
|
|
235
|
+
storageClass: 'ApiStorage'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
241
|
+
expect(content).toContain('endpoint: "/api/v1/users"')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('includes custom storage options', async () => {
|
|
245
|
+
await generateManagers({
|
|
246
|
+
output: testOutputDir,
|
|
247
|
+
entities: {
|
|
248
|
+
users: {
|
|
249
|
+
schema: { name: 'users', endpoint: '/users', fields: {} },
|
|
250
|
+
endpoint: '/users',
|
|
251
|
+
storageImport: '@quazardous/qdadm',
|
|
252
|
+
storageClass: 'ApiStorage',
|
|
253
|
+
storageOptions: {
|
|
254
|
+
timeout: 5000,
|
|
255
|
+
retries: 3
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
262
|
+
expect(content).toContain('timeout: 5000')
|
|
263
|
+
expect(content).toContain('retries: 3')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('applies decorators to schema fields', async () => {
|
|
267
|
+
await generateManagers({
|
|
268
|
+
output: testOutputDir,
|
|
269
|
+
entities: {
|
|
270
|
+
users: {
|
|
271
|
+
schema: {
|
|
272
|
+
name: 'users',
|
|
273
|
+
endpoint: '/users',
|
|
274
|
+
fields: {
|
|
275
|
+
id: { name: 'id', type: 'number' },
|
|
276
|
+
password: { name: 'password', type: 'text' },
|
|
277
|
+
email: { name: 'email', type: 'email' }
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
endpoint: '/users',
|
|
281
|
+
storageImport: '@quazardous/qdadm',
|
|
282
|
+
storageClass: 'ApiStorage',
|
|
283
|
+
decorators: {
|
|
284
|
+
fields: {
|
|
285
|
+
password: { hidden: true },
|
|
286
|
+
email: { label: 'Email Address' }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
294
|
+
|
|
295
|
+
// Password field should have hidden: true
|
|
296
|
+
expect(content).toContain('hidden: true')
|
|
297
|
+
|
|
298
|
+
// Email field should have custom label
|
|
299
|
+
expect(content).toContain('label: "Email Address"')
|
|
300
|
+
|
|
301
|
+
// Hidden fields should not appear in the interface
|
|
302
|
+
expect(content).not.toMatch(/password\??: string/)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('generates PascalCase class name in comments', async () => {
|
|
306
|
+
await generateManagers({
|
|
307
|
+
output: testOutputDir,
|
|
308
|
+
entities: {
|
|
309
|
+
blog_posts: {
|
|
310
|
+
schema: { name: 'blog_posts', endpoint: '/posts', fields: {} },
|
|
311
|
+
endpoint: '/posts',
|
|
312
|
+
storageImport: '@quazardous/qdadm',
|
|
313
|
+
storageClass: 'ApiStorage'
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const content = await readFile(join(testOutputDir, 'blog_postsManager.ts'), 'utf-8')
|
|
319
|
+
expect(content).toContain('BlogPostsManager')
|
|
320
|
+
expect(content).toContain('BlogPostsEntity')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('handles schema with all optional properties', async () => {
|
|
324
|
+
await generateManagers({
|
|
325
|
+
output: testOutputDir,
|
|
326
|
+
entities: {
|
|
327
|
+
users: {
|
|
328
|
+
schema: {
|
|
329
|
+
name: 'users',
|
|
330
|
+
endpoint: '/users',
|
|
331
|
+
label: 'User',
|
|
332
|
+
labelPlural: 'Users',
|
|
333
|
+
labelField: 'name',
|
|
334
|
+
routePrefix: 'user',
|
|
335
|
+
idField: 'uuid',
|
|
336
|
+
readOnly: true,
|
|
337
|
+
fields: {}
|
|
338
|
+
},
|
|
339
|
+
endpoint: '/users',
|
|
340
|
+
storageImport: '@quazardous/qdadm',
|
|
341
|
+
storageClass: 'ApiStorage'
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
347
|
+
expect(content).toContain('label: "User"')
|
|
348
|
+
expect(content).toContain('labelPlural: "Users"')
|
|
349
|
+
expect(content).toContain('labelField: "name"')
|
|
350
|
+
expect(content).toContain('routePrefix: "user"')
|
|
351
|
+
expect(content).toContain('idField: "uuid"')
|
|
352
|
+
expect(content).toContain('readOnly: true')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('generates entity interface with correct field types', async () => {
|
|
356
|
+
await generateManagers({
|
|
357
|
+
output: testOutputDir,
|
|
358
|
+
entities: {
|
|
359
|
+
products: {
|
|
360
|
+
schema: {
|
|
361
|
+
name: 'products',
|
|
362
|
+
endpoint: '/products',
|
|
363
|
+
fields: {
|
|
364
|
+
id: { name: 'id', type: 'number', required: true },
|
|
365
|
+
name: { name: 'name', type: 'text', required: true },
|
|
366
|
+
price: { name: 'price', type: 'number' },
|
|
367
|
+
active: { name: 'active', type: 'boolean', required: true },
|
|
368
|
+
created_at: { name: 'created_at', type: 'datetime' },
|
|
369
|
+
tags: { name: 'tags', type: 'array' },
|
|
370
|
+
metadata: { name: 'metadata', type: 'object' },
|
|
371
|
+
email: { name: 'email', type: 'email' },
|
|
372
|
+
website: { name: 'website', type: 'url' },
|
|
373
|
+
uuid: { name: 'uuid', type: 'uuid' },
|
|
374
|
+
birth_date: { name: 'birth_date', type: 'date' }
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
endpoint: '/products',
|
|
378
|
+
storageImport: '@quazardous/qdadm',
|
|
379
|
+
storageClass: 'ApiStorage'
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const content = await readFile(join(testOutputDir, 'productsManager.ts'), 'utf-8')
|
|
385
|
+
|
|
386
|
+
// Required fields (no optional marker, no null union)
|
|
387
|
+
expect(content).toContain('id: number')
|
|
388
|
+
expect(content).toContain('name: string')
|
|
389
|
+
expect(content).toContain('active: boolean')
|
|
390
|
+
|
|
391
|
+
// Optional fields (with ? and null union)
|
|
392
|
+
expect(content).toContain('price?: number | null')
|
|
393
|
+
expect(content).toContain('created_at?: string | null')
|
|
394
|
+
expect(content).toContain('tags?: unknown[] | null')
|
|
395
|
+
expect(content).toContain('metadata?: Record<string, unknown> | null')
|
|
396
|
+
expect(content).toContain('email?: string | null')
|
|
397
|
+
expect(content).toContain('website?: string | null')
|
|
398
|
+
expect(content).toContain('uuid?: string | null')
|
|
399
|
+
expect(content).toContain('birth_date?: string | null')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('generates class mode with typed interface', async () => {
|
|
403
|
+
await generateManagers({
|
|
404
|
+
output: testOutputDir,
|
|
405
|
+
classMode: true,
|
|
406
|
+
entities: {
|
|
407
|
+
users: {
|
|
408
|
+
schema: {
|
|
409
|
+
name: 'users',
|
|
410
|
+
endpoint: '/users',
|
|
411
|
+
fields: {
|
|
412
|
+
id: { name: 'id', type: 'number' },
|
|
413
|
+
name: { name: 'name', type: 'text', required: true }
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
endpoint: '/users',
|
|
417
|
+
storageImport: '@quazardous/qdadm',
|
|
418
|
+
storageClass: 'ApiStorage'
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const content = await readFile(join(testOutputDir, 'users.ts'), 'utf-8')
|
|
424
|
+
expect(content).toContain('export interface UsersEntity extends EntityRecord')
|
|
425
|
+
expect(content).toContain('extends EntityManager<UsersEntity>')
|
|
426
|
+
|
|
427
|
+
// Storage must be parameterized in class mode too — same TS2322
|
|
428
|
+
// assignability problem as instance mode if we drop the generic.
|
|
429
|
+
expect(content).toContain('new ApiStorage<UsersEntity>(')
|
|
430
|
+
// Constructor signature should accept a typed storage override, not `unknown`
|
|
431
|
+
expect(content).toContain("import type { EntityRecord, IStorage } from '@quazardous/qdadm'")
|
|
432
|
+
expect(content).toContain('storage: IStorage<UsersEntity>')
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe('default output directory', () => {
|
|
437
|
+
it('uses default output when not specified', async () => {
|
|
438
|
+
// This test would write to actual src/generated/managers/
|
|
439
|
+
// We'll just verify the function accepts config without output
|
|
440
|
+
const config = {
|
|
441
|
+
entities: {
|
|
442
|
+
test: {
|
|
443
|
+
schema: { name: 'test', endpoint: '/test', fields: {} },
|
|
444
|
+
endpoint: '/test',
|
|
445
|
+
storageImport: '@quazardous/qdadm',
|
|
446
|
+
storageClass: 'ApiStorage'
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Should not throw (we can't easily test the default path without cleanup concerns)
|
|
452
|
+
// The function validates before writing, so a valid config should work
|
|
453
|
+
expect(() => generateManagers(config)).not.toThrow()
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe('fieldTypeToTsType', () => {
|
|
459
|
+
it('maps text types to string', () => {
|
|
460
|
+
expect(fieldTypeToTsType('text', true)).toBe('string')
|
|
461
|
+
expect(fieldTypeToTsType('email', true)).toBe('string')
|
|
462
|
+
expect(fieldTypeToTsType('url', true)).toBe('string')
|
|
463
|
+
expect(fieldTypeToTsType('uuid', true)).toBe('string')
|
|
464
|
+
expect(fieldTypeToTsType('date', true)).toBe('string')
|
|
465
|
+
expect(fieldTypeToTsType('datetime', true)).toBe('string')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('maps number to number', () => {
|
|
469
|
+
expect(fieldTypeToTsType('number', true)).toBe('number')
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('maps boolean to boolean', () => {
|
|
473
|
+
expect(fieldTypeToTsType('boolean', true)).toBe('boolean')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('maps array to unknown[]', () => {
|
|
477
|
+
expect(fieldTypeToTsType('array', true)).toBe('unknown[]')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('maps object to Record<string, unknown>', () => {
|
|
481
|
+
expect(fieldTypeToTsType('object', true)).toBe('Record<string, unknown>')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('adds null union for optional fields', () => {
|
|
485
|
+
expect(fieldTypeToTsType('text', false)).toBe('string | null')
|
|
486
|
+
expect(fieldTypeToTsType('number', false)).toBe('number | null')
|
|
487
|
+
expect(fieldTypeToTsType('boolean', false)).toBe('boolean | null')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('falls back to unknown for unrecognized types instead of emitting literal undefined', () => {
|
|
491
|
+
// Repro for v1.19.1 bug: connectors could yield types outside UnifiedFieldType
|
|
492
|
+
// (e.g. OpenAPI oneOf/discriminator). Without a default branch the IIFE
|
|
493
|
+
// returned `undefined` and template-literal interpolation wrote the string
|
|
494
|
+
// "undefined" into the generated .ts file.
|
|
495
|
+
expect(fieldTypeToTsType('mystery-type', true)).toBe('unknown')
|
|
496
|
+
expect(fieldTypeToTsType('mystery-type', false)).toBe('unknown | null')
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
describe('generateEntityInterface', () => {
|
|
501
|
+
it('generates interface with correct field types', () => {
|
|
502
|
+
const result = generateEntityInterface('users', {
|
|
503
|
+
id: { name: 'id', type: 'number' },
|
|
504
|
+
name: { name: 'name', type: 'text', required: true },
|
|
505
|
+
email: { name: 'email', type: 'email' }
|
|
506
|
+
}, 'id')
|
|
507
|
+
|
|
508
|
+
expect(result).toContain('export interface UsersEntity extends EntityRecord')
|
|
509
|
+
expect(result).toContain('id: number') // id field is always required
|
|
510
|
+
expect(result).toContain('name: string') // required field
|
|
511
|
+
expect(result).toContain('email?: string | null') // optional field
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('excludes hidden fields', () => {
|
|
515
|
+
const result = generateEntityInterface('users', {
|
|
516
|
+
id: { name: 'id', type: 'number' },
|
|
517
|
+
password: { name: 'password', type: 'text', hidden: true }
|
|
518
|
+
}, 'id')
|
|
519
|
+
|
|
520
|
+
expect(result).not.toContain('password')
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('skips dotted-name children of nested object fields', () => {
|
|
524
|
+
// Repro for v1.19.1 bug: OpenAPIConnector flattens one level of nested
|
|
525
|
+
// object props as dotted keys for runtime metadata. The interface emitter
|
|
526
|
+
// must not write those keys at the top level — they're invalid TS syntax,
|
|
527
|
+
// and the parent object field already carries Record<string, unknown>.
|
|
528
|
+
const result = generateEntityInterface('commands', {
|
|
529
|
+
id: { name: 'id', type: 'uuid' },
|
|
530
|
+
filter: { name: 'filter', type: 'object' },
|
|
531
|
+
'filter.botUuids': { name: 'filter.botUuids', type: 'array' },
|
|
532
|
+
'filter.tags': { name: 'filter.tags', type: 'array' }
|
|
533
|
+
}, 'id')
|
|
534
|
+
|
|
535
|
+
expect(result).toContain('filter?: Record<string, unknown> | null')
|
|
536
|
+
expect(result).not.toContain('filter.botUuids')
|
|
537
|
+
expect(result).not.toContain('filter.tags')
|
|
538
|
+
expect(result).not.toMatch(/^\s*filter\./m)
|
|
539
|
+
})
|
|
540
|
+
})
|