feat(stage-ui): use local first request (#977)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { getActivePinia } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
async function canUseRemote(allowRemote?: () => boolean | Promise<boolean>) {
|
||||
if (allowRemote)
|
||||
return await allowRemote()
|
||||
if (!getActivePinia())
|
||||
return true
|
||||
return useAuthStore().isAuthenticated
|
||||
}
|
||||
|
||||
export interface UseLocalFirstRequestOptions<T> {
|
||||
local: () => Promise<T> | T
|
||||
remote: () => Promise<T>
|
||||
allowRemote?: () => boolean | Promise<boolean>
|
||||
lazy?: boolean
|
||||
}
|
||||
|
||||
export function useLocalFirstRequest<T>(options: UseLocalFirstRequestOptions<T>) {
|
||||
const { local, remote, allowRemote, lazy = false } = options
|
||||
|
||||
const state = ref<T>()
|
||||
const isLoading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
const execute = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
state.value = await local()
|
||||
if (await canUseRemote(allowRemote)) {
|
||||
try {
|
||||
state.value = await remote()
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!lazy)
|
||||
execute()
|
||||
|
||||
return {
|
||||
state,
|
||||
isLoading,
|
||||
error,
|
||||
execute,
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,14 @@ export interface UseOptimisticMutationOptions<T, R, E = unknown> {
|
||||
* Optional callback on error. Rollback is handled automatically.
|
||||
*/
|
||||
onError?: (error?: E | null) => void | Promise<void>
|
||||
/**
|
||||
* Skip the action when this returns true.
|
||||
*/
|
||||
skipActionIf?: () => boolean | Promise<boolean>
|
||||
/**
|
||||
* Decide whether to rollback after an error.
|
||||
*/
|
||||
shouldRollback?: (error: E) => boolean | Promise<boolean>
|
||||
|
||||
/**
|
||||
* Whether to execute the action lazily.
|
||||
@@ -31,10 +39,21 @@ export interface UseOptimisticMutationOptions<T, R, E = unknown> {
|
||||
* TODO: use https://pinia-colada.esm.dev/guide/mutations.html instead.
|
||||
*/
|
||||
export function useOptimisticMutation<T, R = T, E = unknown>(options: UseOptimisticMutationOptions<T, R, E>) {
|
||||
const { apply, action, onSuccess, onError, lazy = false } = options
|
||||
const {
|
||||
apply,
|
||||
action,
|
||||
onSuccess,
|
||||
onError,
|
||||
skipActionIf,
|
||||
shouldRollback,
|
||||
lazy = false,
|
||||
} = options
|
||||
|
||||
return useAsyncState(async () => {
|
||||
const rollback = await apply()
|
||||
if (skipActionIf && await skipActionIf()) {
|
||||
return undefined as R
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await action()
|
||||
@@ -44,7 +63,8 @@ export function useOptimisticMutation<T, R = T, E = unknown>(options: UseOptimis
|
||||
return result as unknown as R
|
||||
}
|
||||
catch (err) {
|
||||
if (typeof rollback === 'function') {
|
||||
const allowRollback = shouldRollback ? await shouldRollback(err as E) : true
|
||||
if (allowRollback && typeof rollback === 'function') {
|
||||
await rollback()
|
||||
}
|
||||
if (onError) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User>()
|
||||
const session = ref<Session>()
|
||||
const isAuthenticated = computed(() => !!user.value && !!session.value)
|
||||
const userId = computed(() => user.value?.id ?? 'local')
|
||||
|
||||
const isLoginOpen = ref(false)
|
||||
|
||||
@@ -26,6 +27,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
return {
|
||||
user,
|
||||
userId,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoginOpen,
|
||||
|
||||
@@ -1,16 +1,80 @@
|
||||
import type { Character, CreateCharacterPayload, UpdateCharacterPayload } from '../types/character'
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { parse } from 'valibot'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { client } from '../composables/api'
|
||||
import { useAsyncState } from '../composables/use-async-state'
|
||||
import { useLocalFirstRequest } from '../composables/use-local-first'
|
||||
import { charactersRepo } from '../database/repos/characters.repo'
|
||||
import { CharacterWithRelationsSchema } from '../types/character'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
function buildLocalCharacter(userId: string, payload: CreateCharacterPayload) {
|
||||
const id = payload.character.id ?? nanoid()
|
||||
const now = new Date()
|
||||
|
||||
return parse(CharacterWithRelationsSchema, {
|
||||
id,
|
||||
version: payload.character.version,
|
||||
coverUrl: payload.character.coverUrl,
|
||||
avatarUrl: undefined,
|
||||
characterAvatarUrl: undefined,
|
||||
coverBackgroundUrl: undefined,
|
||||
creatorRole: undefined,
|
||||
priceCredit: '0',
|
||||
likesCount: 0,
|
||||
bookmarksCount: 0,
|
||||
interactionsCount: 0,
|
||||
forksCount: 0,
|
||||
creatorId: userId,
|
||||
ownerId: userId,
|
||||
characterId: payload.character.characterId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: undefined,
|
||||
capabilities: payload.capabilities?.map(capability => ({
|
||||
id: nanoid(),
|
||||
characterId: id,
|
||||
type: capability.type,
|
||||
config: capability.config,
|
||||
})),
|
||||
avatarModels: payload.avatarModels?.map(model => ({
|
||||
id: nanoid(),
|
||||
characterId: id,
|
||||
name: model.name,
|
||||
type: model.type,
|
||||
description: model.description,
|
||||
config: model.config,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
i18n: payload.i18n?.map(item => ({
|
||||
id: nanoid(),
|
||||
characterId: id,
|
||||
language: item.language,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
tags: item.tags,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
prompts: payload.prompts?.map(prompt => ({
|
||||
id: nanoid(),
|
||||
characterId: id,
|
||||
language: prompt.language,
|
||||
type: prompt.type,
|
||||
content: prompt.content,
|
||||
})),
|
||||
likes: [],
|
||||
bookmarks: [],
|
||||
})
|
||||
}
|
||||
|
||||
export const useCharacterStore = defineStore('characters', () => {
|
||||
const characters = ref<Map<string, Character>>(new Map())
|
||||
const auth = useAuthStore()
|
||||
|
||||
async function fetchList(all: boolean = false) {
|
||||
// Load from storage immediately
|
||||
@@ -22,117 +86,204 @@ export const useCharacterStore = defineStore('characters', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters.$get({
|
||||
query: { all: String(all) },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch characters')
|
||||
}
|
||||
const data = await res.json()
|
||||
return useLocalFirstRequest({
|
||||
local: async () => undefined,
|
||||
remote: async () => {
|
||||
const res = await client.api.characters.$get({
|
||||
query: { all: String(all) },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch characters')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
characters.value.clear()
|
||||
const parsedData: Character[] = []
|
||||
for (const char of data) {
|
||||
const parsed = parse(CharacterWithRelationsSchema, char)
|
||||
characters.value.set(char.id, parsed)
|
||||
parsedData.push(parsed)
|
||||
}
|
||||
await charactersRepo.saveAll(parsedData)
|
||||
}, { immediate: true })
|
||||
characters.value.clear()
|
||||
const parsedData: Character[] = []
|
||||
for (const char of data) {
|
||||
const parsed = parse(CharacterWithRelationsSchema, char)
|
||||
characters.value.set(char.id, parsed)
|
||||
parsedData.push(parsed)
|
||||
}
|
||||
await charactersRepo.saveAll(parsedData)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchById(id: string) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters[':id'].$get({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
const cached = characters.value.get(id) ?? (await charactersRepo.getAll()).find(char => char.id === id)
|
||||
if (cached) {
|
||||
characters.value.set(cached.id, cached)
|
||||
}
|
||||
return cached
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await client.api.characters[':id'].$get({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
}, { immediate: true })
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function create(payload: CreateCharacterPayload) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters.$post({
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to create character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
let localCharacter: Character
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
localCharacter = buildLocalCharacter(auth.userId, payload)
|
||||
characters.value.set(localCharacter.id, localCharacter)
|
||||
await charactersRepo.upsert(localCharacter)
|
||||
return localCharacter
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await client.api.characters.$post({
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to create character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
}, { immediate: true })
|
||||
// Replace local temp character with remote data
|
||||
characters.value.delete(localCharacter.id)
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.remove(localCharacter.id)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function update(id: string, payload: UpdateCharacterPayload) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await (client.api.characters[':id'].$patch)({
|
||||
param: { id },
|
||||
// @ts-expect-error FIXME: hono client typing misses json option for this route
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to update character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
const character = characters.value.get(id)
|
||||
if (!character) {
|
||||
return
|
||||
}
|
||||
if (payload.version !== undefined)
|
||||
character.version = payload.version
|
||||
if (payload.coverUrl !== undefined)
|
||||
character.coverUrl = payload.coverUrl
|
||||
if (payload.characterId !== undefined)
|
||||
character.characterId = payload.characterId
|
||||
character.updatedAt = new Date()
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await (client.api.characters[':id'].$patch)({
|
||||
param: { id },
|
||||
// @ts-expect-error FIXME: hono client typing misses json option for this route
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to update character')
|
||||
}
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
}, { immediate: true })
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters[':id'].$delete({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to remove character')
|
||||
}
|
||||
|
||||
characters.value.delete(id)
|
||||
await charactersRepo.remove(id)
|
||||
}, { immediate: true })
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
characters.value.delete(id)
|
||||
await charactersRepo.remove(id)
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await client.api.characters[':id'].$delete({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to remove character')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function like(id: string) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters[':id'].like.$post({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to like character')
|
||||
}
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
const character = characters.value.get(id)
|
||||
if (!character) {
|
||||
return
|
||||
}
|
||||
const likes = character.likes ?? []
|
||||
if (!likes.some(item => item.userId === auth.userId)) {
|
||||
likes.push({ userId: auth.userId, characterId: id })
|
||||
character.likes = likes
|
||||
character.likesCount += 1
|
||||
character.updatedAt = new Date()
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
}
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await client.api.characters[':id'].like.$post({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to like character')
|
||||
}
|
||||
|
||||
await fetchById(id)
|
||||
}, { immediate: true })
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function bookmark(id: string) {
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.characters[':id'].bookmark.$post({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to bookmark character')
|
||||
}
|
||||
return useLocalFirstRequest({
|
||||
local: async () => {
|
||||
const character = characters.value.get(id)
|
||||
if (!character) {
|
||||
return
|
||||
}
|
||||
const bookmarks = character.bookmarks ?? []
|
||||
if (!bookmarks.some(item => item.userId === auth.userId)) {
|
||||
bookmarks.push({ userId: auth.userId, characterId: id })
|
||||
character.bookmarks = bookmarks
|
||||
character.bookmarksCount += 1
|
||||
character.updatedAt = new Date()
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
}
|
||||
},
|
||||
remote: async () => {
|
||||
const res = await client.api.characters[':id'].bookmark.$post({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to bookmark character')
|
||||
}
|
||||
|
||||
await fetchById(id)
|
||||
}, { immediate: true })
|
||||
const data = await res.json()
|
||||
const character = parse(CharacterWithRelationsSchema, data)
|
||||
characters.value.set(character.id, character)
|
||||
await charactersRepo.upsert(character)
|
||||
return character
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getCharacter(id: string) {
|
||||
|
||||
@@ -5,8 +5,7 @@ import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { client } from '../composables/api'
|
||||
import { useAsyncState } from '../composables/use-async-state'
|
||||
import { useOptimisticMutation } from '../composables/use-optimistic'
|
||||
import { useLocalFirstRequest } from '../composables/use-local-first'
|
||||
import { providersRepo } from '../database/repos/providers.repo'
|
||||
import { getDefinedProvider, listProviders } from '../libs/providers/providers'
|
||||
|
||||
@@ -21,27 +20,30 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
configs.value = cached
|
||||
}
|
||||
|
||||
return useAsyncState(async () => {
|
||||
const res = await client.api.providers.$get()
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch providers')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
const newConfigs: Record<string, ProviderCatalogProvider> = {}
|
||||
for (const item of data) {
|
||||
newConfigs[item.id] = {
|
||||
id: item.id,
|
||||
definitionId: item.definitionId,
|
||||
name: item.name,
|
||||
config: item.config as Record<string, any>,
|
||||
validated: item.validated,
|
||||
validationBypassed: item.validationBypassed,
|
||||
return useLocalFirstRequest({
|
||||
local: async () => undefined,
|
||||
remote: async () => {
|
||||
const res = await client.api.providers.$get()
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch providers')
|
||||
}
|
||||
}
|
||||
configs.value = newConfigs
|
||||
await providersRepo.saveAll(newConfigs)
|
||||
}, { immediate: true })
|
||||
const data = await res.json()
|
||||
|
||||
const newConfigs: Record<string, ProviderCatalogProvider> = {}
|
||||
for (const item of data) {
|
||||
newConfigs[item.id] = {
|
||||
id: item.id,
|
||||
definitionId: item.definitionId,
|
||||
name: item.name,
|
||||
config: item.config as Record<string, any>,
|
||||
validated: item.validated,
|
||||
validationBypassed: item.validationBypassed,
|
||||
}
|
||||
}
|
||||
configs.value = newConfigs
|
||||
await providersRepo.saveAll(newConfigs)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function addProvider(definitionId: string, initialConfig: Record<string, any> = {}) {
|
||||
@@ -60,16 +62,13 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
validationBypassed: false,
|
||||
}
|
||||
|
||||
return useOptimisticMutation<any, any>({
|
||||
apply: async () => {
|
||||
return useLocalFirstRequest<any>({
|
||||
local: async () => {
|
||||
configs.value[id] = provider
|
||||
await providersRepo.upsert(provider)
|
||||
return async () => {
|
||||
delete configs.value[id]
|
||||
await providersRepo.remove(id)
|
||||
}
|
||||
return provider
|
||||
},
|
||||
action: async () => {
|
||||
remote: async () => {
|
||||
const res = await client.api.providers.$post({
|
||||
json: {
|
||||
id,
|
||||
@@ -83,9 +82,7 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to add provider')
|
||||
}
|
||||
return await res.json()
|
||||
},
|
||||
onSuccess: async (item: any) => {
|
||||
const item = await res.json()
|
||||
const finalProvider: ProviderCatalogProvider = {
|
||||
id: item.id,
|
||||
definitionId: item.definitionId,
|
||||
@@ -103,21 +100,16 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
}
|
||||
|
||||
async function removeProvider(providerId: string) {
|
||||
const original = configs.value[providerId]
|
||||
if (!original) {
|
||||
if (!configs.value[providerId]) {
|
||||
return
|
||||
}
|
||||
|
||||
return useOptimisticMutation<void, void>({
|
||||
apply: async () => {
|
||||
return useLocalFirstRequest<void>({
|
||||
local: async () => {
|
||||
delete configs.value[providerId]
|
||||
await providersRepo.remove(providerId)
|
||||
return async () => {
|
||||
configs.value[providerId] = original
|
||||
await providersRepo.upsert(original)
|
||||
}
|
||||
},
|
||||
action: async () => {
|
||||
remote: async () => {
|
||||
const res = await client.api.providers[':id'].$delete({
|
||||
param: { id: providerId },
|
||||
})
|
||||
@@ -134,24 +126,15 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
return
|
||||
}
|
||||
|
||||
const originalConfig = { ...provider.config }
|
||||
const originalValidated = provider.validated
|
||||
const originalValidationBypassed = provider.validationBypassed
|
||||
|
||||
return useOptimisticMutation<any, void>({
|
||||
apply: async () => {
|
||||
return useLocalFirstRequest<any>({
|
||||
local: async () => {
|
||||
provider.config = { ...newConfig }
|
||||
provider.validated = options.validated
|
||||
provider.validationBypassed = options.validationBypassed
|
||||
await providersRepo.upsert(provider)
|
||||
return async () => {
|
||||
provider.config = originalConfig
|
||||
provider.validated = originalValidated
|
||||
provider.validationBypassed = originalValidationBypassed
|
||||
await providersRepo.upsert(provider)
|
||||
}
|
||||
return provider
|
||||
},
|
||||
action: async () => {
|
||||
remote: async () => {
|
||||
const res = await client.api.providers[':id'].$patch({
|
||||
param: { id: providerId },
|
||||
// @ts-expect-error hono client typing misses json option for this route
|
||||
@@ -164,14 +147,13 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to update provider config')
|
||||
}
|
||||
return await res.json()
|
||||
},
|
||||
onSuccess: async (item: any) => {
|
||||
const item = await res.json()
|
||||
// Sync with server response just in case
|
||||
provider.config = { ...item.config as Record<string, any> }
|
||||
provider.validated = item.validated
|
||||
provider.validationBypassed = item.validationBypassed
|
||||
await providersRepo.upsert(provider)
|
||||
return item
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user