@blueprint-ts/core 1.0.0
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/.editorconfig +508 -0
- package/.eslintrc.cjs +15 -0
- package/.prettierrc.json +8 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/docker-compose.yaml +8 -0
- package/docs/.vitepress/config.ts +68 -0
- package/docs/.vitepress/theme/Layout.vue +14 -0
- package/docs/.vitepress/theme/components/VersionSelector.vue +64 -0
- package/docs/.vitepress/theme/index.js +13 -0
- package/docs/index.md +70 -0
- package/docs/services/laravel/pagination.md +54 -0
- package/docs/services/laravel/requests.md +62 -0
- package/docs/services/requests/index.md +74 -0
- package/docs/vue/forms.md +326 -0
- package/docs/vue/requests/route-model-binding.md +66 -0
- package/docs/vue/state.md +293 -0
- package/env.d.ts +1 -0
- package/eslint.config.js +15 -0
- package/examples/files/7z2404-x64.exe +0 -0
- package/examples/index.html +14 -0
- package/examples/js/app.js +8 -0
- package/examples/js/router.js +22 -0
- package/examples/js/view/App.vue +49 -0
- package/examples/js/view/layout/DemoPage.vue +28 -0
- package/examples/js/view/pagination/Pagination.vue +28 -0
- package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +71 -0
- package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +54 -0
- package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +50 -0
- package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +57 -0
- package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +50 -0
- package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +63 -0
- package/examples/js/view/requests/Requests.vue +34 -0
- package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +36 -0
- package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +25 -0
- package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +15 -0
- package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +44 -0
- package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +34 -0
- package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +59 -0
- package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +21 -0
- package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +53 -0
- package/package.json +81 -0
- package/release-tool.json +7 -0
- package/src/helpers.ts +78 -0
- package/src/service/bulkRequests/BulkRequestEvent.enum.ts +4 -0
- package/src/service/bulkRequests/BulkRequestSender.ts +184 -0
- package/src/service/bulkRequests/BulkRequestWrapper.ts +49 -0
- package/src/service/bulkRequests/index.ts +6 -0
- package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +4 -0
- package/src/service/laravel/pagination/contracts/PaginationResponseBodyContract.ts +6 -0
- package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +32 -0
- package/src/service/laravel/pagination/index.ts +7 -0
- package/src/service/laravel/requests/JsonBaseRequest.ts +35 -0
- package/src/service/laravel/requests/PaginationJsonBaseRequest.ts +29 -0
- package/src/service/laravel/requests/index.ts +9 -0
- package/src/service/laravel/requests/responses/JsonResponse.ts +8 -0
- package/src/service/laravel/requests/responses/PaginationResponse.ts +16 -0
- package/src/service/pagination/InfiniteScroller.ts +21 -0
- package/src/service/pagination/Paginator.ts +149 -0
- package/src/service/pagination/contracts/PaginateableRequestContract.ts +13 -0
- package/src/service/pagination/contracts/PaginationDataDriverContract.ts +5 -0
- package/src/service/pagination/contracts/PaginationResponseContract.ts +7 -0
- package/src/service/pagination/contracts/PaginatorLoadDataOptions.ts +4 -0
- package/src/service/pagination/contracts/ViewDriverContract.ts +12 -0
- package/src/service/pagination/contracts/ViewDriverFactoryContract.ts +5 -0
- package/src/service/pagination/dataDrivers/ArrayDriver.ts +28 -0
- package/src/service/pagination/dtos/PaginationDataDto.ts +14 -0
- package/src/service/pagination/factories/VuePaginationDriverFactory.ts +9 -0
- package/src/service/pagination/frontendDrivers/VuePaginationDriver.ts +61 -0
- package/src/service/pagination/index.ts +16 -0
- package/src/service/persistenceDrivers/LocalStorageDriver.ts +22 -0
- package/src/service/persistenceDrivers/NonPersistentDriver.ts +12 -0
- package/src/service/persistenceDrivers/SessionStorageDriver.ts +22 -0
- package/src/service/persistenceDrivers/index.ts +8 -0
- package/src/service/persistenceDrivers/types/PersistenceDriver.ts +5 -0
- package/src/service/requests/BaseRequest.ts +197 -0
- package/src/service/requests/ErrorHandler.ts +64 -0
- package/src/service/requests/RequestEvents.enum.ts +3 -0
- package/src/service/requests/RequestMethod.enum.ts +8 -0
- package/src/service/requests/bodies/FormDataBody.ts +41 -0
- package/src/service/requests/bodies/JsonBody.ts +16 -0
- package/src/service/requests/contracts/AbortableRequestContract.ts +3 -0
- package/src/service/requests/contracts/BaseRequestContract.ts +36 -0
- package/src/service/requests/contracts/BodyContract.ts +7 -0
- package/src/service/requests/contracts/BodyFactoryContract.ts +5 -0
- package/src/service/requests/contracts/DriverConfigContract.ts +7 -0
- package/src/service/requests/contracts/HeadersContract.ts +5 -0
- package/src/service/requests/contracts/RequestDriverContract.ts +15 -0
- package/src/service/requests/contracts/RequestLoaderContract.ts +5 -0
- package/src/service/requests/contracts/RequestLoaderFactoryContract.ts +5 -0
- package/src/service/requests/contracts/ResponseContract.ts +7 -0
- package/src/service/requests/drivers/contracts/ResponseHandlerContract.ts +10 -0
- package/src/service/requests/drivers/fetch/FetchDriver.ts +115 -0
- package/src/service/requests/drivers/fetch/FetchResponse.ts +30 -0
- package/src/service/requests/exceptions/NoResponseReceivedException.ts +3 -0
- package/src/service/requests/exceptions/NotFoundException.ts +3 -0
- package/src/service/requests/exceptions/PageExpiredException.ts +3 -0
- package/src/service/requests/exceptions/ResponseBodyException.ts +15 -0
- package/src/service/requests/exceptions/ResponseException.ts +11 -0
- package/src/service/requests/exceptions/ServerErrorException.ts +3 -0
- package/src/service/requests/exceptions/UnauthorizedException.ts +3 -0
- package/src/service/requests/exceptions/ValidationException.ts +3 -0
- package/src/service/requests/exceptions/index.ts +19 -0
- package/src/service/requests/factories/FormDataFactory.ts +9 -0
- package/src/service/requests/factories/JsonBodyFactory.ts +9 -0
- package/src/service/requests/index.ts +50 -0
- package/src/service/requests/responses/BaseResponse.ts +41 -0
- package/src/service/requests/responses/BlobResponse.ts +19 -0
- package/src/service/requests/responses/JsonResponse.ts +15 -0
- package/src/service/requests/responses/PlainTextResponse.ts +15 -0
- package/src/service/support/DeferredPromise.ts +67 -0
- package/src/service/support/index.ts +3 -0
- package/src/vue/composables/useConfirmDialog.ts +59 -0
- package/src/vue/composables/useGlobalCheckbox.ts +145 -0
- package/src/vue/composables/useIsEmpty.ts +34 -0
- package/src/vue/composables/useIsOpen.ts +37 -0
- package/src/vue/composables/useIsOpenFromVar.ts +61 -0
- package/src/vue/composables/useModelWrapper.ts +24 -0
- package/src/vue/composables/useOnOpen.ts +34 -0
- package/src/vue/contracts/ModelValueOptions.ts +3 -0
- package/src/vue/contracts/ModelValueProps.ts +3 -0
- package/src/vue/forms/BaseForm.ts +1074 -0
- package/src/vue/forms/PropertyAwareArray.ts +78 -0
- package/src/vue/forms/index.ts +11 -0
- package/src/vue/forms/types/PersistedForm.ts +6 -0
- package/src/vue/forms/validation/ValidationMode.enum.ts +14 -0
- package/src/vue/forms/validation/index.ts +12 -0
- package/src/vue/forms/validation/rules/BaseRule.ts +7 -0
- package/src/vue/forms/validation/rules/ConfirmedRule.ts +39 -0
- package/src/vue/forms/validation/rules/MinRule.ts +61 -0
- package/src/vue/forms/validation/rules/RequiredRule.ts +19 -0
- package/src/vue/forms/validation/rules/UrlRule.ts +24 -0
- package/src/vue/forms/validation/types/BidirectionalRule.ts +11 -0
- package/src/vue/index.ts +14 -0
- package/src/vue/requests/factories/VueRequestLoaderFactory.ts +9 -0
- package/src/vue/requests/index.ts +5 -0
- package/src/vue/requests/loaders/VueRequestBatchLoader.ts +30 -0
- package/src/vue/requests/loaders/VueRequestLoader.ts +18 -0
- package/src/vue/router/routeModelBinding/RouteModelRequestResolver.ts +11 -0
- package/src/vue/router/routeModelBinding/defineRoute.ts +31 -0
- package/src/vue/router/routeModelBinding/index.ts +8 -0
- package/src/vue/router/routeModelBinding/installRouteInjection.ts +73 -0
- package/src/vue/router/routeModelBinding/types.ts +46 -0
- package/src/vue/state/State.ts +391 -0
- package/src/vue/state/index.ts +3 -0
- package/tests/service/helpers/mergeDeep.test.ts +53 -0
- package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +84 -0
- package/tests/service/laravel/requests/JsonBaseRequest.test.ts +43 -0
- package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +58 -0
- package/tests/service/laravel/requests/responses/JsonResponse.test.ts +59 -0
- package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +127 -0
- package/tests/service/pagination/dtos/PaginationDataDto.test.ts +35 -0
- package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +32 -0
- package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +66 -0
- package/tests/service/requests/ErrorHandler.test.ts +141 -0
- package/tsconfig.json +114 -0
- package/vite.config.ts +34 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { debounce } from 'lodash-es'
|
|
2
|
+
import { type PersistenceDriver } from '../../service/persistenceDrivers'
|
|
3
|
+
import { NonPersistentDriver } from '../../service/persistenceDrivers'
|
|
4
|
+
import { computed, ref, type Ref, watch, reactive } from 'vue'
|
|
5
|
+
|
|
6
|
+
export interface StateOptions {
|
|
7
|
+
persist?: boolean
|
|
8
|
+
persistSuffix?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Generic type for change handlers
|
|
12
|
+
type ChangeHandler<T> = (val: T, oldVal: T) => void
|
|
13
|
+
|
|
14
|
+
// Type for accessing nested properties with dot notation
|
|
15
|
+
type PathValue<T, P extends string> = P extends keyof T
|
|
16
|
+
? T[P]
|
|
17
|
+
: P extends `${infer K}.${infer Rest}`
|
|
18
|
+
? K extends keyof T
|
|
19
|
+
? T[K] extends Record<string, unknown>
|
|
20
|
+
? PathValue<T[K], Rest>
|
|
21
|
+
: never
|
|
22
|
+
: never
|
|
23
|
+
: never
|
|
24
|
+
|
|
25
|
+
// Helper type to get all possible paths in an object
|
|
26
|
+
type PathsToStringProps<T> = T extends object ? { [K in keyof T]: K extends string ? K | `${K}.${PathsToStringProps<T[K]>}` : never }[keyof T] : never
|
|
27
|
+
|
|
28
|
+
// Union type of all possible dot-notation paths in T
|
|
29
|
+
type Path<T> = keyof T | PathsToStringProps<T>
|
|
30
|
+
|
|
31
|
+
export abstract class State<T extends object> {
|
|
32
|
+
private readonly properties: { [K in keyof T]: Ref<T[K]> }
|
|
33
|
+
private readonly _initial: T
|
|
34
|
+
private readonly _persist: boolean
|
|
35
|
+
private readonly _persistKey: string
|
|
36
|
+
private _driver: PersistenceDriver
|
|
37
|
+
private _stateProxy: T | null = null
|
|
38
|
+
private _watchStopFunctions: Map<string, () => void> = new Map()
|
|
39
|
+
private _resetHandlers: Map<string, (() => void)[]> = new Map()
|
|
40
|
+
|
|
41
|
+
protected constructor(initial: T, options?: StateOptions) {
|
|
42
|
+
this._initial = initial
|
|
43
|
+
this._persist = !!options?.persist
|
|
44
|
+
const className = this.constructor.name
|
|
45
|
+
this._persistKey = className + (options?.persistSuffix ? `_${options.persistSuffix}` : '')
|
|
46
|
+
this._driver = this.getPersistenceDriver()
|
|
47
|
+
|
|
48
|
+
// --- Robust persistence load: invalidate if schema changed ---
|
|
49
|
+
let loaded: T | null = null
|
|
50
|
+
if (this._persist) {
|
|
51
|
+
loaded = this._driver.get<T>(this._persistKey)
|
|
52
|
+
if (loaded) {
|
|
53
|
+
const initialKeys = Object.keys(initial).sort()
|
|
54
|
+
const loadedKeys = Object.keys(loaded).sort()
|
|
55
|
+
const sameKeys = initialKeys.length === loadedKeys.length && initialKeys.every((k, i) => k === loadedKeys[i])
|
|
56
|
+
if (!sameKeys) {
|
|
57
|
+
// Schema mismatch: remove stale/corrupt data, use fresh
|
|
58
|
+
this._driver.remove(this._persistKey)
|
|
59
|
+
loaded = null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Use the valid loaded state, or fallback to initial
|
|
65
|
+
const base = loaded ?? initial
|
|
66
|
+
this.properties = {} as { [K in keyof T]: Ref<T[K]> }
|
|
67
|
+
const keys = Object.keys(base) as Array<keyof T>
|
|
68
|
+
|
|
69
|
+
for (const k of keys) {
|
|
70
|
+
// Use Vue's reactive for objects and arrays
|
|
71
|
+
let value = (base as T)[k]
|
|
72
|
+
if (typeof value === 'object' && value !== null) {
|
|
73
|
+
value = reactive(this.deepClone(value)) as T[typeof k]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const _ref = ref(value) as Ref<T[typeof k]>
|
|
77
|
+
|
|
78
|
+
this.properties[k] = computed({
|
|
79
|
+
get: () => _ref.value,
|
|
80
|
+
set: (val) => {
|
|
81
|
+
// Make objects and arrays reactive
|
|
82
|
+
if (typeof val === 'object' && val !== null) {
|
|
83
|
+
_ref.value = reactive(this.deepClone(val)) as T[typeof k]
|
|
84
|
+
} else {
|
|
85
|
+
_ref.value = val
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this._persist) {
|
|
89
|
+
this._driver.set(this._persistKey, this.export())
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}) as Ref<T[typeof k]>
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Safe deep clone that preserves types
|
|
98
|
+
*/
|
|
99
|
+
private deepClone<V>(value: V): V {
|
|
100
|
+
if (value === null || value === undefined) return value
|
|
101
|
+
if (typeof value !== 'object') return value
|
|
102
|
+
|
|
103
|
+
return JSON.parse(JSON.stringify(value)) as V
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get a proxy to the state that allows direct property access and setting
|
|
108
|
+
* without using .value
|
|
109
|
+
*/
|
|
110
|
+
public get state(): T {
|
|
111
|
+
if (!this._stateProxy) {
|
|
112
|
+
this._stateProxy = new Proxy({} as T, {
|
|
113
|
+
get: (_, prop: string | symbol) => {
|
|
114
|
+
if (typeof prop === 'symbol' || prop === 'toJSON') {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const key = prop as keyof T
|
|
119
|
+
if (key in this.properties) {
|
|
120
|
+
return this.properties[key].value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return undefined
|
|
124
|
+
},
|
|
125
|
+
set: (_, prop: string | symbol, value) => {
|
|
126
|
+
if (typeof prop === 'symbol') {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const key = prop as keyof T
|
|
131
|
+
if (key in this.properties) {
|
|
132
|
+
this.properties[key].value = value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return this._stateProxy
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add a subscription to specific properties or nested properties
|
|
145
|
+
* @param paths Path(s) to the property. Can be:
|
|
146
|
+
* - A single path (top-level key or nested with dot notation)
|
|
147
|
+
* - An array of paths to watch (triggers when any changes)
|
|
148
|
+
* @param handler Function to call when the property changes
|
|
149
|
+
* @param options Optional configuration for debounce and executeOnReset
|
|
150
|
+
* @returns A function to remove the subscription
|
|
151
|
+
*/
|
|
152
|
+
public subscribe<P extends Path<T>>(
|
|
153
|
+
paths: P,
|
|
154
|
+
handler: ChangeHandler<PathValue<T, P & string>>,
|
|
155
|
+
options?: { debounce?: number; executeOnReset?: boolean }
|
|
156
|
+
): () => void
|
|
157
|
+
public subscribe<P extends Path<T>>(
|
|
158
|
+
paths: P[],
|
|
159
|
+
handler: (changedPath: P, state: T) => void,
|
|
160
|
+
options?: { debounce?: number; executeOnReset?: boolean }
|
|
161
|
+
): () => void
|
|
162
|
+
public subscribe<P extends Path<T>>(
|
|
163
|
+
paths: P | P[],
|
|
164
|
+
handler: ChangeHandler<PathValue<T, P & string>> | ((changedPath: P, state: T) => void),
|
|
165
|
+
options?: { debounce?: number; executeOnReset?: boolean }
|
|
166
|
+
): () => void {
|
|
167
|
+
// Keep track of all watchers for cleanup
|
|
168
|
+
const stopFunctions: (() => void)[] = []
|
|
169
|
+
const resetHandlers: (() => void)[] = []
|
|
170
|
+
|
|
171
|
+
// If paths is an array, register handlers for each path
|
|
172
|
+
if (Array.isArray(paths)) {
|
|
173
|
+
for (const path of paths) {
|
|
174
|
+
// For arrays, we expect the handler to have the signature (changedPath, state) => void
|
|
175
|
+
const pathHandler = () => {
|
|
176
|
+
;(handler as (changedPath: P, state: T) => void)(path, this.export())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Store reset handler if needed
|
|
180
|
+
if (options?.executeOnReset) {
|
|
181
|
+
resetHandlers.push(pathHandler)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Register handler for this individual path
|
|
185
|
+
const stop = this.setupWatcher(path as string, pathHandler, options)
|
|
186
|
+
stopFunctions.push(stop)
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const pathHandler = handler as ChangeHandler<unknown>
|
|
190
|
+
|
|
191
|
+
// Store reset handler if needed
|
|
192
|
+
if (options?.executeOnReset) {
|
|
193
|
+
resetHandlers.push(() => pathHandler(undefined, undefined))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// For single paths, register directly with the provided handler
|
|
197
|
+
const stop = this.setupWatcher(paths as string, pathHandler, options)
|
|
198
|
+
stopFunctions.push(stop)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Store reset handlers for later execution
|
|
202
|
+
if (resetHandlers.length > 0) {
|
|
203
|
+
const resetId = Math.random().toString(36)
|
|
204
|
+
this._resetHandlers.set(resetId, resetHandlers)
|
|
205
|
+
|
|
206
|
+
stopFunctions.push(() => {
|
|
207
|
+
this._resetHandlers.delete(resetId)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Return a function that stops all watchers
|
|
212
|
+
return () => stopFunctions.forEach((stop) => stop())
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Internal method to set up a watcher for a specific path
|
|
217
|
+
*/
|
|
218
|
+
private setupWatcher(
|
|
219
|
+
path: string,
|
|
220
|
+
handler: (newVal?: unknown, oldVal?: unknown) => void,
|
|
221
|
+
options?: { debounce?: number; executeOnReset?: boolean }
|
|
222
|
+
): () => void {
|
|
223
|
+
const pathParts = path.split('.')
|
|
224
|
+
const debouncedHandler = options?.debounce && options.debounce > 0 ? debounce(handler, options.debounce) : undefined
|
|
225
|
+
|
|
226
|
+
const effectiveHandler = debouncedHandler || handler
|
|
227
|
+
|
|
228
|
+
// For top-level properties
|
|
229
|
+
if (pathParts.length === 1 && path in this.properties) {
|
|
230
|
+
const key = path as keyof T
|
|
231
|
+
|
|
232
|
+
// Set up watcher for this property
|
|
233
|
+
const stopWatch = watch(
|
|
234
|
+
() => this.properties[key].value,
|
|
235
|
+
(newVal, oldVal) => {
|
|
236
|
+
if (!this.isEqual(newVal, oldVal)) {
|
|
237
|
+
effectiveHandler(newVal, oldVal)
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{ deep: true }
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Save stop function for cleanup
|
|
244
|
+
const watchId = `prop:${String(key)}`
|
|
245
|
+
this._watchStopFunctions.set(watchId, stopWatch)
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
if (this._watchStopFunctions.has(watchId)) {
|
|
249
|
+
this._watchStopFunctions.get(watchId)!()
|
|
250
|
+
this._watchStopFunctions.delete(watchId)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// For nested properties
|
|
256
|
+
const topLevelKey = pathParts[0] as keyof T
|
|
257
|
+
|
|
258
|
+
if (topLevelKey in this.properties) {
|
|
259
|
+
// Create a nested property getter
|
|
260
|
+
const getter = () => {
|
|
261
|
+
let obj = this.properties[topLevelKey].value
|
|
262
|
+
|
|
263
|
+
for (let i = 1; i < pathParts.length; i++) {
|
|
264
|
+
if (!obj || typeof obj !== 'object') return undefined
|
|
265
|
+
obj = (obj as any)[pathParts[i]]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return obj
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Set up watcher for this nested property
|
|
272
|
+
const stopWatch = watch(
|
|
273
|
+
getter,
|
|
274
|
+
(newVal, oldVal) => {
|
|
275
|
+
if (!this.isEqual(newVal, oldVal)) {
|
|
276
|
+
effectiveHandler(newVal, oldVal)
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{ deep: true }
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// Save stop function for cleanup
|
|
283
|
+
const watchId = `nested:${String(path)}`
|
|
284
|
+
this._watchStopFunctions.set(watchId, stopWatch)
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
if (this._watchStopFunctions.has(watchId)) {
|
|
288
|
+
this._watchStopFunctions.get(watchId)!()
|
|
289
|
+
this._watchStopFunctions.delete(watchId)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// If path doesn't exist, return a no-op
|
|
295
|
+
return () => {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Simple deep equality check
|
|
300
|
+
*/
|
|
301
|
+
private isEqual(a: unknown, b: unknown): boolean {
|
|
302
|
+
if (a === b) return true
|
|
303
|
+
|
|
304
|
+
if (a === null || b === null) return false
|
|
305
|
+
if (a === undefined || b === undefined) return false
|
|
306
|
+
|
|
307
|
+
if (typeof a !== typeof b) return false
|
|
308
|
+
|
|
309
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
310
|
+
const aArray = Array.isArray(a)
|
|
311
|
+
const bArray = Array.isArray(b)
|
|
312
|
+
|
|
313
|
+
if (aArray !== bArray) return false
|
|
314
|
+
|
|
315
|
+
if (aArray && bArray) {
|
|
316
|
+
const arrayA = a as unknown[]
|
|
317
|
+
const arrayB = b as unknown[]
|
|
318
|
+
if (arrayA.length !== arrayB.length) return false
|
|
319
|
+
return arrayA.every((val, i) => this.isEqual(val, arrayB[i]))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const objA = a as Record<string, unknown>
|
|
323
|
+
const objB = b as Record<string, unknown>
|
|
324
|
+
|
|
325
|
+
const keysA = Object.keys(objA).sort()
|
|
326
|
+
const keysB = Object.keys(objB).sort()
|
|
327
|
+
|
|
328
|
+
if (keysA.length !== keysB.length) return false
|
|
329
|
+
if (!keysA.every((k, i) => k === keysB[i])) return false
|
|
330
|
+
|
|
331
|
+
return keysA.every((k) => this.isEqual(objA[k], objB[k]))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
protected getPersistenceDriver(): PersistenceDriver {
|
|
338
|
+
return new NonPersistentDriver()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public export(): T {
|
|
342
|
+
const out = {} as T
|
|
343
|
+
for (const k in this.properties) {
|
|
344
|
+
const key = k as keyof T
|
|
345
|
+
out[key] = this.deepClone(this.properties[key].value)
|
|
346
|
+
}
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public import(data: Partial<T>): void {
|
|
351
|
+
for (const k in data) {
|
|
352
|
+
if (k in this.properties) {
|
|
353
|
+
const key = k as keyof T
|
|
354
|
+
this.properties[key].value = data[key] as T[typeof key]
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (this._persist) {
|
|
358
|
+
this._driver.set(this._persistKey, this.export())
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public reset(): void {
|
|
363
|
+
this.import(this._initial)
|
|
364
|
+
|
|
365
|
+
// Execute reset handlers
|
|
366
|
+
for (const handlers of this._resetHandlers.values()) {
|
|
367
|
+
for (const handler of handlers) {
|
|
368
|
+
handler()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public get persistKey(): string {
|
|
374
|
+
return this._persistKey
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Clean up all watchers when the state is no longer needed
|
|
379
|
+
*/
|
|
380
|
+
public destroy(): void {
|
|
381
|
+
// Stop all watchers
|
|
382
|
+
for (const stopFn of this._watchStopFunctions.values()) {
|
|
383
|
+
stopFn()
|
|
384
|
+
}
|
|
385
|
+
this._watchStopFunctions.clear()
|
|
386
|
+
this._resetHandlers.clear()
|
|
387
|
+
|
|
388
|
+
// Remove reference to proxy
|
|
389
|
+
this._stateProxy = null
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mergeDeep } from '../../../src/helpers'
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('mergeDeep', () => {
|
|
5
|
+
it('should merge nested objects and replace primitive values', () => {
|
|
6
|
+
const previous = {
|
|
7
|
+
filter: {
|
|
8
|
+
role: 'supplier',
|
|
9
|
+
search_text: ''
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const params = {
|
|
14
|
+
filter: {
|
|
15
|
+
search_text: 'ad'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = mergeDeep(previous, params)
|
|
20
|
+
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
filter: {
|
|
23
|
+
role: 'supplier',
|
|
24
|
+
search_text: 'ad'
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should deeply merge multiple levels', () => {
|
|
30
|
+
const a = { a: { b: { c: 1 }, d: 2 } }
|
|
31
|
+
const b = { a: { b: { c: 3 }, e: 4 } }
|
|
32
|
+
|
|
33
|
+
const result = mergeDeep( a, b)
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
a: {
|
|
37
|
+
b: { c: 3 },
|
|
38
|
+
d: 2,
|
|
39
|
+
e: 4
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should not mutate the original objects', () => {
|
|
45
|
+
const a = { foo: { bar: 'baz' } }
|
|
46
|
+
const b = { foo: { bar: 'qux' } }
|
|
47
|
+
|
|
48
|
+
const result = mergeDeep({}, a, b)
|
|
49
|
+
|
|
50
|
+
expect(result).toEqual({ foo: { bar: 'qux' } })
|
|
51
|
+
expect(a.foo.bar).toBe('baz') // confirm a was not mutated
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { RequestDriver } from '../../../../../src/service/laravel/pagination'
|
|
3
|
+
import { PaginationDataDto } from '../../../../../src/service/pagination'
|
|
4
|
+
import { PaginationResponse } from '../../../../../src/service/laravel/requests'
|
|
5
|
+
|
|
6
|
+
// Mocks für Schnittstellen und Klassen
|
|
7
|
+
class MockRequest {
|
|
8
|
+
setPaginationParams = vi.fn().mockReturnThis()
|
|
9
|
+
send = vi.fn()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class MockPaginationResponse extends PaginationResponse<any> {
|
|
13
|
+
protected resolveBody = vi.fn().mockResolvedValue(this.mockBody)
|
|
14
|
+
private mockBody
|
|
15
|
+
|
|
16
|
+
constructor(data: any[]) {
|
|
17
|
+
super()
|
|
18
|
+
this.mockBody = data
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getTotal() {
|
|
22
|
+
return this.mockBody.length
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getData() {
|
|
26
|
+
return this.mockBody
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('RequestDriver', () => {
|
|
31
|
+
let mockRequest: MockRequest
|
|
32
|
+
let requestDriver: RequestDriver<typeof mockRequest>
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockRequest = new MockRequest()
|
|
36
|
+
requestDriver = new RequestDriver(mockRequest as any)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('should call setPaginationParams with correct parameters', async () => {
|
|
40
|
+
const pageNumber = 1
|
|
41
|
+
const pageSize = 10
|
|
42
|
+
|
|
43
|
+
// Mock PaginationResponse
|
|
44
|
+
const mockResponse = new MockPaginationResponse([{ id: 1 }, { id: 2 }])
|
|
45
|
+
|
|
46
|
+
// Mock send method to resolve to a mock PaginationResponse
|
|
47
|
+
mockRequest.send.mockResolvedValue(mockResponse)
|
|
48
|
+
await requestDriver.get(pageNumber, pageSize)
|
|
49
|
+
|
|
50
|
+
expect(mockRequest.setPaginationParams).toHaveBeenCalledWith(
|
|
51
|
+
pageNumber,
|
|
52
|
+
pageSize
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('should correctly map response to PaginationDataDto', async () => {
|
|
57
|
+
const pageNumber = 1
|
|
58
|
+
const pageSize = 10
|
|
59
|
+
|
|
60
|
+
const mockData = [{ id: 1 }, { id: 2 }]
|
|
61
|
+
const mockResponse = new MockPaginationResponse(mockData)
|
|
62
|
+
|
|
63
|
+
// Mock send method to resolve to a mock PaginationResponse
|
|
64
|
+
mockRequest.send.mockResolvedValue(mockResponse)
|
|
65
|
+
|
|
66
|
+
const result = await requestDriver.get(pageNumber, pageSize)
|
|
67
|
+
|
|
68
|
+
expect(result).toBeInstanceOf(PaginationDataDto)
|
|
69
|
+
expect(result.data).toEqual(mockData)
|
|
70
|
+
expect(result.total).toBe(mockData.length)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('should handle request errors gracefully', async () => {
|
|
74
|
+
const pageNumber = 1
|
|
75
|
+
const pageSize = 10
|
|
76
|
+
|
|
77
|
+
const error = new Error('Request failed')
|
|
78
|
+
mockRequest.send.mockRejectedValue(error)
|
|
79
|
+
|
|
80
|
+
await expect(requestDriver.get(pageNumber, pageSize)).rejects.toThrowError(
|
|
81
|
+
'Request failed'
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { JsonBaseRequest } from '../../../../src/service/laravel/requests'
|
|
3
|
+
import { JsonResponse } from '../../../../src/service/laravel/requests'
|
|
4
|
+
import { JsonBodyFactory } from '../../../../src/service/requests'
|
|
5
|
+
|
|
6
|
+
interface TestResource {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class TestJsonBaseRequest extends JsonBaseRequest<TestResource> {
|
|
12
|
+
// Abstrakte Klasse muss hier für Tests konkretisiert werden
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('JsonBaseRequest', () => {
|
|
16
|
+
let request: TestJsonBaseRequest;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Instanziiere die Testklasse vor jedem Test
|
|
20
|
+
request = new TestJsonBaseRequest();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should return a JsonResponse instance from getResponse', () => {
|
|
24
|
+
const response = request.getResponse();
|
|
25
|
+
// Überprüfe, ob getResponse eine JsonResponse-Instanz zurückgibt
|
|
26
|
+
expect(response).toBeInstanceOf(JsonResponse);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should return a JsonBodyFactory instance from getRequestBodyFactory', () => {
|
|
30
|
+
const bodyFactory = request.getRequestBodyFactory();
|
|
31
|
+
// Überprüfe, ob getRequestBodyFactory eine JsonBodyFactory-Instanz zurückgibt
|
|
32
|
+
expect(bodyFactory).toBeInstanceOf(JsonBodyFactory);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should handle generic type definitions for JsonResponse and factory', () => {
|
|
36
|
+
// Testen des generischen Typsystems
|
|
37
|
+
const response = request.getResponse();
|
|
38
|
+
const bodyFactory = request.getRequestBodyFactory();
|
|
39
|
+
|
|
40
|
+
expect(response).toBeInstanceOf(JsonResponse<TestResource>);
|
|
41
|
+
expect(bodyFactory).toBeInstanceOf(JsonBodyFactory);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { PaginationJsonBaseRequest } from '../../../../src/service/laravel/requests'
|
|
3
|
+
import { PaginationResponse } from '../../../../src/service/laravel/requests'
|
|
4
|
+
|
|
5
|
+
interface TestResource {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TestRequestParams {
|
|
11
|
+
page_number: number;
|
|
12
|
+
page_size: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class TestPaginationJsonBaseRequest extends PaginationJsonBaseRequest<TestResource, TestRequestParams> {
|
|
16
|
+
// Abstrakte Klasse für Tests konkretisiert
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('PaginationJsonBaseRequest', () => {
|
|
20
|
+
let request: TestPaginationJsonBaseRequest;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Instanziiere die Testklasse vor jedem Test
|
|
24
|
+
request = new TestPaginationJsonBaseRequest();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should return a PaginationResponse instance from getResponse', () => {
|
|
28
|
+
const response = request.getResponse();
|
|
29
|
+
// Überprüfe, ob getResponse eine PaginationResponse-Instanz zurückgibt
|
|
30
|
+
expect(response).toBeInstanceOf(PaginationResponse);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should correctly set pagination parameters with setPaginationParams', () => {
|
|
34
|
+
const page = 2;
|
|
35
|
+
const size = 50;
|
|
36
|
+
|
|
37
|
+
// Mock der `withParams`-Methode, falls sie von `BaseRequest` stammt
|
|
38
|
+
const mockWithParams = vi.spyOn(request, 'withParams');
|
|
39
|
+
|
|
40
|
+
// Rufe `setPaginationParams` auf
|
|
41
|
+
const updatedRequest = request.setPaginationParams(page, size);
|
|
42
|
+
|
|
43
|
+
expect(updatedRequest).toBe(request); // Überprüft die fluente API
|
|
44
|
+
expect(mockWithParams).toHaveBeenCalledWith({
|
|
45
|
+
page_number: page,
|
|
46
|
+
page_size: size,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Aufräumen von Mocks
|
|
50
|
+
mockWithParams.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should handle generic type definitions for getResponse', () => {
|
|
54
|
+
// Testen des generischen Typsystems
|
|
55
|
+
const response = request.getResponse();
|
|
56
|
+
expect(response).toBeInstanceOf(PaginationResponse<TestResource>);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { JsonResponse } from '../../../../../src/service/laravel/requests'
|
|
3
|
+
|
|
4
|
+
describe('JsonResponse', () => {
|
|
5
|
+
let jsonResponse: JsonResponse<any>; // Generische Instanz für flexibles Testen
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jsonResponse = new JsonResponse();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should correctly return data from response body', async () => {
|
|
12
|
+
const mockBody = {
|
|
13
|
+
data: { id: 1, name: 'Test Resource' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Simuliere die Methode `setResponse` mit einem Mock-ResponseHandler
|
|
17
|
+
const mockResponseHandler = {
|
|
18
|
+
async json() {
|
|
19
|
+
return mockBody;
|
|
20
|
+
},
|
|
21
|
+
getRawResponse: vi.fn(),
|
|
22
|
+
getStatusCode: vi.fn(),
|
|
23
|
+
getHeaders: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await jsonResponse.setResponse(mockResponseHandler as any);
|
|
27
|
+
|
|
28
|
+
// Rufe die Methode `getData` auf und erwarte die korrekten Daten
|
|
29
|
+
const data = jsonResponse.getData();
|
|
30
|
+
expect(data).toEqual(mockBody.data);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should throw error if body is not set before calling getData', () => {
|
|
34
|
+
// Direkt getData aufrufen ohne vorher einen Response-Body zu setzen
|
|
35
|
+
expect(() => jsonResponse.getData()).toThrowError('Response body is not set');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should correctly handle nested resource data', async () => {
|
|
39
|
+
const mockBody = {
|
|
40
|
+
data: { id: 42, attributes: { title: 'Nested Test' } },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Simuliere die Methode `setResponse` mit einem Mock-ResponseHandler
|
|
44
|
+
const mockResponseHandler = {
|
|
45
|
+
async json() {
|
|
46
|
+
return mockBody;
|
|
47
|
+
},
|
|
48
|
+
getRawResponse: vi.fn(),
|
|
49
|
+
getStatusCode: vi.fn(),
|
|
50
|
+
getHeaders: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await jsonResponse.setResponse(mockResponseHandler as any);
|
|
54
|
+
|
|
55
|
+
// Rufe die Methode `getData` auf und überprüfe, ob die geschachtelten Daten korrekt verarbeitet werden
|
|
56
|
+
const data = jsonResponse.getData();
|
|
57
|
+
expect(data).toEqual(mockBody.data);
|
|
58
|
+
});
|
|
59
|
+
});
|