@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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryExecutor - MongoDB-like query execution for arrays
|
|
3
|
+
*
|
|
4
|
+
* Provides client-side filtering with the same query syntax used for API calls.
|
|
5
|
+
* Pure functions, no Vue dependencies, null-safe operations.
|
|
6
|
+
*
|
|
7
|
+
* Supported operators:
|
|
8
|
+
* - Implicit: value → $eq, array → $in
|
|
9
|
+
* - Comparison: $eq, $ne, $gt, $gte, $lt, $lte
|
|
10
|
+
* - Set: $in, $nin, $like, $between
|
|
11
|
+
* - Logical: $or, $and
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Simple equality (implicit $eq)
|
|
15
|
+
* QueryExecutor.execute(items, { status: 'active' })
|
|
16
|
+
*
|
|
17
|
+
* // Array membership (implicit $in)
|
|
18
|
+
* QueryExecutor.execute(items, { status: ['active', 'pending'] })
|
|
19
|
+
*
|
|
20
|
+
* // Nested field access
|
|
21
|
+
* QueryExecutor.execute(items, { 'author.name': 'John' })
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Query condition can be a primitive, array, or operator object
|
|
26
|
+
*/
|
|
27
|
+
export type QueryCondition =
|
|
28
|
+
| string
|
|
29
|
+
| number
|
|
30
|
+
| boolean
|
|
31
|
+
| null
|
|
32
|
+
| undefined
|
|
33
|
+
| unknown[]
|
|
34
|
+
| QueryOperators
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Supported query operators
|
|
38
|
+
*/
|
|
39
|
+
export interface QueryOperators {
|
|
40
|
+
$eq?: unknown
|
|
41
|
+
$ne?: unknown
|
|
42
|
+
$gt?: number | string | Date
|
|
43
|
+
$gte?: number | string | Date
|
|
44
|
+
$lt?: number | string | Date
|
|
45
|
+
$lte?: number | string | Date
|
|
46
|
+
$in?: unknown[]
|
|
47
|
+
$nin?: unknown[]
|
|
48
|
+
$like?: string
|
|
49
|
+
$between?: [unknown, unknown]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Query object with field conditions and logical operators
|
|
54
|
+
*/
|
|
55
|
+
export interface QueryObject {
|
|
56
|
+
$or?: QueryObject[]
|
|
57
|
+
$and?: QueryObject[]
|
|
58
|
+
[field: string]: QueryCondition | QueryObject[] | undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Result of query execution
|
|
63
|
+
*/
|
|
64
|
+
export interface QueryResult<T> {
|
|
65
|
+
items: T[]
|
|
66
|
+
total: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get nested value from object using dot notation
|
|
71
|
+
*
|
|
72
|
+
* @param path - Dot-separated path (e.g., 'author.name')
|
|
73
|
+
* @param obj - Source object
|
|
74
|
+
* @returns Value at path or undefined if not found
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* getNestedValue('author.name', { author: { name: 'John' } }) // → 'John'
|
|
78
|
+
* getNestedValue('author.name', { author: null }) // → undefined
|
|
79
|
+
* getNestedValue('missing.path', {}) // → undefined
|
|
80
|
+
*/
|
|
81
|
+
export function getNestedValue(path: string, obj: unknown): unknown {
|
|
82
|
+
if (obj === null || obj === undefined) return undefined
|
|
83
|
+
if (!path || typeof path !== 'string') return undefined
|
|
84
|
+
|
|
85
|
+
const keys = path.split('.')
|
|
86
|
+
let current: unknown = obj
|
|
87
|
+
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
if (current === null || current === undefined) return undefined
|
|
90
|
+
if (typeof current !== 'object') return undefined
|
|
91
|
+
current = (current as Record<string, unknown>)[key]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return current
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a value matches a condition with an operator
|
|
99
|
+
*/
|
|
100
|
+
function matchCondition(itemValue: unknown, condition: QueryCondition): boolean {
|
|
101
|
+
// Null/undefined condition matches null/undefined values
|
|
102
|
+
if (condition === null || condition === undefined) {
|
|
103
|
+
return itemValue === condition
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Explicit operator object (e.g., { $eq: 'value' })
|
|
107
|
+
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
108
|
+
return matchOperator(itemValue, condition as QueryOperators)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Implicit $in for arrays
|
|
112
|
+
if (Array.isArray(condition)) {
|
|
113
|
+
return matchIn(itemValue, condition)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Implicit $eq for primitive values
|
|
117
|
+
return itemValue === condition
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Match a value against an operator object
|
|
122
|
+
*/
|
|
123
|
+
function matchOperator(itemValue: unknown, operators: QueryOperators): boolean {
|
|
124
|
+
for (const [op, expected] of Object.entries(operators)) {
|
|
125
|
+
switch (op) {
|
|
126
|
+
case '$eq':
|
|
127
|
+
if (itemValue !== expected) return false
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
case '$ne':
|
|
131
|
+
if (itemValue === expected) return false
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
case '$gt':
|
|
135
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
136
|
+
if (expected === null || expected === undefined) return false
|
|
137
|
+
if (typeof itemValue !== typeof expected) return false
|
|
138
|
+
if (!((itemValue as number | string) > (expected as number | string))) return false
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
case '$gte':
|
|
142
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
143
|
+
if (expected === null || expected === undefined) return false
|
|
144
|
+
if (typeof itemValue !== typeof expected) return false
|
|
145
|
+
if (!((itemValue as number | string) >= (expected as number | string))) return false
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
case '$lt':
|
|
149
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
150
|
+
if (expected === null || expected === undefined) return false
|
|
151
|
+
if (typeof itemValue !== typeof expected) return false
|
|
152
|
+
if (!((itemValue as number | string) < (expected as number | string))) return false
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
case '$lte':
|
|
156
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
157
|
+
if (expected === null || expected === undefined) return false
|
|
158
|
+
if (typeof itemValue !== typeof expected) return false
|
|
159
|
+
if (!((itemValue as number | string) <= (expected as number | string))) return false
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
case '$in':
|
|
163
|
+
if (!matchIn(itemValue, expected as unknown[])) return false
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
case '$nin':
|
|
167
|
+
if (!matchNin(itemValue, expected as unknown[])) return false
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
case '$like':
|
|
171
|
+
if (!matchLike(itemValue, expected as string)) return false
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
case '$between':
|
|
175
|
+
if (!matchBetween(itemValue, expected as [unknown, unknown])) return false
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
default:
|
|
179
|
+
// Unknown operator - skip (fail-safe, return true for this check)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Match $in operator - value in array
|
|
188
|
+
*/
|
|
189
|
+
function matchIn(itemValue: unknown, values: unknown[]): boolean {
|
|
190
|
+
if (!Array.isArray(values)) return false
|
|
191
|
+
if (values.length === 0) return false
|
|
192
|
+
return values.includes(itemValue)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Match $nin operator - value not in array
|
|
197
|
+
*/
|
|
198
|
+
function matchNin(itemValue: unknown, values: unknown[]): boolean {
|
|
199
|
+
if (!Array.isArray(values)) return true
|
|
200
|
+
if (values.length === 0) return true
|
|
201
|
+
return !values.includes(itemValue)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Match $like operator - case-insensitive substring match
|
|
206
|
+
*/
|
|
207
|
+
function matchLike(itemValue: unknown, pattern: string): boolean {
|
|
208
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
209
|
+
if (pattern === null || pattern === undefined) return false
|
|
210
|
+
const str = String(itemValue).toLowerCase()
|
|
211
|
+
const search = String(pattern).toLowerCase()
|
|
212
|
+
return str.includes(search)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Match $between operator - inclusive range check
|
|
217
|
+
*/
|
|
218
|
+
function matchBetween(itemValue: unknown, range: [unknown, unknown]): boolean {
|
|
219
|
+
if (itemValue === null || itemValue === undefined) return false
|
|
220
|
+
if (!Array.isArray(range) || range.length !== 2) return false
|
|
221
|
+
const [min, max] = range
|
|
222
|
+
return (itemValue as number) >= (min as number) && (itemValue as number) <= (max as number)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Match an item against a query object
|
|
227
|
+
*/
|
|
228
|
+
function matchQuery(item: unknown, query: QueryObject): boolean {
|
|
229
|
+
if (!query || typeof query !== 'object') return true
|
|
230
|
+
if (Object.keys(query).length === 0) return true
|
|
231
|
+
|
|
232
|
+
for (const [key, condition] of Object.entries(query)) {
|
|
233
|
+
// Logical operators
|
|
234
|
+
if (key === '$or') {
|
|
235
|
+
if (!matchOr(item, condition as QueryObject[])) return false
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (key === '$and') {
|
|
240
|
+
if (!matchAnd(item, condition as QueryObject[])) return false
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Field condition
|
|
245
|
+
const itemValue = getNestedValue(key, item)
|
|
246
|
+
if (!matchCondition(itemValue, condition as QueryCondition)) {
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Match $or operator - at least one condition must match
|
|
256
|
+
*/
|
|
257
|
+
function matchOr(item: unknown, conditions: QueryObject[]): boolean {
|
|
258
|
+
if (!Array.isArray(conditions)) return true
|
|
259
|
+
if (conditions.length === 0) return true
|
|
260
|
+
return conditions.some((cond) => matchQuery(item, cond))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Match $and operator - all conditions must match
|
|
265
|
+
*/
|
|
266
|
+
function matchAnd(item: unknown, conditions: QueryObject[]): boolean {
|
|
267
|
+
if (!Array.isArray(conditions)) return true
|
|
268
|
+
if (conditions.length === 0) return true
|
|
269
|
+
return conditions.every((cond) => matchQuery(item, cond))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* QueryExecutor - Static class for MongoDB-like array filtering
|
|
274
|
+
*/
|
|
275
|
+
export class QueryExecutor {
|
|
276
|
+
/**
|
|
277
|
+
* Execute query on array of items
|
|
278
|
+
*
|
|
279
|
+
* @param items - Source items to filter
|
|
280
|
+
* @param query - MongoDB-like query object
|
|
281
|
+
* @returns Query result with filtered items and total count
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* // Filter by status
|
|
285
|
+
* QueryExecutor.execute(books, { status: 'published' })
|
|
286
|
+
* // => { items: [...], total: 5 }
|
|
287
|
+
*
|
|
288
|
+
* // Filter by multiple values
|
|
289
|
+
* QueryExecutor.execute(books, { status: ['draft', 'published'] })
|
|
290
|
+
*
|
|
291
|
+
* // Empty query returns all items
|
|
292
|
+
* QueryExecutor.execute(books, {})
|
|
293
|
+
* // => { items: [...all books...], total: 100 }
|
|
294
|
+
*/
|
|
295
|
+
static execute<T>(items: T[], query: QueryObject): QueryResult<T> {
|
|
296
|
+
// Guard against invalid input
|
|
297
|
+
if (!Array.isArray(items)) {
|
|
298
|
+
return { items: [], total: 0 }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Empty query returns all items
|
|
302
|
+
if (!query || typeof query !== 'object' || Object.keys(query).length === 0) {
|
|
303
|
+
return { items: [...items], total: items.length }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const filtered = items.filter((item) => matchQuery(item, query))
|
|
308
|
+
return { items: filtered, total: filtered.length }
|
|
309
|
+
} catch {
|
|
310
|
+
// Return empty for malformed queries (fail-safe)
|
|
311
|
+
return { items: [], total: 0 }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check if single item matches query
|
|
317
|
+
*
|
|
318
|
+
* @param item - Item to check
|
|
319
|
+
* @param query - Query object
|
|
320
|
+
* @returns True if item matches query
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* const book = { title: 'Vue 3', status: 'published' }
|
|
324
|
+
* QueryExecutor.match(book, { status: 'published' }) // true
|
|
325
|
+
* QueryExecutor.match(book, { status: 'draft' }) // false
|
|
326
|
+
*/
|
|
327
|
+
static match<T>(item: T, query: QueryObject): boolean {
|
|
328
|
+
if (item === null || item === undefined) return false
|
|
329
|
+
|
|
330
|
+
// Empty query matches everything
|
|
331
|
+
if (!query || typeof query !== 'object' || Object.keys(query).length === 0) {
|
|
332
|
+
return true
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
return matchQuery(item, query)
|
|
337
|
+
} catch {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Module - MongoDB-like filtering for arrays
|
|
3
|
+
*
|
|
4
|
+
* Provides client-side filtering with the same query syntax used for API calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { QueryExecutor, getNestedValue } from './QueryExecutor'
|
|
8
|
+
export type { QueryCondition, QueryOperators, QueryObject, QueryResult } from './QueryExecutor'
|
|
9
|
+
|
|
10
|
+
export { FilterQuery } from './FilterQuery'
|
|
11
|
+
export type {
|
|
12
|
+
FilterQuerySource,
|
|
13
|
+
FilterOption,
|
|
14
|
+
ValueResolver,
|
|
15
|
+
FilterQueryOptions,
|
|
16
|
+
} from './FilterQuery'
|