@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,558 @@
1
+ /**
2
+ * Hook Bundle Pattern for qdadm
3
+ *
4
+ * A hook bundle is a function that registers multiple related hooks to implement
5
+ * a complete feature (soft delete, audit trail, timestamps, versioning).
6
+ *
7
+ * Bundles are composable and can be applied to any entity via the hook system.
8
+ * Unlike decorators that wrap a manager instance, bundles work globally through
9
+ * hooks that react to entity lifecycle events.
10
+ *
11
+ * @example
12
+ * // Define a custom bundle
13
+ * const withMyFeature = createHookBundle('myFeature', (register, context) => {
14
+ * register('entity:presave', (event) => {
15
+ * event.data.entity.myField = 'value'
16
+ * })
17
+ * })
18
+ *
19
+ * // Apply bundles to the kernel
20
+ * const cleanup = applyBundles(kernel.hooks, [
21
+ * withSoftDelete(),
22
+ * withTimestamps(),
23
+ * withVersioning()
24
+ * ])
25
+ *
26
+ * // Later: cleanup() // Removes all bundle hooks
27
+ */
28
+
29
+ import { HOOK_PRIORITY, type HookRegistry, type HookRegistrationOptions } from '../hooks/index'
30
+
31
+ /**
32
+ * Bundle register function type
33
+ */
34
+ export type BundleRegisterFn = (
35
+ hookName: string,
36
+ handler: (data: unknown) => unknown | Promise<unknown>,
37
+ options?: HookRegistrationOptions
38
+ ) => () => void
39
+
40
+ /**
41
+ * Bundle context passed to setup function
42
+ */
43
+ export interface BundleContext<T = Record<string, unknown>> {
44
+ target: string
45
+ options: T
46
+ hooks: HookRegistry
47
+ }
48
+
49
+ /**
50
+ * Bundle setup function type
51
+ */
52
+ export type BundleSetupFn<T = Record<string, unknown>> = (
53
+ register: BundleRegisterFn,
54
+ context: BundleContext<T>
55
+ ) => void
56
+
57
+ /**
58
+ * Bundle instance
59
+ */
60
+ export interface Bundle<T = Record<string, unknown>> {
61
+ name: string
62
+ options: T
63
+ setup: BundleSetupFn<T>
64
+ }
65
+
66
+ /**
67
+ * Bundle factory function type
68
+ */
69
+ export type BundleFactory<T = Record<string, unknown>> = (options?: T) => Bundle<T>
70
+
71
+ /**
72
+ * Apply bundle context
73
+ */
74
+ export interface ApplyBundleContext {
75
+ target?: string
76
+ }
77
+
78
+ /**
79
+ * Create a hook bundle factory
80
+ *
81
+ * A bundle factory creates bundle instances with optional configuration.
82
+ * The bundle itself registers hooks via the provided register function.
83
+ *
84
+ * @param name - Bundle name for identification and debugging
85
+ * @param setup - Setup function: (register, context) => void
86
+ * @returns Bundle factory: (options?) => Bundle
87
+ *
88
+ * @example
89
+ * const withAudit = createHookBundle('audit', (register, context) => {
90
+ * const { logger = console.log } = context.options
91
+ *
92
+ * register('entity:postsave', (event) => {
93
+ * logger('Saved:', event.data.entity)
94
+ * }, { priority: HOOK_PRIORITY.LOW })
95
+ * })
96
+ *
97
+ * // Usage: withAudit({ logger: myLogger })
98
+ */
99
+ export function createHookBundle<T = Record<string, unknown>>(
100
+ name: string,
101
+ setup: BundleSetupFn<T>
102
+ ): BundleFactory<T> {
103
+ if (!name || typeof name !== 'string') {
104
+ throw new Error('[createHookBundle] Bundle name must be a non-empty string')
105
+ }
106
+ if (typeof setup !== 'function') {
107
+ throw new Error('[createHookBundle] Setup must be a function')
108
+ }
109
+
110
+ // Return the bundle factory
111
+ return function bundleFactory(options: T = {} as T): Bundle<T> {
112
+ // Return the bundle instance (applied by applyBundle)
113
+ return {
114
+ name,
115
+ options,
116
+ setup,
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Apply a single bundle to a HookRegistry
123
+ *
124
+ * Registers all hooks defined by the bundle and returns a cleanup function.
125
+ *
126
+ * @param hooks - HookRegistry instance
127
+ * @param bundle - Bundle instance from bundle factory
128
+ * @param context - Additional context
129
+ * @returns Cleanup function to remove all bundle hooks
130
+ *
131
+ * @example
132
+ * const cleanup = applyBundle(kernel.hooks, withTimestamps())
133
+ * // ... later
134
+ * cleanup()
135
+ */
136
+ export function applyBundle<T>(
137
+ hooks: HookRegistry,
138
+ bundle: Bundle<T>,
139
+ context: ApplyBundleContext = {}
140
+ ): () => void {
141
+ if (!hooks || typeof hooks.register !== 'function') {
142
+ throw new Error('[applyBundle] First argument must be a HookRegistry')
143
+ }
144
+ if (!bundle || !bundle.setup) {
145
+ throw new Error('[applyBundle] Second argument must be a bundle instance')
146
+ }
147
+
148
+ const { target = '*' } = context
149
+ const unbindFns: Array<() => void> = []
150
+
151
+ // Create a register function that tracks all registrations
152
+ const register: BundleRegisterFn = (hookName, handler, options = {}) => {
153
+ // Prefix handler ID with bundle name for debugging
154
+ const id = options.id
155
+ ? `${bundle.name}:${options.id}`
156
+ : `${bundle.name}:${hookName.replace(/:/g, '-')}`
157
+
158
+ const unbind = hooks.register(hookName, handler, {
159
+ ...options,
160
+ id,
161
+ })
162
+ unbindFns.push(unbind)
163
+ return unbind
164
+ }
165
+
166
+ // Execute bundle setup with context
167
+ bundle.setup(register, {
168
+ target,
169
+ options: bundle.options,
170
+ hooks,
171
+ })
172
+
173
+ // Return cleanup function
174
+ return () => {
175
+ for (const unbind of unbindFns) {
176
+ unbind()
177
+ }
178
+ unbindFns.length = 0
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Apply multiple bundles to a HookRegistry
184
+ *
185
+ * Convenience function to apply multiple bundles at once.
186
+ *
187
+ * @param hooks - HookRegistry instance
188
+ * @param bundles - Array of bundle instances
189
+ * @param context - Context passed to each bundle
190
+ * @returns Combined cleanup function
191
+ *
192
+ * @example
193
+ * const cleanup = applyBundles(kernel.hooks, [
194
+ * withTimestamps(),
195
+ * withSoftDelete({ field: 'deleted_at' }),
196
+ * withVersioning()
197
+ * ])
198
+ */
199
+ export function applyBundles(
200
+ hooks: HookRegistry,
201
+ bundles: Bundle[],
202
+ context: ApplyBundleContext = {}
203
+ ): () => void {
204
+ if (!Array.isArray(bundles)) {
205
+ throw new Error('[applyBundles] Bundles must be an array')
206
+ }
207
+
208
+ const cleanupFns = bundles.map((bundle) => applyBundle(hooks, bundle, context))
209
+
210
+ return () => {
211
+ for (const cleanup of cleanupFns) {
212
+ cleanup()
213
+ }
214
+ }
215
+ }
216
+
217
+ // ============================================================================
218
+ // Built-in Bundles
219
+ // ============================================================================
220
+
221
+ /**
222
+ * Entity manager interface for bundles
223
+ */
224
+ interface BundleEntityManager {
225
+ name: string
226
+ idField?: string
227
+ patch: (id: unknown, data: Record<string, unknown>) => Promise<unknown>
228
+ }
229
+
230
+ /**
231
+ * Entity event data structure
232
+ */
233
+ interface EntityEventData {
234
+ entity: Record<string, unknown>
235
+ manager: BundleEntityManager
236
+ isNew?: boolean
237
+ originalEntity?: Record<string, unknown>
238
+ cancel?: () => void
239
+ }
240
+
241
+ /**
242
+ * Soft delete bundle options
243
+ */
244
+ export interface SoftDeleteOptions {
245
+ field?: string
246
+ timestamp?: () => string
247
+ }
248
+
249
+ /**
250
+ * withSoftDelete bundle
251
+ *
252
+ * Prevents actual deletion by setting a deleted_at timestamp instead.
253
+ * Also provides list filtering to exclude deleted records by default.
254
+ *
255
+ * Hooks registered:
256
+ * - `entity:predelete` - Intercepts delete, sets deleted_at via patch
257
+ * - `list:alter` - Filters out deleted records (unless includeDeleted option)
258
+ *
259
+ * @param options - Options
260
+ * @returns Bundle instance
261
+ *
262
+ * @example
263
+ * applyBundle(hooks, withSoftDelete())
264
+ * applyBundle(hooks, withSoftDelete({ field: 'removed_at' }))
265
+ */
266
+ export const withSoftDelete = createHookBundle<SoftDeleteOptions>(
267
+ 'softDelete',
268
+ (register, context) => {
269
+ const { field = 'deleted_at', timestamp = () => new Date().toISOString() } = context.options
270
+
271
+ // Intercept delete operations
272
+ register(
273
+ 'entity:predelete',
274
+ (event) => {
275
+ const { entity, manager, cancel } = (event as { data: EntityEventData }).data
276
+
277
+ // Get entity ID
278
+ const id = entity[manager.idField || 'id']
279
+
280
+ // Cancel the actual delete
281
+ cancel?.()
282
+
283
+ // Set soft-delete timestamp via patch
284
+ manager.patch(id, { [field]: timestamp() })
285
+ },
286
+ { priority: HOOK_PRIORITY.FIRST, id: 'intercept' }
287
+ )
288
+
289
+ // Filter deleted records from lists by default
290
+ register(
291
+ 'list:alter',
292
+ (config) => {
293
+ const cfg = config as Record<string, unknown>
294
+ // Add default filter unless explicitly including deleted
295
+ if (!cfg.includeDeleted) {
296
+ if (!cfg.filters) {
297
+ cfg.filters = []
298
+ }
299
+ // Add filter: field is null (not deleted)
300
+ ;(cfg.filters as unknown[]).push({
301
+ field,
302
+ operator: 'is_null',
303
+ value: true,
304
+ _bundleManaged: true,
305
+ })
306
+ }
307
+ return config
308
+ },
309
+ { priority: HOOK_PRIORITY.LOW, id: 'filter-list' }
310
+ )
311
+ }
312
+ )
313
+
314
+ /**
315
+ * Timestamp bundle options
316
+ */
317
+ export interface TimestampOptions {
318
+ createdAtField?: string
319
+ updatedAtField?: string
320
+ timestamp?: () => string
321
+ }
322
+
323
+ /**
324
+ * withTimestamps bundle
325
+ *
326
+ * Automatically manages created_at and updated_at timestamps.
327
+ *
328
+ * Hooks registered:
329
+ * - `entity:presave` - Sets created_at on new records, updated_at on all saves
330
+ *
331
+ * @param options - Options
332
+ * @returns Bundle instance
333
+ *
334
+ * @example
335
+ * applyBundle(hooks, withTimestamps())
336
+ * applyBundle(hooks, withTimestamps({ createdAtField: 'createdAt' }))
337
+ */
338
+ export const withTimestamps = createHookBundle<TimestampOptions>(
339
+ 'timestamps',
340
+ (register, context) => {
341
+ const {
342
+ createdAtField = 'created_at',
343
+ updatedAtField = 'updated_at',
344
+ timestamp = () => new Date().toISOString(),
345
+ } = context.options
346
+
347
+ register(
348
+ 'entity:presave',
349
+ (event) => {
350
+ const { entity, isNew } = (event as { data: EntityEventData }).data
351
+ const now = timestamp()
352
+
353
+ // Set created_at only on new records
354
+ if (isNew && !entity[createdAtField]) {
355
+ entity[createdAtField] = now
356
+ }
357
+
358
+ // Always update updated_at
359
+ entity[updatedAtField] = now
360
+ },
361
+ { priority: HOOK_PRIORITY.HIGH, id: 'set-timestamps' }
362
+ )
363
+ }
364
+ )
365
+
366
+ /**
367
+ * Versioning bundle options
368
+ */
369
+ export interface VersioningOptions {
370
+ field?: string
371
+ validateOnUpdate?: boolean
372
+ }
373
+
374
+ /**
375
+ * Version conflict error
376
+ */
377
+ export interface VersionConflictError extends Error {
378
+ name: 'VersionConflictError'
379
+ entityName: string
380
+ expectedVersion: number
381
+ actualVersion: number
382
+ }
383
+
384
+ /**
385
+ * withVersioning bundle
386
+ *
387
+ * Implements optimistic locking via version field.
388
+ * Increments version on each save and validates version matches for updates.
389
+ *
390
+ * Hooks registered:
391
+ * - `entity:presave` - Increments version and validates for conflicts
392
+ *
393
+ * @param options - Options
394
+ * @returns Bundle instance
395
+ *
396
+ * @example
397
+ * applyBundle(hooks, withVersioning())
398
+ * applyBundle(hooks, withVersioning({ field: '_version', validateOnUpdate: false }))
399
+ */
400
+ export const withVersioning = createHookBundle<VersioningOptions>(
401
+ 'versioning',
402
+ (register, context) => {
403
+ const { field = 'version', validateOnUpdate = true } = context.options
404
+
405
+ register(
406
+ 'entity:presave',
407
+ async (event) => {
408
+ const { entity, isNew, manager, originalEntity } = (event as { data: EntityEventData }).data
409
+
410
+ if (isNew) {
411
+ // New record: initialize version to 1
412
+ entity[field] = 1
413
+ } else {
414
+ // Existing record: validate and increment version
415
+ if (validateOnUpdate && originalEntity) {
416
+ const currentVersion = entity[field] as number | undefined
417
+ const originalVersion = originalEntity[field] as number | undefined
418
+
419
+ // Check for version conflict
420
+ if (currentVersion !== undefined && originalVersion !== undefined) {
421
+ if (currentVersion !== originalVersion) {
422
+ const error = new Error(
423
+ `Version conflict: expected ${originalVersion}, got ${currentVersion}`
424
+ ) as VersionConflictError
425
+ error.name = 'VersionConflictError'
426
+ error.entityName = manager.name
427
+ error.expectedVersion = originalVersion
428
+ error.actualVersion = currentVersion
429
+ throw error
430
+ }
431
+ }
432
+ }
433
+
434
+ // Increment version
435
+ const currentVersion = (entity[field] as number) || 0
436
+ entity[field] = currentVersion + 1
437
+ }
438
+ },
439
+ { priority: HOOK_PRIORITY.HIGH, id: 'manage-version' }
440
+ )
441
+ }
442
+ )
443
+
444
+ /**
445
+ * Audit log bundle options
446
+ */
447
+ export interface AuditLogOptions {
448
+ logger?: (entry: AuditLogEntry) => void
449
+ includeData?: boolean
450
+ includeDiff?: boolean
451
+ }
452
+
453
+ /**
454
+ * Audit log entry
455
+ */
456
+ export interface AuditLogEntry {
457
+ action: 'create' | 'update' | 'delete'
458
+ timestamp: string
459
+ entity: string
460
+ id: unknown
461
+ data?: Record<string, unknown>
462
+ changes?: Record<string, { from: unknown; to: unknown }>
463
+ }
464
+
465
+ /**
466
+ * withAuditLog bundle
467
+ *
468
+ * Logs all entity operations for audit trail.
469
+ *
470
+ * Hooks registered:
471
+ * - `entity:postsave` - Logs create/update operations
472
+ * - `entity:postdelete` - Logs delete operations
473
+ *
474
+ * @param options - Options
475
+ * @returns Bundle instance
476
+ *
477
+ * @example
478
+ * applyBundle(hooks, withAuditLog())
479
+ * applyBundle(hooks, withAuditLog({ logger: auditService.log, includeData: true }))
480
+ */
481
+ export const withAuditLog = createHookBundle<AuditLogOptions>('auditLog', (register, context) => {
482
+ const { logger = console.log, includeData = false, includeDiff = false } = context.options
483
+
484
+ const log = (action: AuditLogEntry['action'], details: Omit<AuditLogEntry, 'action' | 'timestamp'>) => {
485
+ logger({
486
+ action,
487
+ timestamp: new Date().toISOString(),
488
+ ...details,
489
+ } as AuditLogEntry)
490
+ }
491
+
492
+ register(
493
+ 'entity:postsave',
494
+ (event) => {
495
+ const { entity, isNew, manager, originalEntity } = (event as { data: EntityEventData }).data
496
+ const action: 'create' | 'update' = isNew ? 'create' : 'update'
497
+ const id = entity[manager.idField || 'id']
498
+
499
+ const details: Partial<AuditLogEntry> = {
500
+ entity: manager.name,
501
+ id,
502
+ }
503
+
504
+ if (includeData) {
505
+ details.data = entity
506
+ }
507
+
508
+ if (includeDiff && !isNew && originalEntity) {
509
+ details.changes = computeDiff(originalEntity, entity)
510
+ }
511
+
512
+ log(action, details as Omit<AuditLogEntry, 'action' | 'timestamp'>)
513
+ },
514
+ { priority: HOOK_PRIORITY.LAST, id: 'log-save' }
515
+ )
516
+
517
+ register(
518
+ 'entity:postdelete',
519
+ (event) => {
520
+ const { entity, manager } = (event as { data: EntityEventData }).data
521
+ const id = entity[manager.idField || 'id']
522
+
523
+ log('delete', {
524
+ entity: manager.name,
525
+ id,
526
+ ...(includeData ? { data: entity } : {}),
527
+ })
528
+ },
529
+ { priority: HOOK_PRIORITY.LAST, id: 'log-delete' }
530
+ )
531
+ })
532
+
533
+ /**
534
+ * Compute simple diff between two objects
535
+ *
536
+ * @param original - Original object
537
+ * @param updated - Updated object
538
+ * @returns Object with changed fields: { field: { from, to } }
539
+ */
540
+ function computeDiff(
541
+ original: Record<string, unknown>,
542
+ updated: Record<string, unknown>
543
+ ): Record<string, { from: unknown; to: unknown }> {
544
+ const diff: Record<string, { from: unknown; to: unknown }> = {}
545
+ const allKeys = new Set([...Object.keys(original), ...Object.keys(updated)])
546
+
547
+ for (const key of allKeys) {
548
+ const oldVal = original[key]
549
+ const newVal = updated[key]
550
+
551
+ // Simple equality check (handles primitives, not deep objects)
552
+ if (oldVal !== newVal) {
553
+ diff[key] = { from: oldVal, to: newVal }
554
+ }
555
+ }
556
+
557
+ return diff
558
+ }