@katlux/providers 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +16 -0
- package/build.config.ts +4 -0
- package/package.json +24 -0
- package/services/CacheProvider/ACacheProvider.ts +13 -0
- package/services/CacheProvider/CApplicationCache.ts +123 -0
- package/services/CacheProvider/CCookieCache.ts +212 -0
- package/services/CacheProvider/CIndexedDBCache.ts +312 -0
- package/services/CacheProvider/CLocalStorageCache.ts +232 -0
- package/services/CacheProvider/CMemoryCache.ts +185 -0
- package/services/CacheProvider/CSessionCache.ts +378 -0
- package/services/CacheProvider/CacheProviderFactory.ts +22 -0
- package/services/DataProvider/AAPIDataProvider.ts +24 -0
- package/services/DataProvider/ADataProvider.ts +126 -0
- package/services/DataProvider/CAPIFlatClientDataProvider.ts +199 -0
- package/services/DataProvider/CAPIFlatServerDataProvider.ts +77 -0
- package/services/DataProvider/CAPITreeClientDataProvider.ts +205 -0
- package/services/DataProvider/CAPITreeServerDataProvider.ts +98 -0
- package/services/DataProvider/CFlatClientDataProvider.ts +52 -0
- package/services/DataProvider/CFlatServerDataProvider.ts +104 -0
- package/services/DataProvider/CTreeClientDataProvider.ts +335 -0
- package/services/DataProvider/CTreeServerDataProvider.ts +207 -0
- package/services/RequestProvider/RequestProvider.ts +165 -0
- package/services/RequestProvider/serverCache.ts +24 -0
- package/services/index.ts +19 -0
- package/services/types.ts +172 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { TDataRow, IDataFilter, IDataSort, IDataResult, IKDatatableAction, IDataProviderOptions } from '../types'
|
|
3
|
+
import { useDebounce } from '@katlux/toolkit/composables/useDebounce'
|
|
4
|
+
|
|
5
|
+
// Note: In an isomorphic package, we must handle Nuxt auto-imports carefully.
|
|
6
|
+
// Standard Vue ref and watch are explicitly imported.
|
|
7
|
+
import { watch } from 'vue'
|
|
8
|
+
const APP_MODULE = '#app';
|
|
9
|
+
// DataProvider abstract class
|
|
10
|
+
export abstract class ADataProvider {
|
|
11
|
+
filter: Ref<IDataFilter | null> = ref(null)
|
|
12
|
+
sortList: Ref<IDataSort[]> = ref([])
|
|
13
|
+
currentPage: Ref<number> = ref(1)
|
|
14
|
+
pageSize: Ref<number> = ref(10)
|
|
15
|
+
pageData: Ref<TDataRow[]> = ref([])
|
|
16
|
+
rowCount: Ref<number> = ref(0)
|
|
17
|
+
loading: Ref<Boolean> = ref(false)
|
|
18
|
+
SSR: Ref<Boolean> = ref(false)
|
|
19
|
+
deduplicate: Ref<Boolean> = ref(true)
|
|
20
|
+
nuxtApp: any = null
|
|
21
|
+
urlPageParam: Ref<string> = ref('')
|
|
22
|
+
selectedRows: Ref<any[]> = ref([])
|
|
23
|
+
selectAll: Ref<boolean> = ref(false)
|
|
24
|
+
|
|
25
|
+
constructor(options?: IDataProviderOptions) {
|
|
26
|
+
if (options?.nuxtApp) {
|
|
27
|
+
this.nuxtApp = options.nuxtApp
|
|
28
|
+
} else if (import.meta.server) {
|
|
29
|
+
try {
|
|
30
|
+
import(/* @vite-ignore */ APP_MODULE).then(appModule => {
|
|
31
|
+
this.nuxtApp = appModule.useNuxtApp()
|
|
32
|
+
}).catch(() => { })
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Outside nuxt context
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options?.filter) {
|
|
39
|
+
this.filter.value = options.filter
|
|
40
|
+
}
|
|
41
|
+
if (options?.sortList) {
|
|
42
|
+
this.sortList.value = options.sortList
|
|
43
|
+
}
|
|
44
|
+
if (options?.pageSize) {
|
|
45
|
+
this.pageSize.value = options.pageSize
|
|
46
|
+
}
|
|
47
|
+
if (options?.currentPage) {
|
|
48
|
+
this.currentPage.value = options.currentPage
|
|
49
|
+
}
|
|
50
|
+
if (options?.SSR) {
|
|
51
|
+
this.SSR.value = options.SSR
|
|
52
|
+
}
|
|
53
|
+
if (options?.deduplicate !== undefined) {
|
|
54
|
+
this.deduplicate.value = options.deduplicate
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// URL-based pagination: read initial page from URL
|
|
58
|
+
if (options?.urlPageParam) {
|
|
59
|
+
this.urlPageParam.value = options.urlPageParam
|
|
60
|
+
try {
|
|
61
|
+
import(/* @vite-ignore */ APP_MODULE).then(appModule => {
|
|
62
|
+
const route = appModule.useRoute()
|
|
63
|
+
const pageFromUrl = route.query[options.urlPageParam!]
|
|
64
|
+
if (pageFromUrl) {
|
|
65
|
+
const parsedPage = parseInt(pageFromUrl as string, 10)
|
|
66
|
+
if (!isNaN(parsedPage) && parsedPage > 0) {
|
|
67
|
+
this.currentPage.value = parsedPage
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}).catch(() => { })
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// useRoute not available (outside Vue context)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Watch for changes in currentPage and pageSize to automatically reload data
|
|
77
|
+
watch(this.currentPage, (newPage) => {
|
|
78
|
+
this.loadPageData()
|
|
79
|
+
|
|
80
|
+
// URL-based pagination: update URL when page changes (client-side only)
|
|
81
|
+
if (import.meta.client && this.urlPageParam.value) {
|
|
82
|
+
try {
|
|
83
|
+
import(/* @vite-ignore */ APP_MODULE).then(appModule => {
|
|
84
|
+
const router = appModule.useRouter()
|
|
85
|
+
const currentQuery = { ...router.currentRoute.value.query }
|
|
86
|
+
if (newPage === 1) {
|
|
87
|
+
delete currentQuery[this.urlPageParam.value]
|
|
88
|
+
} else {
|
|
89
|
+
currentQuery[this.urlPageParam.value] = String(newPage)
|
|
90
|
+
}
|
|
91
|
+
router.replace({ query: currentQuery })
|
|
92
|
+
}).catch(() => { })
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// useRouter not available
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
watch(this.pageSize, () => {
|
|
100
|
+
this.currentPage.value = 1
|
|
101
|
+
this.loadPageData()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
watch(this.filter, useDebounce(() => {
|
|
105
|
+
this.currentPage.value = 1
|
|
106
|
+
this.loadPageData()
|
|
107
|
+
}, 300))
|
|
108
|
+
watch(this.sortList, () => {
|
|
109
|
+
this.currentPage.value = 1
|
|
110
|
+
this.loadPageData()
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
setFilter(filter: IDataFilter | null) {
|
|
114
|
+
this.filter.value = filter
|
|
115
|
+
}
|
|
116
|
+
setSortList(sortList: IDataSort[]) {
|
|
117
|
+
this.sortList.value = sortList
|
|
118
|
+
}
|
|
119
|
+
setCurrentPage(currentPage: number) {
|
|
120
|
+
this.currentPage.value = currentPage
|
|
121
|
+
}
|
|
122
|
+
setPageSize(pageSize: number) {
|
|
123
|
+
this.pageSize.value = pageSize
|
|
124
|
+
}
|
|
125
|
+
abstract loadPageData(): void
|
|
126
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { IDataResult, IDataProviderOptions, ECacheStrategy } from '../types'
|
|
3
|
+
import { CFlatClientDataProvider } from './CFlatClientDataProvider';
|
|
4
|
+
import type { AAPIDataProvider } from './AAPIDataProvider';
|
|
5
|
+
import { RequestProvider } from '../RequestProvider/RequestProvider';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export class CAPIFlatClientDataProvider extends CFlatClientDataProvider implements AAPIDataProvider {
|
|
9
|
+
|
|
10
|
+
apiUrl: Ref<string> = ref("")
|
|
11
|
+
contextKey: Ref<string> = ref("")
|
|
12
|
+
initialLoad: Ref<boolean> = ref(true)
|
|
13
|
+
requestProvider: RequestProvider = new RequestProvider()
|
|
14
|
+
cacheStrategy: Ref<ECacheStrategy | null> = ref(null)
|
|
15
|
+
cacheLifetime: Ref<number> = ref(0)
|
|
16
|
+
refreshOnMutation: Ref<boolean | undefined> = ref(undefined)
|
|
17
|
+
|
|
18
|
+
constructor(options?: IDataProviderOptions) {
|
|
19
|
+
super(options)
|
|
20
|
+
if (options?.cacheStrategy) this.cacheStrategy.value = options.cacheStrategy
|
|
21
|
+
if (options?.cacheLifetime) this.cacheLifetime.value = options.cacheLifetime
|
|
22
|
+
if (options?.refreshOnMutation !== undefined) this.refreshOnMutation.value = options.refreshOnMutation
|
|
23
|
+
}
|
|
24
|
+
setContextKey(key: string) {
|
|
25
|
+
this.contextKey.value = key
|
|
26
|
+
this.requestProvider.cacheKey = key
|
|
27
|
+
if (this.cacheStrategy.value) this.requestProvider.setCacheProvider(this.cacheStrategy.value)
|
|
28
|
+
if (this.cacheLifetime.value) this.requestProvider.setCacheLifetime(this.cacheLifetime.value)
|
|
29
|
+
}
|
|
30
|
+
override async refreshData(options?: { disableCache?: boolean }): Promise<void> {
|
|
31
|
+
if (!this.SSR.value && import.meta.server) return;
|
|
32
|
+
|
|
33
|
+
const fetcher = async (): Promise<IDataResult> => {
|
|
34
|
+
// Register request - execution happens automatically
|
|
35
|
+
const promise = this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
36
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
37
|
+
nuxtApp: this.nuxtApp,
|
|
38
|
+
disableCache: options?.disableCache
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// If SSR is enabled and we're on server, await to ensure content is ready for rendering
|
|
42
|
+
if (this.SSR.value && import.meta.server) {
|
|
43
|
+
return await promise
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// For non-SSR, return promise - useAsyncData will handle it
|
|
47
|
+
return promise
|
|
48
|
+
}
|
|
49
|
+
const key = `${this.contextKey.value}`
|
|
50
|
+
|
|
51
|
+
if (this.contextKey.value && this.initialLoad.value) {
|
|
52
|
+
const { data } = useNuxtData(key)
|
|
53
|
+
if (data.value) {
|
|
54
|
+
const result = data.value as IDataResult
|
|
55
|
+
this.setData(result.rows)
|
|
56
|
+
this.initialLoad.value = false
|
|
57
|
+
this.loading.value = false
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (import.meta.server && this.contextKey.value) {
|
|
62
|
+
const { data } = await useAsyncData(key, fetcher)
|
|
63
|
+
if (data.value) {
|
|
64
|
+
const result = data.value as IDataResult
|
|
65
|
+
this.setData(result.rows)
|
|
66
|
+
this.initialLoad.value = false
|
|
67
|
+
this.loading.value = false
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// For non-SSR client-side, register and handle asynchronously
|
|
73
|
+
if (!this.SSR.value) {
|
|
74
|
+
const promise = this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
75
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
76
|
+
nuxtApp: this.nuxtApp,
|
|
77
|
+
disableCache: options?.disableCache
|
|
78
|
+
})
|
|
79
|
+
// Don't await - deliver when finished
|
|
80
|
+
promise.then((returnedData) => {
|
|
81
|
+
this.initialLoad.value = false
|
|
82
|
+
this.loading.value = false
|
|
83
|
+
this.setData(returnedData.rows)
|
|
84
|
+
}).catch(() => {
|
|
85
|
+
this.initialLoad.value = false
|
|
86
|
+
this.loading.value = false
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// SSR client-side or fallback: await the request
|
|
92
|
+
const returnedData: IDataResult = await fetcher()
|
|
93
|
+
this.initialLoad.value = false
|
|
94
|
+
this.loading.value = false
|
|
95
|
+
this.setData(returnedData.rows)
|
|
96
|
+
}
|
|
97
|
+
async setAPIUrl(url: string) {
|
|
98
|
+
this.loading.value = true
|
|
99
|
+
this.apiUrl.value = url
|
|
100
|
+
this.setContextKey(url)
|
|
101
|
+
if (this.allData.value.length == 0)
|
|
102
|
+
await this.refreshData()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async create(item: any): Promise<void> {
|
|
106
|
+
if (!this.apiUrl.value) return
|
|
107
|
+
|
|
108
|
+
const result = await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: item,
|
|
111
|
+
nuxtApp: this.nuxtApp
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (this.refreshOnMutation.value) {
|
|
115
|
+
await this.refreshData({ disableCache: true })
|
|
116
|
+
} else {
|
|
117
|
+
if (result && (result as any).item) {
|
|
118
|
+
this.allData.value.push((result as any).item)
|
|
119
|
+
this.loadPageData() // Refresh view
|
|
120
|
+
} else {
|
|
121
|
+
await this.refreshData({ disableCache: true })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.requestProvider.cacheProvider) {
|
|
125
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
126
|
+
}
|
|
127
|
+
if (this.contextKey.value) {
|
|
128
|
+
clearNuxtData(this.contextKey.value)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async update(item: any): Promise<void> {
|
|
134
|
+
if (!this.apiUrl.value) return
|
|
135
|
+
|
|
136
|
+
const result = await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
137
|
+
method: 'PUT',
|
|
138
|
+
body: item,
|
|
139
|
+
nuxtApp: this.nuxtApp
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (this.refreshOnMutation.value) {
|
|
143
|
+
await this.refreshData({ disableCache: true })
|
|
144
|
+
} else {
|
|
145
|
+
// Local update
|
|
146
|
+
// We don't have idKey property on CFlatClientDataProvider explicitly defined like Tree,
|
|
147
|
+
// assuming 'id' or we need to look at implementation.
|
|
148
|
+
// CFlatClientDataProvider doesn't enforce idKey. Assuming 'id' for now as standard.
|
|
149
|
+
const index = this.allData.value.findIndex(row => row['id'] === item['id'])
|
|
150
|
+
if (index !== -1) {
|
|
151
|
+
if (result && (result as any).item) {
|
|
152
|
+
this.allData.value[index] = (result as any).item
|
|
153
|
+
} else {
|
|
154
|
+
this.allData.value[index] = { ...this.allData.value[index], ...item }
|
|
155
|
+
}
|
|
156
|
+
this.loadPageData()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.requestProvider.cacheProvider) {
|
|
160
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
161
|
+
}
|
|
162
|
+
if (this.contextKey.value) {
|
|
163
|
+
clearNuxtData(this.contextKey.value)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async delete(items: any[]): Promise<void> {
|
|
169
|
+
if (!this.apiUrl.value) return
|
|
170
|
+
if (!items || items.length === 0) return
|
|
171
|
+
|
|
172
|
+
const ids = items.map(i => i.id)
|
|
173
|
+
|
|
174
|
+
// Use RequestProvider for the delete request
|
|
175
|
+
await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
176
|
+
method: 'DELETE',
|
|
177
|
+
body: { id: ids },
|
|
178
|
+
nuxtApp: this.nuxtApp
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (this.refreshOnMutation.value) {
|
|
182
|
+
// Refresh data from server with cache disabled
|
|
183
|
+
await this.refreshData({ disableCache: true })
|
|
184
|
+
} else {
|
|
185
|
+
// Optimization: Remove deleted items locally instead of refetching
|
|
186
|
+
const idsToDelete = new Set(ids)
|
|
187
|
+
this.allData.value = this.allData.value.filter(row => !idsToDelete.has(row["id"]))
|
|
188
|
+
|
|
189
|
+
// Clear the cache for this provider to prevent stale data on reload
|
|
190
|
+
if (this.requestProvider.cacheProvider) {
|
|
191
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
192
|
+
}
|
|
193
|
+
// Also clear Nuxt's internal cache for this key
|
|
194
|
+
if (this.contextKey.value) {
|
|
195
|
+
clearNuxtData(this.contextKey.value)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { IDataResult, IDataFilter, IDataSort, IDataProviderOptions, ECacheStrategy } from '../types'
|
|
3
|
+
import { CFlatServerDataProvider } from './CFlatServerDataProvider';
|
|
4
|
+
import type { AAPIDataProvider } from './AAPIDataProvider';
|
|
5
|
+
import { RequestProvider } from '../RequestProvider/RequestProvider';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export class CAPIFlatServerDataProvider extends CFlatServerDataProvider implements AAPIDataProvider {
|
|
9
|
+
|
|
10
|
+
apiUrl: Ref<string> = ref("")
|
|
11
|
+
requestProvider: RequestProvider = new RequestProvider()
|
|
12
|
+
cacheStrategy: Ref<ECacheStrategy | null> = ref(null)
|
|
13
|
+
cacheLifetime: Ref<number> = ref(0)
|
|
14
|
+
refreshOnMutation: Ref<boolean | undefined> = ref(true) // Default true for server
|
|
15
|
+
|
|
16
|
+
constructor(options?: IDataProviderOptions) {
|
|
17
|
+
super(options)
|
|
18
|
+
if (options?.cacheStrategy) this.cacheStrategy.value = options.cacheStrategy
|
|
19
|
+
if (options?.cacheLifetime) this.cacheLifetime.value = options.cacheLifetime
|
|
20
|
+
if (options?.refreshOnMutation !== undefined) this.refreshOnMutation.value = options.refreshOnMutation
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
async setAPIUrl(url: string) {
|
|
24
|
+
this.apiUrl.value = url
|
|
25
|
+
this.setContextKey(url)
|
|
26
|
+
if (this.cacheStrategy.value) this.requestProvider.setCacheProvider(this.cacheStrategy.value)
|
|
27
|
+
if (this.cacheLifetime.value) this.requestProvider.setCacheLifetime(this.cacheLifetime.value)
|
|
28
|
+
|
|
29
|
+
await this.setPageDataHandler(
|
|
30
|
+
async (
|
|
31
|
+
currentPage: number,
|
|
32
|
+
pageSize: number,
|
|
33
|
+
filter: IDataFilter,
|
|
34
|
+
sortList: IDataSort[],
|
|
35
|
+
options?: { disableCache?: boolean }
|
|
36
|
+
): Promise<IDataResult> => {
|
|
37
|
+
// Register request - execution happens automatically
|
|
38
|
+
// Always await the promise - loadPageData will decide whether to await the fetcher
|
|
39
|
+
return await this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
40
|
+
query: {
|
|
41
|
+
pageNumber: currentPage,
|
|
42
|
+
pageSize: pageSize,
|
|
43
|
+
filter: filter,
|
|
44
|
+
sortList: sortList,
|
|
45
|
+
},
|
|
46
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
47
|
+
nuxtApp: this.nuxtApp,
|
|
48
|
+
disableCache: options?.disableCache
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(items: any[]): Promise<void> {
|
|
55
|
+
if (!this.apiUrl.value) return
|
|
56
|
+
if (!items || items.length === 0) return
|
|
57
|
+
|
|
58
|
+
const ids = items.map(i => i.id)
|
|
59
|
+
|
|
60
|
+
// Use RequestProvider for the delete request
|
|
61
|
+
await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
62
|
+
method: 'DELETE',
|
|
63
|
+
body: { id: ids },
|
|
64
|
+
nuxtApp: this.nuxtApp
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Refresh data (refetch from server)
|
|
68
|
+
if (this.refreshOnMutation.value !== false) {
|
|
69
|
+
// Clear client-side cache for this resource
|
|
70
|
+
if (this.requestProvider.cacheProvider) {
|
|
71
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.loadPageData({ disableCache: true })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { IDataResult, ITreeDataProviderOptions, ECacheStrategy } from '../types'
|
|
3
|
+
import { CTreeClientDataProvider } from './CTreeClientDataProvider'
|
|
4
|
+
import type { AAPIDataProvider } from './AAPIDataProvider'
|
|
5
|
+
import { RequestProvider } from '../RequestProvider/RequestProvider'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export class CAPITreeClientDataProvider extends CTreeClientDataProvider implements AAPIDataProvider {
|
|
9
|
+
|
|
10
|
+
apiUrl: Ref<string> = ref("")
|
|
11
|
+
contextKey: Ref<string> = ref("")
|
|
12
|
+
initialLoad: Ref<boolean> = ref(true)
|
|
13
|
+
requestProvider: RequestProvider = new RequestProvider()
|
|
14
|
+
cacheStrategy: Ref<ECacheStrategy | null> = ref(null)
|
|
15
|
+
cacheLifetime: Ref<number> = ref(0)
|
|
16
|
+
refreshOnMutation: Ref<boolean | undefined> = ref(undefined)
|
|
17
|
+
|
|
18
|
+
constructor(options?: ITreeDataProviderOptions) {
|
|
19
|
+
super(options)
|
|
20
|
+
if (options?.cacheStrategy) this.cacheStrategy.value = options.cacheStrategy
|
|
21
|
+
if (options?.cacheLifetime) this.cacheLifetime.value = options.cacheLifetime
|
|
22
|
+
if (options?.refreshOnMutation !== undefined) this.refreshOnMutation.value = options.refreshOnMutation
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setContextKey(key: string) {
|
|
26
|
+
this.contextKey.value = key
|
|
27
|
+
this.requestProvider.cacheKey = key
|
|
28
|
+
if (this.cacheStrategy.value) this.requestProvider.setCacheProvider(this.cacheStrategy.value)
|
|
29
|
+
if (this.cacheLifetime.value) this.requestProvider.setCacheLifetime(this.cacheLifetime.value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override async refreshData(options?: { disableCache?: boolean }): Promise<void> {
|
|
33
|
+
if (!this.SSR.value && import.meta.server) return;
|
|
34
|
+
|
|
35
|
+
const fetcher = async (): Promise<IDataResult> => {
|
|
36
|
+
// Register request - execution happens automatically
|
|
37
|
+
const promise = this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
38
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
39
|
+
nuxtApp: this.nuxtApp,
|
|
40
|
+
disableCache: options?.disableCache
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// If SSR is enabled and we're on server, await to ensure content is ready for rendering
|
|
44
|
+
if (this.SSR.value && import.meta.server) {
|
|
45
|
+
return await promise
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// For non-SSR, return promise - useAsyncData will handle it
|
|
49
|
+
return promise
|
|
50
|
+
}
|
|
51
|
+
const key = `${this.contextKey.value}`
|
|
52
|
+
|
|
53
|
+
if (this.contextKey.value && this.initialLoad.value) {
|
|
54
|
+
const { data } = useNuxtData(key)
|
|
55
|
+
if (data.value) {
|
|
56
|
+
const result = data.value as IDataResult
|
|
57
|
+
this.setData(result.rows)
|
|
58
|
+
this.initialLoad.value = false
|
|
59
|
+
this.loading.value = false
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (import.meta.server && this.contextKey.value) {
|
|
64
|
+
const { data } = await useAsyncData(key, fetcher)
|
|
65
|
+
if (data.value) {
|
|
66
|
+
const result = data.value as IDataResult
|
|
67
|
+
this.setData(result.rows)
|
|
68
|
+
this.initialLoad.value = false
|
|
69
|
+
this.loading.value = false
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For non-SSR client-side, register and handle asynchronously
|
|
75
|
+
if (!this.SSR.value) {
|
|
76
|
+
const promise = this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
77
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
78
|
+
nuxtApp: this.nuxtApp,
|
|
79
|
+
disableCache: options?.disableCache
|
|
80
|
+
})
|
|
81
|
+
// Don't await - deliver when finished
|
|
82
|
+
promise.then((returnedData) => {
|
|
83
|
+
this.initialLoad.value = false
|
|
84
|
+
this.loading.value = false
|
|
85
|
+
this.setData(returnedData.rows)
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
this.initialLoad.value = false
|
|
88
|
+
this.loading.value = false
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// SSR client-side or fallback: await the request
|
|
94
|
+
const returnedData: IDataResult = await fetcher()
|
|
95
|
+
this.initialLoad.value = false
|
|
96
|
+
this.loading.value = false
|
|
97
|
+
this.setData(returnedData.rows)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async setAPIUrl(url: string) {
|
|
101
|
+
this.loading.value = true
|
|
102
|
+
this.apiUrl.value = url
|
|
103
|
+
this.setContextKey(url)
|
|
104
|
+
if (this.allData.value.length == 0)
|
|
105
|
+
await this.refreshData()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async create(item: any): Promise<void> {
|
|
109
|
+
if (!this.apiUrl.value) return
|
|
110
|
+
|
|
111
|
+
// Use RequestProvider for the create request
|
|
112
|
+
const result = await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: item,
|
|
115
|
+
nuxtApp: this.nuxtApp
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (this.refreshOnMutation.value) {
|
|
119
|
+
await this.refreshData({ disableCache: true })
|
|
120
|
+
} else {
|
|
121
|
+
// Optimistic UI update or fetch fresh data
|
|
122
|
+
if (result && (result as any).item) {
|
|
123
|
+
this.allData.value.push((result as any).item)
|
|
124
|
+
} else {
|
|
125
|
+
// Fallback if no item returned
|
|
126
|
+
await this.refreshData({ disableCache: true })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clear cache
|
|
130
|
+
if (this.requestProvider.cacheProvider) {
|
|
131
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
132
|
+
}
|
|
133
|
+
if (this.contextKey.value) {
|
|
134
|
+
clearNuxtData(this.contextKey.value)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async update(item: any): Promise<void> {
|
|
140
|
+
if (!this.apiUrl.value) return
|
|
141
|
+
|
|
142
|
+
// Use RequestProvider for the update request
|
|
143
|
+
const result = await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
144
|
+
method: 'PUT',
|
|
145
|
+
body: item,
|
|
146
|
+
nuxtApp: this.nuxtApp
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (this.refreshOnMutation.value) {
|
|
150
|
+
await this.refreshData({ disableCache: true })
|
|
151
|
+
} else {
|
|
152
|
+
// Local update
|
|
153
|
+
const index = this.allData.value.findIndex(row => row[this.idKey] === item[this.idKey])
|
|
154
|
+
if (index !== -1) {
|
|
155
|
+
if (result && (result as any).item) {
|
|
156
|
+
this.allData.value[index] = (result as any).item
|
|
157
|
+
} else {
|
|
158
|
+
this.allData.value[index] = { ...this.allData.value[index], ...item }
|
|
159
|
+
}
|
|
160
|
+
// Trigger reactivity
|
|
161
|
+
this.allData.value = [...this.allData.value]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clear cache
|
|
165
|
+
if (this.requestProvider.cacheProvider) {
|
|
166
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
167
|
+
}
|
|
168
|
+
if (this.contextKey.value) {
|
|
169
|
+
clearNuxtData(this.contextKey.value)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async delete(items: any[]): Promise<void> {
|
|
175
|
+
if (!this.apiUrl.value) return
|
|
176
|
+
if (!items || items.length === 0) return
|
|
177
|
+
|
|
178
|
+
const ids = items.map(i => i.id)
|
|
179
|
+
|
|
180
|
+
// Use RequestProvider for the delete request
|
|
181
|
+
await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
182
|
+
method: 'DELETE',
|
|
183
|
+
body: { id: ids },
|
|
184
|
+
nuxtApp: this.nuxtApp
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if (this.refreshOnMutation.value) {
|
|
188
|
+
// Refresh data from server with cache disabled
|
|
189
|
+
await this.refreshData({ disableCache: true })
|
|
190
|
+
} else {
|
|
191
|
+
// Optimization: Remove deleted items locally instead of refetching
|
|
192
|
+
const idsToDelete = new Set(ids)
|
|
193
|
+
this.allData.value = this.allData.value.filter(row => !idsToDelete.has(row[this.idKey])) // Use this.idKey
|
|
194
|
+
|
|
195
|
+
// Clear the cache for this provider to prevent stale data on reload
|
|
196
|
+
if (this.requestProvider.cacheProvider) {
|
|
197
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
198
|
+
}
|
|
199
|
+
// Also clear Nuxt's internal cache for this key
|
|
200
|
+
if (this.contextKey.value) {
|
|
201
|
+
clearNuxtData(this.contextKey.value)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { IDataResult, IDataFilter, IDataSort, ITreeDataProviderOptions, ECacheStrategy, TreePaginationMode } from '../types'
|
|
3
|
+
import { CTreeServerDataProvider } from './CTreeServerDataProvider';
|
|
4
|
+
import type { AAPIDataProvider } from './AAPIDataProvider';
|
|
5
|
+
import { RequestProvider } from '../RequestProvider/RequestProvider';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* API-based Server-side Tree Data Provider
|
|
10
|
+
*
|
|
11
|
+
* Extends CTreeServerDataProvider with:
|
|
12
|
+
* - API URL configuration
|
|
13
|
+
* - Request management (caching, deduplication)
|
|
14
|
+
* - Automatic tree parameter serialization in query
|
|
15
|
+
*/
|
|
16
|
+
export class CAPITreeServerDataProvider extends CTreeServerDataProvider implements AAPIDataProvider {
|
|
17
|
+
|
|
18
|
+
apiUrl: Ref<string> = ref("")
|
|
19
|
+
requestProvider: RequestProvider = new RequestProvider()
|
|
20
|
+
cacheStrategy: Ref<ECacheStrategy | null> = ref(null)
|
|
21
|
+
cacheLifetime: Ref<number> = ref(0)
|
|
22
|
+
refreshOnMutation: Ref<boolean | undefined> = ref(true) // Default true for server
|
|
23
|
+
|
|
24
|
+
constructor(options?: ITreeDataProviderOptions) {
|
|
25
|
+
super(options)
|
|
26
|
+
if (options?.cacheStrategy) this.cacheStrategy.value = options.cacheStrategy
|
|
27
|
+
if (options?.cacheLifetime) this.cacheLifetime.value = options.cacheLifetime
|
|
28
|
+
if (options?.refreshOnMutation !== undefined) this.refreshOnMutation.value = options.refreshOnMutation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async setAPIUrl(url: string) {
|
|
32
|
+
this.apiUrl.value = url
|
|
33
|
+
this.setContextKey(url)
|
|
34
|
+
if (this.cacheStrategy.value) this.requestProvider.setCacheProvider(this.cacheStrategy.value)
|
|
35
|
+
if (this.cacheLifetime.value) this.requestProvider.setCacheLifetime(this.cacheLifetime.value)
|
|
36
|
+
|
|
37
|
+
await this.setPageDataHandler(
|
|
38
|
+
async (
|
|
39
|
+
currentPage: number,
|
|
40
|
+
pageSize: number,
|
|
41
|
+
filter: IDataFilter,
|
|
42
|
+
sortList: IDataSort[],
|
|
43
|
+
treeParams: {
|
|
44
|
+
expandedNodes: (string | number)[],
|
|
45
|
+
parentKey: string,
|
|
46
|
+
idKey: string,
|
|
47
|
+
paginateBy: TreePaginationMode,
|
|
48
|
+
expandedByDefault: boolean
|
|
49
|
+
},
|
|
50
|
+
options?: { disableCache?: boolean }
|
|
51
|
+
): Promise<IDataResult> => {
|
|
52
|
+
// Send all parameters to server for server-side tree pagination
|
|
53
|
+
return await this.requestProvider.registerRequest<IDataResult>(this.apiUrl.value, {
|
|
54
|
+
query: {
|
|
55
|
+
pageNumber: currentPage,
|
|
56
|
+
pageSize: pageSize,
|
|
57
|
+
filter: filter,
|
|
58
|
+
sortList: sortList,
|
|
59
|
+
// Tree-specific params for server-side processing
|
|
60
|
+
expandedNodes: JSON.stringify(treeParams.expandedNodes),
|
|
61
|
+
parentKey: treeParams.parentKey,
|
|
62
|
+
idKey: treeParams.idKey,
|
|
63
|
+
paginateBy: treeParams.paginateBy,
|
|
64
|
+
expandedByDefault: treeParams.expandedByDefault
|
|
65
|
+
},
|
|
66
|
+
deduplicate: this.deduplicate.value as boolean,
|
|
67
|
+
nuxtApp: this.nuxtApp,
|
|
68
|
+
disableCache: options?.disableCache
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async delete(items: any[]): Promise<void> {
|
|
75
|
+
if (!this.apiUrl.value) return
|
|
76
|
+
if (!items || items.length === 0) return
|
|
77
|
+
|
|
78
|
+
const ids = items.map(i => i.id)
|
|
79
|
+
|
|
80
|
+
// Use RequestProvider for the delete request
|
|
81
|
+
await this.requestProvider.registerRequest(this.apiUrl.value, {
|
|
82
|
+
method: 'DELETE',
|
|
83
|
+
body: { id: ids },
|
|
84
|
+
nuxtApp: this.nuxtApp
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (this.refreshOnMutation.value !== false) {
|
|
88
|
+
// Clear client-side cache for this resource to ensure consistency (e.g. other pages/filters)
|
|
89
|
+
if (this.requestProvider.cacheProvider) {
|
|
90
|
+
await this.requestProvider.cacheProvider.removeByPrefix(this.apiUrl.value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Trigger reload which calls the setPageDataHandler defined above
|
|
94
|
+
// Disable cache to ensure fresh data
|
|
95
|
+
this.loadPageData({ disableCache: true })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|