bt-core-app 0.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/.vscode/extensions.json +3 -0
- package/README.md +45 -0
- package/index.html +13 -0
- package/package.json +39 -0
- package/public/vite.svg +1 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/BT-Btn.vue +40 -0
- package/src/components/BT-Col.vue +36 -0
- package/src/components/BT-Span.vue +21 -0
- package/src/components/Dialog-Confirm.vue +47 -0
- package/src/components/Dialog-Select-Date.vue +89 -0
- package/src/components/Dialog-Select.vue +140 -0
- package/src/components/Dialog-Text.vue +59 -0
- package/src/composables/actions-tracker.ts +99 -0
- package/src/composables/actions.ts +354 -0
- package/src/composables/api.ts +471 -0
- package/src/composables/auth.ts +382 -0
- package/src/composables/cosmetics.ts +178 -0
- package/src/composables/csv.ts +198 -0
- package/src/composables/dates.ts +79 -0
- package/src/composables/demo.ts +25 -0
- package/src/composables/dialogs.ts +115 -0
- package/src/composables/document-meta.ts +40 -0
- package/src/composables/draggable.ts +189 -0
- package/src/composables/filters.ts +256 -0
- package/src/composables/forage.ts +49 -0
- package/src/composables/helpers.ts +694 -0
- package/src/composables/id.ts +20 -0
- package/src/composables/list.ts +601 -0
- package/src/composables/navigation.ts +214 -0
- package/src/composables/presets.ts +20 -0
- package/src/composables/pwa.ts +89 -0
- package/src/composables/resizable.ts +382 -0
- package/src/composables/rules.ts +40 -0
- package/src/composables/stores.ts +797 -0
- package/src/composables/track.ts +55 -0
- package/src/composables/urls.ts +11 -0
- package/src/core.ts +92 -0
- package/src/index.ts +16 -0
- package/src/types.ts +13 -0
- package/src/useApi.ts +68 -0
- package/src/vite-env.d.ts +1 -0
- package/test/api.test.ts +84 -0
- package/test/forage.test.ts +31 -0
- package/test/helpers.test.ts +231 -0
- package/test/navigation.test.ts +99 -0
- package/test/stores-last-update.test.ts +138 -0
- package/test/stores-session.test.ts +118 -0
- package/test/track.test.ts +29 -0
- package/test/utils.ts +15 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { defineStore, StoreDefinition } from 'pinia'
|
|
2
|
+
import { ref, type Ref } from 'vue'
|
|
3
|
+
import { type PathOptions, type BTApi } from '@/composables/api'
|
|
4
|
+
import { DateTime } from 'luxon'
|
|
5
|
+
import { toValue } from 'vue'
|
|
6
|
+
import { appendUrl, getMinDateString } from '@/composables/helpers'
|
|
7
|
+
import { firstBy } from 'thenby'
|
|
8
|
+
import { useLocalDb } from '@/composables/forage'
|
|
9
|
+
import { BaseModel } from '@/types'
|
|
10
|
+
import { BTAuth } from './auth'
|
|
11
|
+
|
|
12
|
+
export type StoreMode = 'whole-last-updated' | 'partial-last-updated' | 'session'
|
|
13
|
+
export type StorageMode = 'session' | 'local-cache'
|
|
14
|
+
|
|
15
|
+
export interface LocalMeta {
|
|
16
|
+
storedOn: string //days
|
|
17
|
+
isDetailed?: boolean
|
|
18
|
+
earliestData?: string
|
|
19
|
+
lastUpdate?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LocallyStoredItem<T> {
|
|
23
|
+
meta: LocalMeta,
|
|
24
|
+
data: T,
|
|
25
|
+
count?: number,
|
|
26
|
+
filters?: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StoreGetAllReturn<T> {
|
|
30
|
+
data: T[],
|
|
31
|
+
count?: number,
|
|
32
|
+
filters?: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StoreGetReturn<T> {
|
|
36
|
+
data: T
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BTStore extends StoreDefinition<any, {}, {},
|
|
40
|
+
{
|
|
41
|
+
deleteItem: (dOptions: PathOptions) => Promise<string | undefined>
|
|
42
|
+
get: <T extends BaseModel>(dOptions: PathOptions) => Promise<StoreGetReturn<T>>
|
|
43
|
+
getAll: <T extends BaseModel>(dOptions: PathOptions) => Promise<StoreGetAllReturn<T>>
|
|
44
|
+
patch: <T extends BaseModel>(dOptions: PathOptions) => Promise<T | undefined>
|
|
45
|
+
post: <T extends BaseModel>(dOptions: PathOptions) => Promise<T | undefined>
|
|
46
|
+
restore: <T extends BaseModel>(dOptions: PathOptions) => Promise<T | undefined>
|
|
47
|
+
}> {}
|
|
48
|
+
|
|
49
|
+
export interface ApiError {
|
|
50
|
+
message: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface UseStoreOptions {
|
|
54
|
+
/**ideally required. Otherwise will struggle to find url path */
|
|
55
|
+
api?: BTApi
|
|
56
|
+
/**ideally required. For setting unique local storage keys */
|
|
57
|
+
auth?: BTAuth
|
|
58
|
+
/**build a query. Overrides the default */
|
|
59
|
+
builderQuery?: (params: any) => string
|
|
60
|
+
/**overrides the default */
|
|
61
|
+
buildUrl?: (path: PathOptions) => string
|
|
62
|
+
/**particularly what store syle to use */
|
|
63
|
+
storeMode: StoreMode
|
|
64
|
+
/**whether to store data locally or only for the duration of the session */
|
|
65
|
+
storageMode: StorageMode
|
|
66
|
+
/**the name of this store */
|
|
67
|
+
storeName: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CreateStoreBuilderOptions {
|
|
71
|
+
/**ideally required. Otherwise will struggle to find url path */
|
|
72
|
+
api?: BTApi
|
|
73
|
+
/**ideally required. For setting unique local storage keys */
|
|
74
|
+
auth?: BTAuth
|
|
75
|
+
/**build a query. Overrides the default */
|
|
76
|
+
builderQuery?: (params: any) => string
|
|
77
|
+
/**overrides the default */
|
|
78
|
+
buildUrl?: (path: PathOptions) => string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CreateStoreOptions {
|
|
82
|
+
/**particularly what store syle to use */
|
|
83
|
+
storeMode: StoreMode
|
|
84
|
+
/**whether to store data locally or only for the duration of the session */
|
|
85
|
+
storageMode: StorageMode
|
|
86
|
+
/**the name of this store */
|
|
87
|
+
storeName: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createStoreBuilder(options: CreateStoreBuilderOptions): (opt: CreateStoreOptions) => BTStore {
|
|
91
|
+
return (opt: CreateStoreOptions) => {
|
|
92
|
+
return createStore({
|
|
93
|
+
...options,
|
|
94
|
+
...opt
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createStore(options: UseStoreOptions): BTStore {
|
|
100
|
+
if (options.storeMode == 'whole-last-updated') {
|
|
101
|
+
if (options.api == null) throw new Error('Must supply an api object to use store')
|
|
102
|
+
|
|
103
|
+
return createWholeLastUpdateStore(options)
|
|
104
|
+
}
|
|
105
|
+
else if (options.storeMode == 'partial-last-updated') {
|
|
106
|
+
if (options.api == null) throw new Error('Must supply an api object to use store')
|
|
107
|
+
|
|
108
|
+
return createWholeLastUpdateStore(options)
|
|
109
|
+
}
|
|
110
|
+
else { //if (options.storeMode == 'session') {
|
|
111
|
+
return createSessionStore(options)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const defaultPathBuilder = (path: PathOptions) => {
|
|
116
|
+
let url: string | undefined = toValue(path.url) ?? undefined
|
|
117
|
+
|
|
118
|
+
if (path.additionalUrl != null) {
|
|
119
|
+
if (url == null)
|
|
120
|
+
url = path.additionalUrl
|
|
121
|
+
else
|
|
122
|
+
url = appendUrl(url, path.additionalUrl)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (path.id != null) {
|
|
126
|
+
if (url?.includes('{id}'))
|
|
127
|
+
url = url.replaceAll('{id}', path.id)
|
|
128
|
+
else
|
|
129
|
+
url = appendUrl(url, path.id)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (path.params != null) {
|
|
133
|
+
url ??= ''
|
|
134
|
+
let query = new URLSearchParams()
|
|
135
|
+
let entries = Object.entries(path.params).sort(firstBy(x => x[0]))
|
|
136
|
+
entries.forEach(entry => {
|
|
137
|
+
if (entry[1] != null)
|
|
138
|
+
query.append(entry[0], entry[1].toString())
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
url = `${url}?${query.toString()}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
path.finalUrl = url
|
|
145
|
+
|
|
146
|
+
return path.finalUrl ?? ''
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface UseSessionStoreOptions {
|
|
150
|
+
/**ideally required. Otherwise will struggle to find url path */
|
|
151
|
+
api?: BTApi
|
|
152
|
+
/**ideally required. For setting unique local storage keys */
|
|
153
|
+
auth?: BTAuth
|
|
154
|
+
/**whether to store data locally or only for the duration of the session */
|
|
155
|
+
storageMode: 'session' | 'local-cache'
|
|
156
|
+
/**build a query. Overrides the default */
|
|
157
|
+
buildQuery?: (params: any) => string
|
|
158
|
+
/**overrides the default */
|
|
159
|
+
buildUrl?: (path: PathOptions) => string
|
|
160
|
+
/**the name of this store */
|
|
161
|
+
storeName: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createSessionStore(options: UseSessionStoreOptions): BTStore {
|
|
165
|
+
return defineStore(options.storeName, () => {
|
|
166
|
+
const currentTimeStampDays = DateTime.utc().toSeconds() / 86400
|
|
167
|
+
const searchMemory: Ref<any> = ref({})
|
|
168
|
+
const promiseMemory: Ref<any> = ref({})
|
|
169
|
+
const cacheLocally = options.storageMode == 'local-cache'
|
|
170
|
+
|
|
171
|
+
const buildPath = options.buildUrl ?? options.api?.buildUrl ?? defaultPathBuilder
|
|
172
|
+
// const authData = useAuthData()
|
|
173
|
+
|
|
174
|
+
function getKey(dOptions: PathOptions) {
|
|
175
|
+
return `${options.storeName}_${options.auth?.credentials.userID ?? 'no-user-id'}_${dOptions.id ?? dOptions.data?.id ?? 'no-item-id'}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function getAll<T extends BaseModel>(dOptions: PathOptions): Promise<StoreGetAllReturn<T>> {
|
|
179
|
+
dOptions.additionalUrl ??= '/getAll'
|
|
180
|
+
|
|
181
|
+
const path = buildPath(dOptions)
|
|
182
|
+
const key = `${options.storeName}_${options.auth?.credentials.userID ?? 'no-user-id'}_${path}`
|
|
183
|
+
const refresh = dOptions.refresh
|
|
184
|
+
|
|
185
|
+
if (!refresh && searchMemory.value[key] !== undefined)
|
|
186
|
+
return searchMemory.value[key] ?? null
|
|
187
|
+
|
|
188
|
+
if (!refresh && cacheLocally == true) {
|
|
189
|
+
//attempt to get locally
|
|
190
|
+
const localRes = await useLocalDb().getItem<LocallyStoredItem<T[]>>(key)
|
|
191
|
+
|
|
192
|
+
if (localRes != null &&
|
|
193
|
+
(options.api == null || parseFloat(localRes.meta.storedOn) > (currentTimeStampDays - 7))) {
|
|
194
|
+
searchMemory.value[key] = localRes
|
|
195
|
+
return localRes
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//nothing exists in session so far so load from api
|
|
200
|
+
if (options.api == null) {
|
|
201
|
+
return searchMemory.value[key]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
let apiPrm = promiseMemory.value[key]
|
|
206
|
+
|
|
207
|
+
if (apiPrm == null) {
|
|
208
|
+
apiPrm = new Promise<StoreGetAllReturn<T> | undefined>(async (resolve, reject) => {
|
|
209
|
+
try {
|
|
210
|
+
let res = await options.api?.getAll<StoreGetAllReturn<T>>(dOptions)
|
|
211
|
+
|
|
212
|
+
if (cacheLocally == true) {
|
|
213
|
+
await useLocalDb().setItem(key, {
|
|
214
|
+
meta: { storedOn: currentTimeStampDays },
|
|
215
|
+
...res
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
resolve(res)
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
reject(err)
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
//remove promise
|
|
226
|
+
delete promiseMemory.value[key]
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
promiseMemory.value[key] = apiPrm
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const apiRes = await apiPrm
|
|
234
|
+
|
|
235
|
+
searchMemory.value[key] = apiRes
|
|
236
|
+
return apiRes
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
let e = err as ApiError
|
|
240
|
+
throw new Error(e.message ?? 'Problem')
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function get<T extends BaseModel>(dOptions: PathOptions): Promise<StoreGetReturn<T>> {
|
|
245
|
+
dOptions.additionalUrl ??= '/get'
|
|
246
|
+
|
|
247
|
+
const key = getKey(dOptions)
|
|
248
|
+
const refresh = dOptions.refresh
|
|
249
|
+
|
|
250
|
+
if (!refresh && searchMemory.value[key] !== undefined)
|
|
251
|
+
return searchMemory.value[key] ?? []
|
|
252
|
+
|
|
253
|
+
if (!refresh && cacheLocally == true ) {
|
|
254
|
+
//attempt to get locally
|
|
255
|
+
const localRes = await useLocalDb().getItem<LocallyStoredItem<T>>(key)
|
|
256
|
+
|
|
257
|
+
if (localRes != null &&
|
|
258
|
+
(options.api == null || parseFloat(localRes.meta.storedOn) > (currentTimeStampDays - 7))) {
|
|
259
|
+
searchMemory.value[key] = localRes
|
|
260
|
+
return localRes
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//nothing exists in session so far so load from api
|
|
265
|
+
if (options.api == null) {
|
|
266
|
+
return searchMemory.value[key]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
let apiPrm = promiseMemory.value[key]
|
|
271
|
+
|
|
272
|
+
if (apiPrm == null) {
|
|
273
|
+
apiPrm = new Promise(async (resolve, reject) => {
|
|
274
|
+
try {
|
|
275
|
+
let res = await options.api?.get<LocallyStoredItem<T>>(dOptions)
|
|
276
|
+
|
|
277
|
+
if (cacheLocally == true) {
|
|
278
|
+
await useLocalDb().setItem(key, {
|
|
279
|
+
meta: { storedOn: currentTimeStampDays },
|
|
280
|
+
...res
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
resolve(res)
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
reject(err)
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
//remove promise
|
|
291
|
+
delete promiseMemory.value[key]
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
promiseMemory.value[key] = apiPrm
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const apiRes = await apiPrm
|
|
299
|
+
searchMemory.value[key] = apiRes
|
|
300
|
+
return apiRes
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
let e = err as ApiError
|
|
304
|
+
throw new Error(e.message ?? 'Problem')
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function patch<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
309
|
+
dOptions.additionalUrl ??= '/patch'
|
|
310
|
+
|
|
311
|
+
const key = getKey(dOptions)
|
|
312
|
+
let patchedObject: any
|
|
313
|
+
|
|
314
|
+
//patch api
|
|
315
|
+
if (options.api != null) {
|
|
316
|
+
//do not bother saving the promise
|
|
317
|
+
try {
|
|
318
|
+
let apiRes = await options.api.patch<StoreGetReturn<T>>(dOptions)
|
|
319
|
+
patchedObject = apiRes?.data
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
let e = err as ApiError
|
|
323
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
patchedObject = dOptions.data
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//update local versions
|
|
331
|
+
if (patchedObject != null) {
|
|
332
|
+
//update keyed search memory
|
|
333
|
+
const existingItem = searchMemory.value[key]
|
|
334
|
+
if (existingItem != null) {
|
|
335
|
+
existingItem.data = { ...existingItem.data, ...patchedObject }
|
|
336
|
+
|
|
337
|
+
//override local cache
|
|
338
|
+
if (cacheLocally == true) {
|
|
339
|
+
await useLocalDb().setItem(key, {
|
|
340
|
+
meta: { storedOn: currentTimeStampDays },
|
|
341
|
+
...existingItem
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
//update other session stored items
|
|
347
|
+
if (patchedObject.id != null && patchedObject.rowVersion != null) {
|
|
348
|
+
let memoryItems = Object.entries(searchMemory.value)
|
|
349
|
+
memoryItems.forEach(entry => {
|
|
350
|
+
let entryVal = entry[1] as any
|
|
351
|
+
entryVal = entryVal.data
|
|
352
|
+
if (entryVal != null) {
|
|
353
|
+
if (Array.isArray(entryVal)) {
|
|
354
|
+
for (let i = 0; i < entryVal.length; i++) {
|
|
355
|
+
const listItem = entryVal[i];
|
|
356
|
+
if (listItem.id == patchedObject.id) {
|
|
357
|
+
entryVal.splice(i, 1, { ...listItem, ...patchedObject})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
if (entryVal.id == patchedObject.id) {
|
|
363
|
+
searchMemory.value[entry[0]].data = { ...entryVal, ...patchedObject}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return patchedObject
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function post<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
375
|
+
dOptions.additionalUrl ??= '/post'
|
|
376
|
+
|
|
377
|
+
const key = getKey(dOptions)
|
|
378
|
+
let postedObject: any
|
|
379
|
+
|
|
380
|
+
//patch api
|
|
381
|
+
if (options.api != null) {
|
|
382
|
+
//do not bother saving the promise
|
|
383
|
+
try {
|
|
384
|
+
let apiRes = await options.api.post<StoreGetReturn<T>>(dOptions)
|
|
385
|
+
postedObject = apiRes?.data
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
let e = err as ApiError
|
|
389
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
postedObject = dOptions.data
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
//update local versions
|
|
397
|
+
searchMemory.value[key] = {
|
|
398
|
+
meta: { storedOn: currentTimeStampDays },
|
|
399
|
+
data: postedObject
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (cacheLocally == true) {
|
|
403
|
+
await useLocalDb().setItem(key, searchMemory.value[key])
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return postedObject
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function deleteItem(dOptions: PathOptions): Promise<string | undefined> {
|
|
410
|
+
dOptions.additionalUrl ??= '/delete'
|
|
411
|
+
const key = getKey(dOptions)
|
|
412
|
+
|
|
413
|
+
//delete api
|
|
414
|
+
if (options.api != null) {
|
|
415
|
+
//do not bother saving the promise
|
|
416
|
+
try {
|
|
417
|
+
let res = await options.api.deleteItem(dOptions)
|
|
418
|
+
if (res != null)
|
|
419
|
+
return res
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
let e = err as ApiError
|
|
423
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//delete local memory
|
|
428
|
+
delete searchMemory.value[key]
|
|
429
|
+
|
|
430
|
+
if (cacheLocally == true) {
|
|
431
|
+
await useLocalDb().removeItem(key)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return undefined
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function restore<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
438
|
+
dOptions.additionalUrl ??= `/patch/restore?id=${dOptions.data.id}`
|
|
439
|
+
|
|
440
|
+
buildPath(dOptions)
|
|
441
|
+
|
|
442
|
+
//patch api
|
|
443
|
+
if (options.api != null) {
|
|
444
|
+
//do not bother saving the promise
|
|
445
|
+
try {
|
|
446
|
+
let apiRes = await options.api.patch<StoreGetReturn<T>>(dOptions)
|
|
447
|
+
return apiRes?.data
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
let e = err as ApiError
|
|
451
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return undefined
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
deleteItem,
|
|
460
|
+
get,
|
|
461
|
+
getAll,
|
|
462
|
+
patch,
|
|
463
|
+
post,
|
|
464
|
+
restore,
|
|
465
|
+
searchMemory //mainly for testing
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
interface UseWholeLastUpdateStoreOptions {
|
|
471
|
+
/**ideally required. Otherwise will struggle to find url path */
|
|
472
|
+
api?: BTApi
|
|
473
|
+
/**ideally required. For setting unique local storage keys */
|
|
474
|
+
auth?: BTAuth
|
|
475
|
+
storageMode: 'session' | 'local-cache'
|
|
476
|
+
storeName: string
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function createWholeLastUpdateStore(options: UseWholeLastUpdateStoreOptions): BTStore {
|
|
480
|
+
return defineStore(options.storeName, () => {
|
|
481
|
+
const dataItems: Ref<any[] | undefined> = ref()
|
|
482
|
+
const count = ref(0)
|
|
483
|
+
const currentTimeStampHours = DateTime.utc().toSeconds() / 3600
|
|
484
|
+
const filters: Ref<string[] | undefined> = ref()
|
|
485
|
+
const meta: Ref<LocalMeta | undefined> = ref()
|
|
486
|
+
const promiseMemory: Ref<any> = ref({})
|
|
487
|
+
const cacheLocally = options.storageMode == 'local-cache'
|
|
488
|
+
|
|
489
|
+
function getKey() {
|
|
490
|
+
return `${options.storeName}_${options.auth?.credentials.userID ?? 'no-user-id'}`
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function trySaveToLocalCache() {
|
|
494
|
+
if (cacheLocally == true) {
|
|
495
|
+
//save locally
|
|
496
|
+
await useLocalDb().setItem(getKey(), {
|
|
497
|
+
meta: meta.value,
|
|
498
|
+
data: dataItems.value,
|
|
499
|
+
count: count.value,
|
|
500
|
+
filters: []
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function createRefreshPromise<T extends BaseModel>(dOptions: PathOptions): Promise<StoreGetAllReturn<T>> {
|
|
506
|
+
const key = getKey()
|
|
507
|
+
|
|
508
|
+
if (promiseMemory.value[key])
|
|
509
|
+
return promiseMemory.value[key]
|
|
510
|
+
|
|
511
|
+
promiseMemory.value[key] = new Promise<StoreGetAllReturn<T>>(async (resolve, reject) => {
|
|
512
|
+
try {
|
|
513
|
+
let res = await options.api?.getAll<StoreGetAllReturn<T>>({
|
|
514
|
+
additionalUrl: '/getAll',
|
|
515
|
+
nav: dOptions.nav,
|
|
516
|
+
params: {
|
|
517
|
+
lastUpdate: meta.value?.lastUpdate ?? getMinDateString()
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
if (res == null) {
|
|
522
|
+
reject(res)
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
//update
|
|
526
|
+
dataItems.value ??= []
|
|
527
|
+
res.data.forEach(serverItem => {
|
|
528
|
+
const existingInd = dataItems.value!.findIndex(x => x.id == serverItem.id)
|
|
529
|
+
if (existingInd >= 0)
|
|
530
|
+
dataItems.value?.splice(existingInd, 1, serverItem)
|
|
531
|
+
else
|
|
532
|
+
dataItems.value?.push(serverItem)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
count.value = dataItems.value.length
|
|
536
|
+
meta.value = {
|
|
537
|
+
lastUpdate: DateTime.utc().toString(),
|
|
538
|
+
storedOn: currentTimeStampHours.toString()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
await trySaveToLocalCache()
|
|
542
|
+
|
|
543
|
+
resolve(res)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
reject(err)
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
//remove promise
|
|
551
|
+
delete promiseMemory.value[key]
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
return promiseMemory.value[key]
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function getAll<T extends BaseModel>(dOptions: PathOptions): Promise<StoreGetAllReturn<T>> {
|
|
559
|
+
const key = getKey()
|
|
560
|
+
const refresh = dOptions.refresh
|
|
561
|
+
dOptions.nav ??= options.storeName
|
|
562
|
+
|
|
563
|
+
if (!refresh && dataItems.value != null) {
|
|
564
|
+
return {
|
|
565
|
+
count: dataItems.value?.length,
|
|
566
|
+
data: dataItems.value,
|
|
567
|
+
filters: []
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!refresh && cacheLocally == true) {
|
|
572
|
+
//retrieve from local cache
|
|
573
|
+
const localRes = await useLocalDb().getItem<LocallyStoredItem<T[]>>(key)
|
|
574
|
+
|
|
575
|
+
if (localRes != null &&
|
|
576
|
+
parseFloat(localRes.meta.storedOn) > (currentTimeStampHours - 12)) {
|
|
577
|
+
dataItems.value = localRes.data
|
|
578
|
+
count.value = localRes.data.length
|
|
579
|
+
filters.value = localRes.filters ?? []
|
|
580
|
+
meta.value = localRes.meta
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
count: dataItems.value?.length,
|
|
584
|
+
data: dataItems.value,
|
|
585
|
+
filters: []
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
//nothing exists in session so far so load from api
|
|
591
|
+
if (options.api == null) {
|
|
592
|
+
return {
|
|
593
|
+
count: dataItems.value?.length,
|
|
594
|
+
data: dataItems.value ?? [],
|
|
595
|
+
filters: filters.value
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
dataItems.value ??= []
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
return await createRefreshPromise(dOptions)
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
let e = err as ApiError
|
|
607
|
+
throw new Error(e.message ?? 'Problem')
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function get<T extends BaseModel>(dOptions: PathOptions): Promise<StoreGetReturn<T>> {
|
|
612
|
+
const key = getKey()
|
|
613
|
+
const refresh = dOptions.refresh
|
|
614
|
+
dOptions.nav ??= options.storeName
|
|
615
|
+
|
|
616
|
+
if (!refresh && dataItems.value != null) {
|
|
617
|
+
return {
|
|
618
|
+
data: dataItems.value.find(x => x.id == dOptions.id)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (!refresh && cacheLocally == true) {
|
|
623
|
+
//retrieve from local cache
|
|
624
|
+
const localRes = await useLocalDb().getItem<LocallyStoredItem<T[]>>(key)
|
|
625
|
+
|
|
626
|
+
if (localRes != null &&
|
|
627
|
+
parseFloat(localRes.meta.storedOn) > (currentTimeStampHours - 12)) {
|
|
628
|
+
dataItems.value = localRes.data
|
|
629
|
+
count.value = localRes.data.length
|
|
630
|
+
filters.value = localRes.filters ?? []
|
|
631
|
+
meta.value = localRes.meta
|
|
632
|
+
|
|
633
|
+
let existingItem = dataItems.value.find(x => x.id == dOptions.id)
|
|
634
|
+
|
|
635
|
+
if (existingItem != null)
|
|
636
|
+
return { data: existingItem }
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (options.api == null) {
|
|
641
|
+
return {
|
|
642
|
+
data: dataItems.value?.find(x => x.id == dOptions.id)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
dataItems.value ??= []
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
await createRefreshPromise<T>(dOptions)
|
|
651
|
+
|
|
652
|
+
return { data: dataItems.value?.find(x => x.id == dOptions.id) }
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
let e = err as ApiError
|
|
656
|
+
throw new Error(e.message ?? 'Problem')
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function patch<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
661
|
+
dOptions.additionalUrl ??= '/patch'
|
|
662
|
+
dOptions.nav ??= options.storeName
|
|
663
|
+
|
|
664
|
+
let patchedObject: any
|
|
665
|
+
|
|
666
|
+
//patch api
|
|
667
|
+
if (options.api != null) {
|
|
668
|
+
//do not bother saving the promise
|
|
669
|
+
try {
|
|
670
|
+
let apiRes = await options.api.patch<StoreGetReturn<T>>(dOptions)
|
|
671
|
+
patchedObject = { ...dOptions.data, ...apiRes?.data }
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
let e = err as ApiError
|
|
675
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
patchedObject = dOptions.data
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (patchedObject != null) {
|
|
683
|
+
let existingInd = dataItems.value?.findIndex(x => x.id == patchedObject.id)
|
|
684
|
+
if (existingInd != null && existingInd >= 0) {
|
|
685
|
+
dataItems.value ??= []
|
|
686
|
+
dataItems.value?.splice(existingInd, 1, patchedObject)
|
|
687
|
+
await trySaveToLocalCache()
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return dataItems.value?.find(x => x.id == (dOptions.id ?? dOptions.data.id))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function post<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
695
|
+
dOptions.additionalUrl ??= '/post'
|
|
696
|
+
dOptions.nav ??= options.storeName
|
|
697
|
+
|
|
698
|
+
let postedObject: any
|
|
699
|
+
|
|
700
|
+
//post api
|
|
701
|
+
if (options.api != null) {
|
|
702
|
+
//do not bother saving the promise
|
|
703
|
+
try {
|
|
704
|
+
let apiRes = await options.api.post<StoreGetReturn<T>>(dOptions)
|
|
705
|
+
postedObject = apiRes?.data
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
let e = err as ApiError
|
|
709
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
postedObject = dOptions.data
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (postedObject != null) {
|
|
717
|
+
dataItems.value ??= []
|
|
718
|
+
dataItems.value?.unshift(postedObject)
|
|
719
|
+
count.value += 1
|
|
720
|
+
await trySaveToLocalCache()
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return dataItems.value?.find(x => x.id == (dOptions.id ?? dOptions.data.id))
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function deleteItem(dOptions: PathOptions): Promise<string | undefined> {
|
|
727
|
+
dOptions.additionalUrl ??= '/delete'
|
|
728
|
+
dOptions.nav ??= options.storeName
|
|
729
|
+
|
|
730
|
+
//delete api
|
|
731
|
+
if (options.api != null) {
|
|
732
|
+
//do not bother saving the promise
|
|
733
|
+
try {
|
|
734
|
+
let res = await options.api.deleteItem(dOptions)
|
|
735
|
+
if (res != null)
|
|
736
|
+
return res
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
let e = err as ApiError
|
|
740
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const id = dOptions.id ?? dOptions.data.id
|
|
745
|
+
if (id != null) {
|
|
746
|
+
let existingInd = dataItems.value?.findIndex(x => x.id == id)
|
|
747
|
+
if (existingInd != null && existingInd >= 0) {
|
|
748
|
+
dataItems.value ??= []
|
|
749
|
+
dataItems.value?.splice(existingInd, 1)
|
|
750
|
+
await trySaveToLocalCache()
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return undefined
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function restore<T extends BaseModel>(dOptions: PathOptions): Promise<T | undefined> {
|
|
758
|
+
dOptions.additionalUrl ??= `/patch/restore?id=${dOptions.data.id}`
|
|
759
|
+
dOptions.nav ??= options.storeName
|
|
760
|
+
|
|
761
|
+
//patch api
|
|
762
|
+
if (options.api != null) {
|
|
763
|
+
//do not bother saving the promise
|
|
764
|
+
try {
|
|
765
|
+
await options.api.patch<StoreGetReturn<T>>(dOptions)
|
|
766
|
+
// let apiRes = await options.api.patch<StoreGetReturn<T>>(dOptions)
|
|
767
|
+
// return apiRes?.data
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
let e = err as ApiError
|
|
771
|
+
throw new Error(e.message ?? 'Patch Problem')
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
//refresh
|
|
776
|
+
try {
|
|
777
|
+
dOptions.additionalUrl = '/getAll'
|
|
778
|
+
await createRefreshPromise<any>(dOptions)
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
let e = err as ApiError
|
|
782
|
+
throw new Error(e.message ?? 'Problem')
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return dataItems.value?.find(x => x.id == (dOptions.id ?? dOptions.data.id))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
deleteItem,
|
|
790
|
+
get,
|
|
791
|
+
getAll,
|
|
792
|
+
patch,
|
|
793
|
+
post,
|
|
794
|
+
restore
|
|
795
|
+
}
|
|
796
|
+
})
|
|
797
|
+
}
|