@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.
Files changed (158) hide show
  1. package/.editorconfig +508 -0
  2. package/.eslintrc.cjs +15 -0
  3. package/.prettierrc.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.md +1 -0
  6. package/docker-compose.yaml +8 -0
  7. package/docs/.vitepress/config.ts +68 -0
  8. package/docs/.vitepress/theme/Layout.vue +14 -0
  9. package/docs/.vitepress/theme/components/VersionSelector.vue +64 -0
  10. package/docs/.vitepress/theme/index.js +13 -0
  11. package/docs/index.md +70 -0
  12. package/docs/services/laravel/pagination.md +54 -0
  13. package/docs/services/laravel/requests.md +62 -0
  14. package/docs/services/requests/index.md +74 -0
  15. package/docs/vue/forms.md +326 -0
  16. package/docs/vue/requests/route-model-binding.md +66 -0
  17. package/docs/vue/state.md +293 -0
  18. package/env.d.ts +1 -0
  19. package/eslint.config.js +15 -0
  20. package/examples/files/7z2404-x64.exe +0 -0
  21. package/examples/index.html +14 -0
  22. package/examples/js/app.js +8 -0
  23. package/examples/js/router.js +22 -0
  24. package/examples/js/view/App.vue +49 -0
  25. package/examples/js/view/layout/DemoPage.vue +28 -0
  26. package/examples/js/view/pagination/Pagination.vue +28 -0
  27. package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +71 -0
  28. package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +54 -0
  29. package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +50 -0
  30. package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +57 -0
  31. package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +50 -0
  32. package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +63 -0
  33. package/examples/js/view/requests/Requests.vue +34 -0
  34. package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +36 -0
  35. package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +25 -0
  36. package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +15 -0
  37. package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +44 -0
  38. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +34 -0
  39. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +59 -0
  40. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +21 -0
  41. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +53 -0
  42. package/package.json +81 -0
  43. package/release-tool.json +7 -0
  44. package/src/helpers.ts +78 -0
  45. package/src/service/bulkRequests/BulkRequestEvent.enum.ts +4 -0
  46. package/src/service/bulkRequests/BulkRequestSender.ts +184 -0
  47. package/src/service/bulkRequests/BulkRequestWrapper.ts +49 -0
  48. package/src/service/bulkRequests/index.ts +6 -0
  49. package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +4 -0
  50. package/src/service/laravel/pagination/contracts/PaginationResponseBodyContract.ts +6 -0
  51. package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +32 -0
  52. package/src/service/laravel/pagination/index.ts +7 -0
  53. package/src/service/laravel/requests/JsonBaseRequest.ts +35 -0
  54. package/src/service/laravel/requests/PaginationJsonBaseRequest.ts +29 -0
  55. package/src/service/laravel/requests/index.ts +9 -0
  56. package/src/service/laravel/requests/responses/JsonResponse.ts +8 -0
  57. package/src/service/laravel/requests/responses/PaginationResponse.ts +16 -0
  58. package/src/service/pagination/InfiniteScroller.ts +21 -0
  59. package/src/service/pagination/Paginator.ts +149 -0
  60. package/src/service/pagination/contracts/PaginateableRequestContract.ts +13 -0
  61. package/src/service/pagination/contracts/PaginationDataDriverContract.ts +5 -0
  62. package/src/service/pagination/contracts/PaginationResponseContract.ts +7 -0
  63. package/src/service/pagination/contracts/PaginatorLoadDataOptions.ts +4 -0
  64. package/src/service/pagination/contracts/ViewDriverContract.ts +12 -0
  65. package/src/service/pagination/contracts/ViewDriverFactoryContract.ts +5 -0
  66. package/src/service/pagination/dataDrivers/ArrayDriver.ts +28 -0
  67. package/src/service/pagination/dtos/PaginationDataDto.ts +14 -0
  68. package/src/service/pagination/factories/VuePaginationDriverFactory.ts +9 -0
  69. package/src/service/pagination/frontendDrivers/VuePaginationDriver.ts +61 -0
  70. package/src/service/pagination/index.ts +16 -0
  71. package/src/service/persistenceDrivers/LocalStorageDriver.ts +22 -0
  72. package/src/service/persistenceDrivers/NonPersistentDriver.ts +12 -0
  73. package/src/service/persistenceDrivers/SessionStorageDriver.ts +22 -0
  74. package/src/service/persistenceDrivers/index.ts +8 -0
  75. package/src/service/persistenceDrivers/types/PersistenceDriver.ts +5 -0
  76. package/src/service/requests/BaseRequest.ts +197 -0
  77. package/src/service/requests/ErrorHandler.ts +64 -0
  78. package/src/service/requests/RequestEvents.enum.ts +3 -0
  79. package/src/service/requests/RequestMethod.enum.ts +8 -0
  80. package/src/service/requests/bodies/FormDataBody.ts +41 -0
  81. package/src/service/requests/bodies/JsonBody.ts +16 -0
  82. package/src/service/requests/contracts/AbortableRequestContract.ts +3 -0
  83. package/src/service/requests/contracts/BaseRequestContract.ts +36 -0
  84. package/src/service/requests/contracts/BodyContract.ts +7 -0
  85. package/src/service/requests/contracts/BodyFactoryContract.ts +5 -0
  86. package/src/service/requests/contracts/DriverConfigContract.ts +7 -0
  87. package/src/service/requests/contracts/HeadersContract.ts +5 -0
  88. package/src/service/requests/contracts/RequestDriverContract.ts +15 -0
  89. package/src/service/requests/contracts/RequestLoaderContract.ts +5 -0
  90. package/src/service/requests/contracts/RequestLoaderFactoryContract.ts +5 -0
  91. package/src/service/requests/contracts/ResponseContract.ts +7 -0
  92. package/src/service/requests/drivers/contracts/ResponseHandlerContract.ts +10 -0
  93. package/src/service/requests/drivers/fetch/FetchDriver.ts +115 -0
  94. package/src/service/requests/drivers/fetch/FetchResponse.ts +30 -0
  95. package/src/service/requests/exceptions/NoResponseReceivedException.ts +3 -0
  96. package/src/service/requests/exceptions/NotFoundException.ts +3 -0
  97. package/src/service/requests/exceptions/PageExpiredException.ts +3 -0
  98. package/src/service/requests/exceptions/ResponseBodyException.ts +15 -0
  99. package/src/service/requests/exceptions/ResponseException.ts +11 -0
  100. package/src/service/requests/exceptions/ServerErrorException.ts +3 -0
  101. package/src/service/requests/exceptions/UnauthorizedException.ts +3 -0
  102. package/src/service/requests/exceptions/ValidationException.ts +3 -0
  103. package/src/service/requests/exceptions/index.ts +19 -0
  104. package/src/service/requests/factories/FormDataFactory.ts +9 -0
  105. package/src/service/requests/factories/JsonBodyFactory.ts +9 -0
  106. package/src/service/requests/index.ts +50 -0
  107. package/src/service/requests/responses/BaseResponse.ts +41 -0
  108. package/src/service/requests/responses/BlobResponse.ts +19 -0
  109. package/src/service/requests/responses/JsonResponse.ts +15 -0
  110. package/src/service/requests/responses/PlainTextResponse.ts +15 -0
  111. package/src/service/support/DeferredPromise.ts +67 -0
  112. package/src/service/support/index.ts +3 -0
  113. package/src/vue/composables/useConfirmDialog.ts +59 -0
  114. package/src/vue/composables/useGlobalCheckbox.ts +145 -0
  115. package/src/vue/composables/useIsEmpty.ts +34 -0
  116. package/src/vue/composables/useIsOpen.ts +37 -0
  117. package/src/vue/composables/useIsOpenFromVar.ts +61 -0
  118. package/src/vue/composables/useModelWrapper.ts +24 -0
  119. package/src/vue/composables/useOnOpen.ts +34 -0
  120. package/src/vue/contracts/ModelValueOptions.ts +3 -0
  121. package/src/vue/contracts/ModelValueProps.ts +3 -0
  122. package/src/vue/forms/BaseForm.ts +1074 -0
  123. package/src/vue/forms/PropertyAwareArray.ts +78 -0
  124. package/src/vue/forms/index.ts +11 -0
  125. package/src/vue/forms/types/PersistedForm.ts +6 -0
  126. package/src/vue/forms/validation/ValidationMode.enum.ts +14 -0
  127. package/src/vue/forms/validation/index.ts +12 -0
  128. package/src/vue/forms/validation/rules/BaseRule.ts +7 -0
  129. package/src/vue/forms/validation/rules/ConfirmedRule.ts +39 -0
  130. package/src/vue/forms/validation/rules/MinRule.ts +61 -0
  131. package/src/vue/forms/validation/rules/RequiredRule.ts +19 -0
  132. package/src/vue/forms/validation/rules/UrlRule.ts +24 -0
  133. package/src/vue/forms/validation/types/BidirectionalRule.ts +11 -0
  134. package/src/vue/index.ts +14 -0
  135. package/src/vue/requests/factories/VueRequestLoaderFactory.ts +9 -0
  136. package/src/vue/requests/index.ts +5 -0
  137. package/src/vue/requests/loaders/VueRequestBatchLoader.ts +30 -0
  138. package/src/vue/requests/loaders/VueRequestLoader.ts +18 -0
  139. package/src/vue/router/routeModelBinding/RouteModelRequestResolver.ts +11 -0
  140. package/src/vue/router/routeModelBinding/defineRoute.ts +31 -0
  141. package/src/vue/router/routeModelBinding/index.ts +8 -0
  142. package/src/vue/router/routeModelBinding/installRouteInjection.ts +73 -0
  143. package/src/vue/router/routeModelBinding/types.ts +46 -0
  144. package/src/vue/state/State.ts +391 -0
  145. package/src/vue/state/index.ts +3 -0
  146. package/tests/service/helpers/mergeDeep.test.ts +53 -0
  147. package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +84 -0
  148. package/tests/service/laravel/requests/JsonBaseRequest.test.ts +43 -0
  149. package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +58 -0
  150. package/tests/service/laravel/requests/responses/JsonResponse.test.ts +59 -0
  151. package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +127 -0
  152. package/tests/service/pagination/dtos/PaginationDataDto.test.ts +35 -0
  153. package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +32 -0
  154. package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +66 -0
  155. package/tests/service/requests/ErrorHandler.test.ts +141 -0
  156. package/tsconfig.json +114 -0
  157. package/vite.config.ts +34 -0
  158. 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,3 @@
1
+ import { State } from './State'
2
+
3
+ export { State }
@@ -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
+ });