@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,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time Manager File Generation
|
|
3
|
+
*
|
|
4
|
+
* Generates individual EntityManager files for each entity in the configuration.
|
|
5
|
+
* This enables better IDE support and static analysis by creating explicit files
|
|
6
|
+
* rather than relying on runtime factory creation.
|
|
7
|
+
*
|
|
8
|
+
* Outputs TypeScript (.ts) files with auto-generated entity interfaces derived
|
|
9
|
+
* from the field schemas, providing end-to-end type safety.
|
|
10
|
+
*
|
|
11
|
+
* @module gen/generateManagers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
import type { UnifiedEntitySchema, UnifiedFieldSchema, UnifiedFieldType } from './schema'
|
|
18
|
+
import type { EntityDecorator } from './decorators'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default output directory for generated managers
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_OUTPUT_DIR = 'src/generated/managers/'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Entity configuration for generation
|
|
27
|
+
*/
|
|
28
|
+
export interface GenerateManagersEntityConfig {
|
|
29
|
+
/** Entity schema */
|
|
30
|
+
schema: UnifiedEntitySchema
|
|
31
|
+
/** API endpoint path (e.g., '/users') */
|
|
32
|
+
endpoint: string
|
|
33
|
+
/** Import path for storage (e.g., 'qdadm' or './storage') */
|
|
34
|
+
storageImport: string
|
|
35
|
+
/** Storage class name (e.g., 'ApiStorage') */
|
|
36
|
+
storageClass: string
|
|
37
|
+
/** Additional storage constructor options */
|
|
38
|
+
storageOptions?: Record<string, unknown>
|
|
39
|
+
/** Field decorators to apply */
|
|
40
|
+
decorators?: EntityDecorator
|
|
41
|
+
/** Generate class instead of instance (for extension) */
|
|
42
|
+
classMode?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for generateManagers
|
|
47
|
+
*/
|
|
48
|
+
export interface GenerateManagersConfig {
|
|
49
|
+
/** Output directory (default: 'src/generated/managers/') */
|
|
50
|
+
output?: string
|
|
51
|
+
/** Generate classes instead of instances (for extension pattern) */
|
|
52
|
+
classMode?: boolean
|
|
53
|
+
/** Entity configurations keyed by entity name */
|
|
54
|
+
entities: Record<string, GenerateManagersEntityConfig>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Convert entity name to PascalCase for class name
|
|
59
|
+
*
|
|
60
|
+
* @param name - Entity name (e.g., 'users', 'blog_posts')
|
|
61
|
+
* @returns PascalCase name (e.g., 'Users', 'BlogPosts')
|
|
62
|
+
*/
|
|
63
|
+
function toPascalCase(name: string): string {
|
|
64
|
+
return name
|
|
65
|
+
.split(/[_-]/)
|
|
66
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
67
|
+
.join('')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Map UnifiedFieldType to TypeScript type string
|
|
72
|
+
*/
|
|
73
|
+
export function fieldTypeToTsType(type: UnifiedFieldType, required: boolean): string {
|
|
74
|
+
const tsType = (() => {
|
|
75
|
+
switch (type) {
|
|
76
|
+
case 'text':
|
|
77
|
+
case 'email':
|
|
78
|
+
case 'url':
|
|
79
|
+
case 'uuid':
|
|
80
|
+
case 'date':
|
|
81
|
+
case 'datetime':
|
|
82
|
+
return 'string'
|
|
83
|
+
case 'number':
|
|
84
|
+
return 'number'
|
|
85
|
+
case 'boolean':
|
|
86
|
+
return 'boolean'
|
|
87
|
+
case 'array':
|
|
88
|
+
return 'unknown[]'
|
|
89
|
+
case 'object':
|
|
90
|
+
return 'Record<string, unknown>'
|
|
91
|
+
default:
|
|
92
|
+
// Fallback for types widened beyond UnifiedFieldType at runtime
|
|
93
|
+
// (e.g. OpenAPI oneOf/discriminator that FieldMapper couldn't classify).
|
|
94
|
+
// Without this, the IIFE returns undefined and we'd emit the literal
|
|
95
|
+
// string "undefined" into the generated .ts file.
|
|
96
|
+
return 'unknown'
|
|
97
|
+
}
|
|
98
|
+
})()
|
|
99
|
+
return required ? tsType : `${tsType} | null`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate a TypeScript interface from entity fields
|
|
104
|
+
*/
|
|
105
|
+
export function generateEntityInterface(
|
|
106
|
+
entityName: string,
|
|
107
|
+
fields: Record<string, UnifiedFieldSchema>,
|
|
108
|
+
idField: string
|
|
109
|
+
): string {
|
|
110
|
+
const className = toPascalCase(entityName)
|
|
111
|
+
const lines: string[] = []
|
|
112
|
+
lines.push(`export interface ${className}Entity extends EntityRecord {`)
|
|
113
|
+
|
|
114
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
115
|
+
if (field.hidden) continue
|
|
116
|
+
// Connectors (e.g. OpenAPIConnector) flatten one level of nested object
|
|
117
|
+
// properties as dotted keys ("filter.botUuids") for runtime form/column
|
|
118
|
+
// metadata. The parent object field is already typed as
|
|
119
|
+
// Record<string, unknown>, and dotted keys are not legal at the top level
|
|
120
|
+
// of a TS interface body — skip them here.
|
|
121
|
+
if (fieldName.includes('.')) continue
|
|
122
|
+
const isRequired = field.required || fieldName === idField
|
|
123
|
+
const tsType = fieldTypeToTsType(field.type, isRequired)
|
|
124
|
+
const optional = isRequired ? '' : '?'
|
|
125
|
+
lines.push(` ${fieldName}${optional}: ${tsType}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push('}')
|
|
129
|
+
return lines.join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Serialize a value to JavaScript source code
|
|
134
|
+
*
|
|
135
|
+
* @param value - Value to serialize
|
|
136
|
+
* @param indent - Current indentation level
|
|
137
|
+
* @returns JavaScript source code
|
|
138
|
+
*/
|
|
139
|
+
function serializeValue(value: unknown, indent: number = 0): string {
|
|
140
|
+
const spaces = ' '.repeat(indent)
|
|
141
|
+
const innerSpaces = ' '.repeat(indent + 1)
|
|
142
|
+
|
|
143
|
+
if (value === null) return 'null'
|
|
144
|
+
if (value === undefined) return 'undefined'
|
|
145
|
+
if (typeof value === 'string') return JSON.stringify(value)
|
|
146
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
if (value.length === 0) return '[]'
|
|
149
|
+
const items = value.map((v) => serializeValue(v, indent + 1))
|
|
150
|
+
return `[\n${innerSpaces}${items.join(`,\n${innerSpaces}`)}\n${spaces}]`
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === 'object') {
|
|
153
|
+
const obj = value as Record<string, unknown>
|
|
154
|
+
const keys = Object.keys(obj)
|
|
155
|
+
if (keys.length === 0) return '{}'
|
|
156
|
+
const entries = keys.map((key) => {
|
|
157
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
|
|
158
|
+
return `${innerSpaces}${safeKey}: ${serializeValue(obj[key], indent + 1)}`
|
|
159
|
+
})
|
|
160
|
+
return `{\n${entries.join(',\n')}\n${spaces}}`
|
|
161
|
+
}
|
|
162
|
+
return String(value)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Note: ManagerOptions interface removed - using inline Record<string, unknown> type instead
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate the source code for a single entity manager file
|
|
169
|
+
*
|
|
170
|
+
* @param entityName - Entity name
|
|
171
|
+
* @param entityConfig - Entity configuration
|
|
172
|
+
* @param globalOptions - Global generation options
|
|
173
|
+
* @returns Generated TypeScript source code
|
|
174
|
+
*/
|
|
175
|
+
function generateManagerSource(
|
|
176
|
+
entityName: string,
|
|
177
|
+
entityConfig: GenerateManagersEntityConfig,
|
|
178
|
+
globalOptions: { classMode?: boolean } = {}
|
|
179
|
+
): string {
|
|
180
|
+
const { schema, endpoint, storageImport, storageClass, storageOptions = {}, decorators } = entityConfig
|
|
181
|
+
const classMode = entityConfig.classMode ?? globalOptions.classMode ?? false
|
|
182
|
+
const className = toPascalCase(entityName)
|
|
183
|
+
|
|
184
|
+
// Build storage options object
|
|
185
|
+
const fullStorageOptions: Record<string, unknown> = {
|
|
186
|
+
endpoint,
|
|
187
|
+
...storageOptions,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Apply decorators to schema if provided
|
|
191
|
+
let finalSchema = schema
|
|
192
|
+
if (decorators?.fields) {
|
|
193
|
+
// Create decorated fields inline
|
|
194
|
+
const decoratedFields: Record<string, UnifiedFieldSchema> = {}
|
|
195
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
196
|
+
const decorator = decorators.fields[fieldName]
|
|
197
|
+
if (decorator) {
|
|
198
|
+
decoratedFields[fieldName] = { ...fieldSchema, ...decorator }
|
|
199
|
+
} else {
|
|
200
|
+
decoratedFields[fieldName] = { ...fieldSchema }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finalSchema = { ...schema, fields: decoratedFields }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build EntityManager options
|
|
207
|
+
const managerOptions: Record<string, unknown> = {
|
|
208
|
+
name: finalSchema.name,
|
|
209
|
+
idField: finalSchema.idField || 'id',
|
|
210
|
+
label: finalSchema.label,
|
|
211
|
+
labelPlural: finalSchema.labelPlural,
|
|
212
|
+
labelField: finalSchema.labelField,
|
|
213
|
+
routePrefix: finalSchema.routePrefix,
|
|
214
|
+
readOnly: finalSchema.readOnly,
|
|
215
|
+
fields: finalSchema.fields,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove undefined values
|
|
219
|
+
for (const key of Object.keys(managerOptions)) {
|
|
220
|
+
if (managerOptions[key] === undefined) {
|
|
221
|
+
delete managerOptions[key]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Generate entity interface
|
|
226
|
+
const idField = finalSchema.idField || 'id'
|
|
227
|
+
const entityInterface = generateEntityInterface(entityName, finalSchema.fields, idField)
|
|
228
|
+
|
|
229
|
+
// Class-based template for extension pattern
|
|
230
|
+
if (classMode) {
|
|
231
|
+
return `/**
|
|
232
|
+
* Generated${className}Manager - Auto-generated base class
|
|
233
|
+
*
|
|
234
|
+
* Generated by qdadm/gen/generateManagers
|
|
235
|
+
* DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
236
|
+
*
|
|
237
|
+
* Extend this class for custom logic.
|
|
238
|
+
*
|
|
239
|
+
* Entity: ${entityName}
|
|
240
|
+
* Endpoint: ${endpoint}
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
import { EntityManager } from '@quazardous/qdadm'
|
|
244
|
+
import type { EntityRecord, IStorage } from '@quazardous/qdadm'
|
|
245
|
+
import { ${storageClass} } from '${storageImport}'
|
|
246
|
+
|
|
247
|
+
${entityInterface}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generated base manager for ${entityName}
|
|
251
|
+
*
|
|
252
|
+
* @extends EntityManager
|
|
253
|
+
*/
|
|
254
|
+
export class Generated${className}Manager extends EntityManager<${className}Entity> {
|
|
255
|
+
constructor(options: Partial<{ name: string; idField: string; fields: Record<string, unknown>; storage: IStorage<${className}Entity>; [key: string]: unknown }> = {}) {
|
|
256
|
+
super({
|
|
257
|
+
...${serializeValue(managerOptions, 2)},
|
|
258
|
+
storage: new ${storageClass}<${className}Entity>(${serializeValue(fullStorageOptions, 2)}),
|
|
259
|
+
...options
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Instance-based template (default)
|
|
267
|
+
return `/**
|
|
268
|
+
* ${className}Manager - Auto-generated EntityManager
|
|
269
|
+
*
|
|
270
|
+
* Generated by qdadm/gen/generateManagers
|
|
271
|
+
* DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
272
|
+
*
|
|
273
|
+
* Entity: ${entityName}
|
|
274
|
+
* Endpoint: ${endpoint}
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
import { EntityManager } from '@quazardous/qdadm'
|
|
278
|
+
import type { EntityRecord } from '@quazardous/qdadm'
|
|
279
|
+
import { ${storageClass} } from '${storageImport}'
|
|
280
|
+
|
|
281
|
+
${entityInterface}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Schema definition for ${entityName}
|
|
285
|
+
*/
|
|
286
|
+
export const ${entityName}Schema = ${serializeValue(finalSchema, 0)} as const
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Storage options for ${entityName}
|
|
290
|
+
*/
|
|
291
|
+
const storageOptions = ${serializeValue(fullStorageOptions, 0)}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* ${className}Manager instance
|
|
295
|
+
*
|
|
296
|
+
* Provides CRUD operations for ${entityName} entity.
|
|
297
|
+
*/
|
|
298
|
+
export const ${entityName}Manager = new EntityManager<${className}Entity>({
|
|
299
|
+
...${serializeValue(managerOptions, 0)},
|
|
300
|
+
storage: new ${storageClass}<${className}Entity>(storageOptions)
|
|
301
|
+
})
|
|
302
|
+
`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate EntityManager files from configuration
|
|
307
|
+
*
|
|
308
|
+
* Creates one TypeScript file per entity in the output directory.
|
|
309
|
+
* Each file exports the entity's interface, schema, and a configured EntityManager instance.
|
|
310
|
+
*
|
|
311
|
+
* @param config - Configuration object
|
|
312
|
+
* @returns List of generated file paths
|
|
313
|
+
* @throws If config is invalid or file operations fail
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* import { generateManagers } from '@quazardous/qdadm/gen'
|
|
318
|
+
*
|
|
319
|
+
* const generatedFiles = await generateManagers({
|
|
320
|
+
* output: 'src/generated/managers/',
|
|
321
|
+
* entities: {
|
|
322
|
+
* users: {
|
|
323
|
+
* schema: usersSchema,
|
|
324
|
+
* endpoint: '/api/users',
|
|
325
|
+
* storageImport: '@quazardous/qdadm',
|
|
326
|
+
* storageClass: 'ApiStorage',
|
|
327
|
+
* storageOptions: { client: 'apiClient' },
|
|
328
|
+
* decorators: {
|
|
329
|
+
* fields: {
|
|
330
|
+
* password: { hidden: true }
|
|
331
|
+
* }
|
|
332
|
+
* }
|
|
333
|
+
* }
|
|
334
|
+
* }
|
|
335
|
+
* })
|
|
336
|
+
*
|
|
337
|
+
* console.log('Generated:', generatedFiles)
|
|
338
|
+
* // ['src/generated/managers/usersManager.ts']
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export async function generateManagers(config: GenerateManagersConfig): Promise<string[]> {
|
|
342
|
+
// Validate config
|
|
343
|
+
if (!config || typeof config !== 'object') {
|
|
344
|
+
throw new Error('generateManagers: config is required and must be an object')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!config.entities || typeof config.entities !== 'object') {
|
|
348
|
+
throw new Error('generateManagers: config.entities is required and must be an object')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const outputDir = config.output || DEFAULT_OUTPUT_DIR
|
|
352
|
+
const globalOptions = { classMode: config.classMode ?? false }
|
|
353
|
+
const generatedFiles: string[] = []
|
|
354
|
+
|
|
355
|
+
// Validate each entity config
|
|
356
|
+
for (const [entityName, entityConfig] of Object.entries(config.entities)) {
|
|
357
|
+
if (!entityConfig || typeof entityConfig !== 'object') {
|
|
358
|
+
throw new Error(`generateManagers: entity '${entityName}' config must be an object`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!entityConfig.schema || typeof entityConfig.schema !== 'object') {
|
|
362
|
+
throw new Error(`generateManagers: entity '${entityName}' requires 'schema' property`)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!entityConfig.endpoint || typeof entityConfig.endpoint !== 'string') {
|
|
366
|
+
throw new Error(`generateManagers: entity '${entityName}' requires 'endpoint' property`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!entityConfig.storageImport || typeof entityConfig.storageImport !== 'string') {
|
|
370
|
+
throw new Error(`generateManagers: entity '${entityName}' requires 'storageImport' property`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!entityConfig.storageClass || typeof entityConfig.storageClass !== 'string') {
|
|
374
|
+
throw new Error(`generateManagers: entity '${entityName}' requires 'storageClass' property`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Create output directory if it doesn't exist
|
|
379
|
+
await mkdir(outputDir, { recursive: true })
|
|
380
|
+
|
|
381
|
+
// Generate files for each entity
|
|
382
|
+
for (const [entityName, entityConfig] of Object.entries(config.entities)) {
|
|
383
|
+
const isClassMode = entityConfig.classMode ?? globalOptions.classMode
|
|
384
|
+
// Use entity.ts for classMode (matches existing pattern), entityManager.ts for instance mode
|
|
385
|
+
const fileName = isClassMode ? `${entityName}.ts` : `${entityName}Manager.ts`
|
|
386
|
+
const filePath = join(outputDir, fileName)
|
|
387
|
+
|
|
388
|
+
// Generate source code
|
|
389
|
+
const source = generateManagerSource(entityName, entityConfig, globalOptions)
|
|
390
|
+
|
|
391
|
+
// Write file
|
|
392
|
+
await writeFile(filePath, source, 'utf-8')
|
|
393
|
+
|
|
394
|
+
generatedFiles.push(filePath)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return generatedFiles
|
|
398
|
+
}
|
package/src/gen/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gen Module - Code generation utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides UnifiedSchema and FieldMapper for generating admin interfaces
|
|
5
|
+
* from database schemas.
|
|
6
|
+
*
|
|
7
|
+
* @module gen
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// T00309: UnifiedSchema types and helpers
|
|
11
|
+
export {
|
|
12
|
+
UNIFIED_FIELD_TYPES,
|
|
13
|
+
isValidFieldType,
|
|
14
|
+
createFieldSchema,
|
|
15
|
+
createEntitySchema,
|
|
16
|
+
type UnifiedFieldType,
|
|
17
|
+
type UnifiedFieldReference,
|
|
18
|
+
type UnifiedFieldSchema,
|
|
19
|
+
type UnifiedEntitySchema,
|
|
20
|
+
} from './schema'
|
|
21
|
+
|
|
22
|
+
// T00310: FieldMapper - JSON Schema to UnifiedFieldSchema type conversion
|
|
23
|
+
export {
|
|
24
|
+
BASE_TYPE_MAPPINGS,
|
|
25
|
+
FORMAT_MAPPINGS,
|
|
26
|
+
getDefaultType,
|
|
27
|
+
type CustomMappings,
|
|
28
|
+
type SchemaProperty,
|
|
29
|
+
} from './FieldMapper'
|
|
30
|
+
|
|
31
|
+
// T00312: BaseConnector - Abstract base for schema connectors
|
|
32
|
+
export {
|
|
33
|
+
BaseConnector,
|
|
34
|
+
type ConnectorOptions,
|
|
35
|
+
type ParseWarning,
|
|
36
|
+
type ParseResult,
|
|
37
|
+
} from './connectors'
|
|
38
|
+
|
|
39
|
+
// T00313: ManualConnector - Inline entity/field definitions
|
|
40
|
+
export {
|
|
41
|
+
ManualConnector,
|
|
42
|
+
type ManualEntityInput,
|
|
43
|
+
type ManualFieldInput,
|
|
44
|
+
type ManualSourceInput,
|
|
45
|
+
} from './connectors'
|
|
46
|
+
|
|
47
|
+
// OpenAPIConnector - Parse OpenAPI 3.x specifications
|
|
48
|
+
export { OpenAPIConnector, type OpenAPIConnectorOptions, type OperationFilter } from './connectors'
|
|
49
|
+
|
|
50
|
+
// T00316: StorageProfileFactory - Type definitions for storage profile pattern
|
|
51
|
+
export { type StorageProfileFactory, type StorageProfileOptions } from './StorageProfileFactory'
|
|
52
|
+
|
|
53
|
+
// T00317: createManagers - Runtime factory for EntityManager creation
|
|
54
|
+
export { createManagers, type CreateManagersConfig, type EntityConfig } from './createManagers'
|
|
55
|
+
|
|
56
|
+
// T00318: Decorators - Per-entity field customization layer
|
|
57
|
+
export { applyDecorators, type FieldDecorator, type EntityDecorator } from './decorators'
|
|
58
|
+
|
|
59
|
+
// T00319: generateManagers - Build-time file generation for EntityManagers
|
|
60
|
+
export {
|
|
61
|
+
generateManagers,
|
|
62
|
+
fieldTypeToTsType,
|
|
63
|
+
generateEntityInterface,
|
|
64
|
+
type GenerateManagersConfig,
|
|
65
|
+
type GenerateManagersEntityConfig,
|
|
66
|
+
} from './generateManagers'
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Schema Types
|
|
3
|
+
*
|
|
4
|
+
* Core contracts that abstract differences between OpenAPI, Pydantic, and manual schema sources.
|
|
5
|
+
* These types provide a common format for entity and field definitions that can be used
|
|
6
|
+
* by connectors (schema sources) and generators (code output).
|
|
7
|
+
*
|
|
8
|
+
* @module gen/schema
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Field Type Enumeration
|
|
13
|
+
*
|
|
14
|
+
* Unified field types that abstract source-specific types (OpenAPI string/integer,
|
|
15
|
+
* Pydantic str/int, etc.) into a common set.
|
|
16
|
+
*/
|
|
17
|
+
export type UnifiedFieldType =
|
|
18
|
+
| 'text'
|
|
19
|
+
| 'number'
|
|
20
|
+
| 'boolean'
|
|
21
|
+
| 'date'
|
|
22
|
+
| 'datetime'
|
|
23
|
+
| 'email'
|
|
24
|
+
| 'url'
|
|
25
|
+
| 'uuid'
|
|
26
|
+
| 'array'
|
|
27
|
+
| 'object'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reference Definition
|
|
31
|
+
*
|
|
32
|
+
* Describes a relation to another entity for foreign key fields.
|
|
33
|
+
*/
|
|
34
|
+
export interface UnifiedFieldReference {
|
|
35
|
+
/** Target entity name (e.g., 'users', 'categories') */
|
|
36
|
+
entity: string
|
|
37
|
+
/** Field to display as label (e.g., 'name', 'title') */
|
|
38
|
+
labelField?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Unified Field Schema
|
|
43
|
+
*
|
|
44
|
+
* Common field definition that abstracts OpenAPI property, Pydantic field, or manual definition.
|
|
45
|
+
* Connectors transform their source-specific formats into this common shape.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Simple text field
|
|
49
|
+
* const nameField: UnifiedFieldSchema = {
|
|
50
|
+
* name: 'name',
|
|
51
|
+
* type: 'text',
|
|
52
|
+
* label: 'Full Name',
|
|
53
|
+
* required: true
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Enum field with options
|
|
58
|
+
* const statusField: UnifiedFieldSchema = {
|
|
59
|
+
* name: 'status',
|
|
60
|
+
* type: 'text',
|
|
61
|
+
* label: 'Status',
|
|
62
|
+
* enum: ['draft', 'published', 'archived'],
|
|
63
|
+
* default: 'draft'
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Foreign key with reference
|
|
68
|
+
* const authorField: UnifiedFieldSchema = {
|
|
69
|
+
* name: 'author_id',
|
|
70
|
+
* type: 'number',
|
|
71
|
+
* label: 'Author',
|
|
72
|
+
* reference: {
|
|
73
|
+
* entity: 'users',
|
|
74
|
+
* labelField: 'username'
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
*/
|
|
78
|
+
export interface UnifiedFieldSchema {
|
|
79
|
+
/** Field name (e.g., 'email', 'created_at') */
|
|
80
|
+
name: string
|
|
81
|
+
/** Unified field type */
|
|
82
|
+
type: UnifiedFieldType
|
|
83
|
+
/** Human-readable label (e.g., 'Email Address') */
|
|
84
|
+
label?: string
|
|
85
|
+
/** Whether field is required (default: false) */
|
|
86
|
+
required?: boolean
|
|
87
|
+
/** Whether field is read-only (default: false) */
|
|
88
|
+
readOnly?: boolean
|
|
89
|
+
/** Whether field should be hidden in UI (default: false) */
|
|
90
|
+
hidden?: boolean
|
|
91
|
+
/** Original format hint from source (e.g., 'date-time', 'uri', 'email') */
|
|
92
|
+
format?: string
|
|
93
|
+
/** Allowed values for enumeration fields */
|
|
94
|
+
enum?: string[]
|
|
95
|
+
/** Default value for the field */
|
|
96
|
+
default?: unknown
|
|
97
|
+
/** Relation to another entity */
|
|
98
|
+
reference?: UnifiedFieldReference
|
|
99
|
+
/** Project-specific extensions */
|
|
100
|
+
extensions?: Record<string, unknown>
|
|
101
|
+
/** Field display order */
|
|
102
|
+
order?: number
|
|
103
|
+
/** Additional properties */
|
|
104
|
+
[key: string]: unknown
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Unified Entity Schema
|
|
109
|
+
*
|
|
110
|
+
* Common entity definition that abstracts OpenAPI path/schema, Pydantic model, or manual definition.
|
|
111
|
+
* This is the primary contract between schema sources (connectors) and consumers (generators, runtime).
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // Complete entity schema
|
|
115
|
+
* const usersSchema: UnifiedEntitySchema = {
|
|
116
|
+
* name: 'users',
|
|
117
|
+
* endpoint: '/api/users',
|
|
118
|
+
* label: 'User',
|
|
119
|
+
* labelPlural: 'Users',
|
|
120
|
+
* labelField: 'username',
|
|
121
|
+
* routePrefix: 'user',
|
|
122
|
+
* idField: 'id',
|
|
123
|
+
* readOnly: false,
|
|
124
|
+
* fields: {
|
|
125
|
+
* id: { name: 'id', type: 'number', readOnly: true },
|
|
126
|
+
* username: { name: 'username', type: 'text', required: true },
|
|
127
|
+
* email: { name: 'email', type: 'email', required: true },
|
|
128
|
+
* created_at: { name: 'created_at', type: 'datetime', readOnly: true }
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
export interface UnifiedEntitySchema {
|
|
133
|
+
/** Entity name, typically plural lowercase (e.g., 'users', 'blog_posts') */
|
|
134
|
+
name: string
|
|
135
|
+
/** API endpoint path (e.g., '/users', '/api/v1/posts') */
|
|
136
|
+
endpoint: string
|
|
137
|
+
/** Human-readable singular label (e.g., 'User', 'Blog Post') */
|
|
138
|
+
label?: string
|
|
139
|
+
/** Human-readable plural label (e.g., 'Users', 'Blog Posts') */
|
|
140
|
+
labelPlural?: string
|
|
141
|
+
/** Field used as display label (e.g., 'name', 'title') */
|
|
142
|
+
labelField?: string
|
|
143
|
+
/** Route prefix for admin UI (e.g., 'user', 'blog-post') */
|
|
144
|
+
routePrefix?: string
|
|
145
|
+
/** Primary key field name (default: 'id') */
|
|
146
|
+
idField?: string
|
|
147
|
+
/** Whether entity is read-only (no create/update/delete) */
|
|
148
|
+
readOnly?: boolean
|
|
149
|
+
/** Field definitions keyed by field name */
|
|
150
|
+
fields: Record<string, UnifiedFieldSchema>
|
|
151
|
+
/** Project-specific extensions */
|
|
152
|
+
extensions?: Record<string, unknown>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Valid field types for UnifiedFieldSchema
|
|
157
|
+
*
|
|
158
|
+
* Used for validation and documentation. Maps to common UI input types:
|
|
159
|
+
* - text: Single-line text input
|
|
160
|
+
* - number: Numeric input
|
|
161
|
+
* - boolean: Checkbox/toggle
|
|
162
|
+
* - date: Date picker (date only)
|
|
163
|
+
* - datetime: Date-time picker
|
|
164
|
+
* - email: Email input with validation
|
|
165
|
+
* - url: URL input with validation
|
|
166
|
+
* - uuid: UUID text input
|
|
167
|
+
* - array: Multi-value field (tags, list)
|
|
168
|
+
* - object: Nested object (JSON editor or subform)
|
|
169
|
+
*/
|
|
170
|
+
export const UNIFIED_FIELD_TYPES = [
|
|
171
|
+
'text',
|
|
172
|
+
'number',
|
|
173
|
+
'boolean',
|
|
174
|
+
'date',
|
|
175
|
+
'datetime',
|
|
176
|
+
'email',
|
|
177
|
+
'url',
|
|
178
|
+
'uuid',
|
|
179
|
+
'array',
|
|
180
|
+
'object',
|
|
181
|
+
] as const
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a type is a valid UnifiedFieldType
|
|
185
|
+
*
|
|
186
|
+
* @param type - Type to validate
|
|
187
|
+
* @returns True if valid
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* isValidFieldType('text') // true
|
|
191
|
+
* isValidFieldType('string') // false (OpenAPI type, not unified)
|
|
192
|
+
*/
|
|
193
|
+
export function isValidFieldType(type: string): type is UnifiedFieldType {
|
|
194
|
+
return (UNIFIED_FIELD_TYPES as readonly string[]).includes(type)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a minimal field schema with defaults
|
|
199
|
+
*
|
|
200
|
+
* @param name - Field name
|
|
201
|
+
* @param type - Field type
|
|
202
|
+
* @param overrides - Additional field properties
|
|
203
|
+
* @returns Complete field schema
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const field = createFieldSchema('email', 'email', { required: true })
|
|
207
|
+
* // { name: 'email', type: 'email', required: true }
|
|
208
|
+
*/
|
|
209
|
+
export function createFieldSchema(
|
|
210
|
+
name: string,
|
|
211
|
+
type: UnifiedFieldType,
|
|
212
|
+
overrides: Partial<UnifiedFieldSchema> = {}
|
|
213
|
+
): UnifiedFieldSchema {
|
|
214
|
+
return {
|
|
215
|
+
name,
|
|
216
|
+
type,
|
|
217
|
+
...overrides,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a minimal entity schema with defaults
|
|
223
|
+
*
|
|
224
|
+
* @param name - Entity name
|
|
225
|
+
* @param endpoint - API endpoint
|
|
226
|
+
* @param fields - Field definitions
|
|
227
|
+
* @param overrides - Additional entity properties
|
|
228
|
+
* @returns Complete entity schema
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* const schema = createEntitySchema('users', '/api/users', {
|
|
232
|
+
* id: createFieldSchema('id', 'number', { readOnly: true }),
|
|
233
|
+
* name: createFieldSchema('name', 'text', { required: true })
|
|
234
|
+
* })
|
|
235
|
+
*/
|
|
236
|
+
export function createEntitySchema(
|
|
237
|
+
name: string,
|
|
238
|
+
endpoint: string,
|
|
239
|
+
fields: Record<string, UnifiedFieldSchema>,
|
|
240
|
+
overrides: Partial<UnifiedEntitySchema> = {}
|
|
241
|
+
): UnifiedEntitySchema {
|
|
242
|
+
return {
|
|
243
|
+
name,
|
|
244
|
+
endpoint,
|
|
245
|
+
fields,
|
|
246
|
+
...overrides,
|
|
247
|
+
}
|
|
248
|
+
}
|