@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.
Files changed (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. 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
+ })