feat(fe2): greatly improved DX for apollo cache modification (#2831)
* proof of concept - types work * WIP * wipp * new modifyObjectFieldf * updatePathIfExists * wipp * working? * projects dashboard test * more improvements * more improvements * fixx * bugfix * minor fixes and cleanup * moar cleanup * autoEvictFiltered
This commit is contained in:
committed by
GitHub
parent
2e272b321e
commit
596ccf8ee3
@@ -35,7 +35,7 @@ const config: CodegenConfig = {
|
||||
fragmentMasking: false,
|
||||
dedupeFragments: true
|
||||
},
|
||||
plugins: []
|
||||
plugins: ['./tools/gqlCacheHelpersCodegenPlugin.js']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,7 @@ import {
|
||||
projectsDashboardWorkspaceQuery
|
||||
} from '~~/lib/projects/graphql/queries'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import {
|
||||
getCacheId,
|
||||
evictObjectFields,
|
||||
modifyObjectFields
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import type { User, UserProjectsArgs } from '~~/lib/common/generated/gql/graphql'
|
||||
import { getCacheId, modifyObjectField } from '~~/lib/common/helpers/graphql'
|
||||
import { UserProjectsUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
|
||||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
|
||||
import { projectRoute } from '~~/lib/common/helpers/route'
|
||||
@@ -171,35 +166,19 @@ onUserProjectsUpdate((res) => {
|
||||
|
||||
if (isNewProject && incomingProject) {
|
||||
// Add to User.projects where possible
|
||||
modifyObjectFields<UserProjectsArgs, User['projects']>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('User', activeUserId),
|
||||
(fieldName, variables, value, { ref }) => {
|
||||
if (fieldName !== 'projects') return
|
||||
if (variables.filter?.search?.length) return
|
||||
if (variables.filter?.onlyWithRoles?.length) {
|
||||
const roles = variables.filter.onlyWithRoles
|
||||
if (!roles.includes(incomingProject.role || '')) return
|
||||
}
|
||||
|
||||
return {
|
||||
...value,
|
||||
items: [ref('Project', incomingProject.id), ...(value.items || [])],
|
||||
totalCount: (value.totalCount || 0) + 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Elsewhere - just evict fields directly
|
||||
evictObjectFields<UserProjectsArgs, User['projects']>(
|
||||
cache,
|
||||
getCacheId('User', activeUserId),
|
||||
(fieldName, variables) => {
|
||||
if (fieldName !== 'projects') return false
|
||||
if (variables.filter?.search?.length) return true
|
||||
|
||||
return false
|
||||
}
|
||||
'projects',
|
||||
({ helpers: { ref, createUpdatedValue } }) =>
|
||||
createUpdatedValue(({ update }) => {
|
||||
update('items', (items) => [
|
||||
ref('Project', incomingProject.id),
|
||||
...(items || [])
|
||||
])
|
||||
update('totalCount', (count) => count + 1)
|
||||
}),
|
||||
{ autoEvictFiltered: true }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useQuery } from '@vue/apollo-composable'
|
||||
import { convertThrowIntoFetchResult } from '~/lib/common/helpers/graphql'
|
||||
import type { InfiniteLoaderState } from '@speckle/ui-components'
|
||||
import { isUndefined } from 'lodash-es'
|
||||
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
|
||||
import { type MaybeNullOrUndefined, type Optional } from '@speckle/shared'
|
||||
import { useScopedState } from '~/lib/common/composables/scopedState'
|
||||
|
||||
export const useApolloClientIfAvailable = () => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { isUndefinedOrVoid } from '@speckle/shared'
|
||||
import type { Optional } from '@speckle/shared'
|
||||
import { isNullOrUndefined, isUndefinedOrVoid } from '@speckle/shared'
|
||||
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
|
||||
import { ApolloError, defaultDataIdFromObject } from '@apollo/client/core'
|
||||
import type {
|
||||
FetchResult,
|
||||
@@ -15,30 +15,54 @@ import { GraphQLError } from 'graphql'
|
||||
import type { DocumentNode } from 'graphql'
|
||||
import {
|
||||
flatten,
|
||||
isUndefined,
|
||||
has,
|
||||
isFunction,
|
||||
isString,
|
||||
isArray,
|
||||
intersection
|
||||
intersection,
|
||||
get,
|
||||
set,
|
||||
cloneDeep,
|
||||
isObjectLike
|
||||
} from 'lodash-es'
|
||||
import type { Modifier, Reference } from '@apollo/client/cache'
|
||||
import type { PartialDeep } from 'type-fest'
|
||||
import type { Modifier, ModifierDetails, Reference } from '@apollo/client/cache'
|
||||
import type { Get, PartialDeep, Paths, ReadonlyDeep, Tagged } from 'type-fest'
|
||||
import type { GraphQLErrors, NetworkError } from '@apollo/client/errors'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { StackTrace } from '~~/lib/common/helpers/debugging'
|
||||
import dayjs from 'dayjs'
|
||||
import { base64Encode } from '~/lib/common/helpers/encodeDecode'
|
||||
import type { ErrorResponse } from '@apollo/client/link/error'
|
||||
import type {
|
||||
AllObjectFieldArgTypes,
|
||||
AllObjectTypes
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
/**
|
||||
* Cache key of a specific cached GQL object. Tends to look like `Type:id`.
|
||||
*/
|
||||
export type ApolloCacheObjectKey<Type extends keyof AllObjectTypes> = Tagged<
|
||||
string,
|
||||
Type
|
||||
>
|
||||
|
||||
export const isServerError = (err: Error): err is ServerError =>
|
||||
has(err, 'response') && has(err, 'result') && has(err, 'statusCode')
|
||||
export const isServerParseError = (err: Error): err is ServerParseError =>
|
||||
has(err, 'response') && has(err, 'bodyText') && has(err, 'statusCode')
|
||||
|
||||
export const ROOT_QUERY = 'ROOT_QUERY'
|
||||
export const ROOT_MUTATION = 'ROOT_MUTATION'
|
||||
export const ROOT_SUBSCRIPTION = 'ROOT_SUBSCRIPTION'
|
||||
export const ROOT_QUERY = 'ROOT_QUERY' as ApolloCacheObjectKey<'Query'>
|
||||
export const ROOT_MUTATION = 'ROOT_MUTATION' as ApolloCacheObjectKey<'Mutation'>
|
||||
export const ROOT_SUBSCRIPTION =
|
||||
'ROOT_SUBSCRIPTION' as ApolloCacheObjectKey<'Subscription'>
|
||||
|
||||
type ModifyFnCacheDataSingle<Data> = Data extends Record<string, unknown>
|
||||
? Data extends { id: string; __typename?: infer TypeName }
|
||||
? CacheObjectReference<TypeName extends keyof AllObjectTypes ? TypeName : string>
|
||||
: PartialDeep<{
|
||||
[key in keyof Data]: ModifyFnCacheData<Data[key]>
|
||||
}>
|
||||
: Data
|
||||
|
||||
/**
|
||||
* Utility type for typing cached data in Apollo modify functions.
|
||||
@@ -46,33 +70,24 @@ export const ROOT_SUBSCRIPTION = 'ROOT_SUBSCRIPTION'
|
||||
* to CacheObjectReference objects and additionally all properties are optional as you can never know what exactly has been requested & cached
|
||||
* and what hasn't
|
||||
*/
|
||||
export type ModifyFnCacheData<Data> = Data extends
|
||||
| Record<string, unknown>
|
||||
| Record<string, unknown>[]
|
||||
? Data extends { id: string }
|
||||
? CacheObjectReference
|
||||
: Data extends { id: string }[]
|
||||
? CacheObjectReference[]
|
||||
: PartialDeep<{
|
||||
[key in keyof Data]: Data[key] extends { id: string }
|
||||
? CacheObjectReference
|
||||
: Data[key] extends { id: string }[]
|
||||
? CacheObjectReference[]
|
||||
: ModifyFnCacheData<Data[key]>
|
||||
}>
|
||||
: Data
|
||||
export type ModifyFnCacheData<Data> = Data extends Array<infer ArrayItem>
|
||||
? ModifyFnCacheDataSingle<ArrayItem>[]
|
||||
: ModifyFnCacheDataSingle<Data>
|
||||
|
||||
/**
|
||||
* Get a cached object's identifier
|
||||
*/
|
||||
export function getCacheId(typeName: string, id: string) {
|
||||
export function getCacheId<Type extends keyof AllObjectTypes>(
|
||||
typeName: Type,
|
||||
id: string
|
||||
): ApolloCacheObjectKey<Type> {
|
||||
const cachedId = defaultDataIdFromObject({
|
||||
__typename: typeName,
|
||||
id
|
||||
})
|
||||
if (!cachedId) throw new Error('Unable to build Apollo cache ID')
|
||||
|
||||
return cachedId
|
||||
return cachedId as ApolloCacheObjectKey<Type>
|
||||
}
|
||||
|
||||
export function isInvalidAuth(error: ApolloError | NetworkError) {
|
||||
@@ -277,16 +292,23 @@ export function getStoreFieldName(
|
||||
* Inside cache.modify calls you'll get these instead of full objects when reading fields that hold
|
||||
* identifiable objects or object arrays
|
||||
*/
|
||||
export type CacheObjectReference = Reference
|
||||
export type CacheObjectReference<Type extends string = string> = {
|
||||
readonly __ref: Type extends keyof AllObjectTypes
|
||||
? ApolloCacheObjectKey<Type>
|
||||
: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Objects & object arrays in `cache.modify` calls are represented through reference objects, so
|
||||
* if you want to add new ones you shouldn't add the entire object, but only its reference
|
||||
*/
|
||||
export function getObjectReference(typeName: string, id: string): CacheObjectReference {
|
||||
export function getObjectReference<Type extends keyof AllObjectTypes>(
|
||||
typeName: Type,
|
||||
id: string
|
||||
): CacheObjectReference<Type> {
|
||||
return {
|
||||
__ref: getCacheId(typeName, id)
|
||||
}
|
||||
} as CacheObjectReference<Type>
|
||||
}
|
||||
|
||||
export function isReference(obj: unknown): obj is CacheObjectReference {
|
||||
@@ -332,6 +354,7 @@ export const revolveFieldNameAndVariables = <
|
||||
* better filtering capabilities to filter filters to update (e.g. you can actually get each field's variables)
|
||||
* Note: This uses cache.modify underneath which means that `data` will only hold object references (CacheObjectReference) not
|
||||
* full objects. Read more: https://www.apollographql.com/docs/react/caching/cache-interaction/#values-vs-references
|
||||
* @deprecated Use modifyObjectField instead
|
||||
*/
|
||||
export function modifyObjectFields<
|
||||
Variables extends Optional<Record<string, unknown>> = undefined,
|
||||
@@ -350,7 +373,8 @@ export function modifyObjectFields<
|
||||
) =>
|
||||
| Optional<ModifyFnCacheData<FieldData>>
|
||||
| Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1]['DELETE']
|
||||
| Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1]['INVALIDATE'],
|
||||
| Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1]['INVALIDATE']
|
||||
| void,
|
||||
options?: Partial<{
|
||||
fieldNameWhitelist: string[]
|
||||
debug: boolean
|
||||
@@ -391,22 +415,27 @@ export function modifyObjectFields<
|
||||
)
|
||||
|
||||
log('invoking updater', { fieldName, variables, fieldValue })
|
||||
const res = updater(
|
||||
fieldName,
|
||||
(variables || {}) as Variables,
|
||||
fieldValue as ModifyFnCacheData<FieldData>,
|
||||
{
|
||||
...details,
|
||||
ref: getObjectReference,
|
||||
revolveFieldNameAndVariables
|
||||
}
|
||||
)
|
||||
try {
|
||||
const res = updater(
|
||||
fieldName,
|
||||
(variables || {}) as Variables,
|
||||
fieldValue as ModifyFnCacheData<FieldData>,
|
||||
{
|
||||
...details,
|
||||
ref: getObjectReference,
|
||||
revolveFieldNameAndVariables
|
||||
}
|
||||
)
|
||||
|
||||
if (isUndefined(res)) {
|
||||
return fieldValue as unknown
|
||||
} else {
|
||||
log('updater returned', { res })
|
||||
return res
|
||||
if (isUndefinedOrVoid(res)) {
|
||||
return fieldValue as unknown
|
||||
} else {
|
||||
log('updater returned', { res })
|
||||
return res
|
||||
}
|
||||
} catch (e) {
|
||||
log('updater threw an error', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -416,6 +445,7 @@ export function modifyObjectFields<
|
||||
* Iterate over a cached object's fields and evict/delete the ones that the predicate returns true for
|
||||
* Note: This uses cache.modify underneath which means that `data` will only hold object references (CacheObjectReference) not
|
||||
* full objects. Read more: https://www.apollographql.com/docs/react/caching/cache-interaction/#values-vs-references
|
||||
* @deprecated Use modifyObjectField instead, just return the evict() call from the updater
|
||||
*/
|
||||
export function evictObjectFields<
|
||||
V extends Optional<Record<string, unknown>> = undefined,
|
||||
@@ -498,44 +528,6 @@ export const getDateCursorFromReference = (params: {
|
||||
return base64Encode(iso)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified version of modifyObjectFields, just targetting a single field
|
||||
* @see modifyObjectFields
|
||||
*/
|
||||
export const modifyObjectField = <
|
||||
FieldData = unknown,
|
||||
Variables extends Optional<Record<string, unknown>> = undefined
|
||||
>(
|
||||
cache: ApolloCache<unknown>,
|
||||
id: string,
|
||||
fieldName: string,
|
||||
updater: (params: {
|
||||
fieldName: string
|
||||
variables: Variables
|
||||
value: ModifyFnCacheData<FieldData>
|
||||
details: Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1] & {
|
||||
ref: typeof getObjectReference
|
||||
revolveFieldNameAndVariables: typeof revolveFieldNameAndVariables
|
||||
}
|
||||
}) =>
|
||||
| Optional<ModifyFnCacheData<FieldData>>
|
||||
| Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1]['DELETE']
|
||||
| Parameters<Modifier<ModifyFnCacheData<FieldData>>>[1]['INVALIDATE'],
|
||||
options?: Partial<{
|
||||
debug: boolean
|
||||
}>
|
||||
) => {
|
||||
modifyObjectFields<Variables, FieldData>(
|
||||
cache,
|
||||
id,
|
||||
(field, variables, value, details) => {
|
||||
if (field !== fieldName) return
|
||||
return updater({ fieldName: field, variables, value, details })
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build skipLoggingErrors function that skips logging errors if there's only one error and it's related to a specific field
|
||||
*/
|
||||
@@ -548,3 +540,210 @@ export const skipLoggingErrorsIfOneFieldError =
|
||||
err.graphQLErrors.some((e) => intersection(e.path || [], fieldNames).length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
type NonUndefined<T> = T extends undefined ? never : T
|
||||
|
||||
/**
|
||||
* Update field at specific path in object, only if it exists. Useful for cache modification
|
||||
* when fields should only be updated if they exist.
|
||||
*/
|
||||
export const updatePathIfExists = <Value, Path extends Paths<Value> & string>(
|
||||
val: Value,
|
||||
path: Path,
|
||||
updater: (val: NonUndefined<Get<Value, Path>>) => NonUndefined<Get<Value, Path>>
|
||||
) => {
|
||||
if (!val) return val
|
||||
|
||||
if (has(val, path)) {
|
||||
const pathVal = get(val, path) as NonUndefined<Get<Value, Path>>
|
||||
const newVal = updater(pathVal)
|
||||
set(val, path, newVal)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from specific path in object, only if it exists.
|
||||
*/
|
||||
export const getFromPathIfExists = <Value, Path extends Paths<Value> & string>(
|
||||
val: MaybeNullOrUndefined<Value>,
|
||||
path: Path
|
||||
): Optional<Get<Value, Path>> => {
|
||||
if (!val) return undefined
|
||||
if (!has(val, path)) return undefined
|
||||
return get(val, path) as Get<Value, Path>
|
||||
}
|
||||
|
||||
type ModifyObjectFieldValue<
|
||||
Type extends keyof AllObjectTypes,
|
||||
Field extends keyof AllObjectTypes[Type]
|
||||
> = ModifyFnCacheData<AllObjectTypes[Type][Field]>
|
||||
|
||||
/**
|
||||
* Simplified & improved version of modifyObjectFields, just targetting a single field for a cache modification
|
||||
* @see modifyObjectFields
|
||||
*/
|
||||
export const modifyObjectField = <
|
||||
Type extends keyof AllObjectTypes,
|
||||
Field extends keyof AllObjectTypes[Type]
|
||||
>(
|
||||
cache: ApolloCache<unknown>,
|
||||
key: ApolloCacheObjectKey<Type>,
|
||||
fieldName: Field,
|
||||
updater: (params: {
|
||||
fieldName: string
|
||||
variables: Field extends keyof AllObjectFieldArgTypes[Type]
|
||||
? AllObjectFieldArgTypes[Type][Field]
|
||||
: never
|
||||
/**
|
||||
* Value found in the cache. Read-only and should not be mutated directly. Use the
|
||||
* createUpdatedValue() helper to build a new value with updated fields.
|
||||
*/
|
||||
value: ReadonlyDeep<ModifyObjectFieldValue<Type, Field>>
|
||||
helpers: {
|
||||
/**
|
||||
* Build new value with the values at specific paths updated with the provided updater functions,
|
||||
* ONLY if the paths exist in the cache.
|
||||
*
|
||||
* This function operates on a deeply cloned value that is safe to mutate
|
||||
*/
|
||||
createUpdatedValue: (
|
||||
updateHandler: (params: {
|
||||
/**
|
||||
* Invoke this function to update one specific path in the object
|
||||
*/
|
||||
update: <Path extends Paths<ModifyObjectFieldValue<Type, Field>> & string>(
|
||||
path: Path,
|
||||
pathUpdate: (
|
||||
val: NonUndefined<Get<ModifyObjectFieldValue<Type, Field>, Path>>
|
||||
) => NonUndefined<Get<ModifyObjectFieldValue<Type, Field>, Path>>
|
||||
) => MaybeNullOrUndefined<ModifyObjectFieldValue<Type, Field>>
|
||||
}) => void
|
||||
) => ModifyObjectFieldValue<Type, Field>
|
||||
/**
|
||||
* Get value from specific path, only if it exists in the cache value
|
||||
*/
|
||||
get: <Path extends Paths<ModifyObjectFieldValue<Type, Field>> & string>(
|
||||
path: Path
|
||||
) => Optional<Get<ModifyObjectFieldValue<Type, Field>, Path>>
|
||||
/**
|
||||
* Invoke and return this out from the modify call to evict the field from the cache
|
||||
*/
|
||||
evict: () => ModifierDetails['DELETE']
|
||||
/**
|
||||
* Read field data from a Reference object
|
||||
*/
|
||||
readField: <
|
||||
ReadFieldType extends keyof AllObjectTypes,
|
||||
ReadFieldName extends keyof AllObjectTypes[ReadFieldType] & string
|
||||
>(
|
||||
ref: CacheObjectReference<ReadFieldType>,
|
||||
fieldName: ReadFieldName
|
||||
) => Optional<AllObjectTypes[ReadFieldType][ReadFieldName]>
|
||||
/**
|
||||
* Build a reference object for a specific object in the cache
|
||||
*/
|
||||
ref: typeof getObjectReference
|
||||
}
|
||||
}) =>
|
||||
| Optional<ModifyObjectFieldValue<Type, Field>>
|
||||
| ReadonlyDeep<ModifyObjectFieldValue<Type, Field>>
|
||||
| ModifierDetails['DELETE']
|
||||
| ModifierDetails['INVALIDATE']
|
||||
| void,
|
||||
options?: Partial<{
|
||||
debug: boolean
|
||||
/**
|
||||
* Whether to auto evict values that have variables with common filters in them (e.g. a 'filter' or
|
||||
* 'search' prop). Often its better to evict filtered values, because we can't tell if the newly
|
||||
* added item should be included in the filtered list or not.
|
||||
*/
|
||||
autoEvictFiltered: boolean
|
||||
}>
|
||||
) => {
|
||||
const { autoEvictFiltered } = options || {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
modifyObjectFields<any, any>(
|
||||
cache,
|
||||
key,
|
||||
(field, variables, value, details) => {
|
||||
if (field !== fieldName) return
|
||||
|
||||
// Auto evict filtered values?
|
||||
if (autoEvictFiltered && isObjectLike(variables)) {
|
||||
const checkFilter = (filter: string) => {
|
||||
if (!has(variables, filter)) return false
|
||||
const val = get(variables, filter)
|
||||
|
||||
// True, if any primitive value (e.g. string, number) && arrays
|
||||
if (isNullOrUndefined(val)) return false
|
||||
if (isArray(val)) return true
|
||||
if (!isObjectLike(val)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const commonFilters = ['query', 'filter', 'search', 'filter.search']
|
||||
const hasFilter = commonFilters.some(checkFilter)
|
||||
|
||||
if (hasFilter) {
|
||||
return details.DELETE
|
||||
}
|
||||
}
|
||||
|
||||
// Build helpers & clone value to allow for direct mutation
|
||||
const createUpdatedValue = (
|
||||
updateHandler: (params: {
|
||||
update: <Path extends Paths<ModifyObjectFieldValue<Type, Field>> & string>(
|
||||
path: Path,
|
||||
pathUpdate: (
|
||||
val: NonUndefined<Get<ModifyObjectFieldValue<Type, Field>, Path>>
|
||||
) => NonUndefined<Get<ModifyObjectFieldValue<Type, Field>, Path>>
|
||||
) => MaybeNullOrUndefined<ModifyObjectFieldValue<Type, Field>>
|
||||
}) => void
|
||||
) => {
|
||||
let clonedValue = cloneDeep(value) as ModifyObjectFieldValue<Type, Field>
|
||||
updateHandler({
|
||||
update: (path, pathUpdate) => {
|
||||
clonedValue = updatePathIfExists(clonedValue, path, pathUpdate)
|
||||
return clonedValue
|
||||
}
|
||||
})
|
||||
return clonedValue
|
||||
}
|
||||
|
||||
const getIfExists = <
|
||||
Path extends Paths<ModifyObjectFieldValue<Type, Field>> & string
|
||||
>(
|
||||
path: Path
|
||||
) => getFromPathIfExists<ModifyObjectFieldValue<Type, Field>, Path>(value, path)
|
||||
const evict = () => details.DELETE
|
||||
const readField = <
|
||||
ReadFieldType extends keyof AllObjectTypes,
|
||||
ReadFieldName extends keyof AllObjectTypes[ReadFieldType] & string
|
||||
>(
|
||||
ref: CacheObjectReference<ReadFieldType>,
|
||||
fieldName: ReadFieldName
|
||||
) =>
|
||||
details.readField(
|
||||
fieldName,
|
||||
ref
|
||||
) as AllObjectTypes[ReadFieldType][ReadFieldName]
|
||||
|
||||
return updater({
|
||||
fieldName: field,
|
||||
variables,
|
||||
value,
|
||||
helpers: {
|
||||
createUpdatedValue,
|
||||
get: getIfExists,
|
||||
evict,
|
||||
readField,
|
||||
ref: getObjectReference
|
||||
}
|
||||
})
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ import { waitForever, type MaybeAsync, type Optional } from '@speckle/shared'
|
||||
import { useApolloClient, useMutation } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
Query,
|
||||
QueryWorkspaceArgs,
|
||||
QueryWorkspaceInviteArgs,
|
||||
User,
|
||||
UserWorkspacesArgs,
|
||||
UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragment,
|
||||
Workspace,
|
||||
WorkspaceCreateInput,
|
||||
@@ -33,7 +28,7 @@ import {
|
||||
processWorkspaceInviteMutation,
|
||||
workspaceUpdateRoleMutation
|
||||
} from '~/lib/workspaces/graphql/mutations'
|
||||
import { isFunction, isUndefined } from 'lodash-es'
|
||||
import { isFunction } from 'lodash-es'
|
||||
import type { GraphQLError } from 'graphql'
|
||||
import { useClipboard } from '~~/composables/browser'
|
||||
|
||||
@@ -143,33 +138,33 @@ export const useProcessWorkspaceInvite = () => {
|
||||
|
||||
if (accepted) {
|
||||
// Evict Query.workspace
|
||||
modifyObjectField<Query['workspace'], QueryWorkspaceArgs>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
ROOT_QUERY,
|
||||
'workspace',
|
||||
({ variables, details: { DELETE } }) => {
|
||||
if (variables.id === workspaceId) return DELETE
|
||||
({ variables, helpers: { evict } }) => {
|
||||
if (variables.id === workspaceId) return evict()
|
||||
}
|
||||
)
|
||||
|
||||
// Evict all User.workspaces
|
||||
modifyObjectField<User['workspaces'], UserWorkspacesArgs>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('User', userId),
|
||||
'workspaces',
|
||||
({ details: { DELETE } }) => DELETE
|
||||
({ helpers: { evict } }) => evict()
|
||||
)
|
||||
}
|
||||
|
||||
// Set Query.workspaceInvite(id) = null (no invite)
|
||||
modifyObjectField<Query['workspaceInvite'], QueryWorkspaceInviteArgs>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
ROOT_QUERY,
|
||||
'workspaceInvite',
|
||||
({ value, variables, details: { readField } }) => {
|
||||
({ value, variables, helpers: { readField } }) => {
|
||||
if (value) {
|
||||
const workspaceId = readField('workspaceId', value)
|
||||
if (workspaceId === workspaceId) return null
|
||||
const inviteWorkspaceId = readField(value, 'workspaceId')
|
||||
if (inviteWorkspaceId === workspaceId) return null
|
||||
} else {
|
||||
if (variables.workspaceId === workspaceId) return null
|
||||
}
|
||||
@@ -367,25 +362,18 @@ export function useCreateWorkspace() {
|
||||
if (!workspaceId) return
|
||||
// Navigation to workspace is gonna fetch everything needed for the page, so we only
|
||||
// really need to update workspace fields used in sidebar & settings: User.workspaces
|
||||
modifyObjectField<User['workspaces'], UserWorkspacesArgs>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('User', userId),
|
||||
'workspaces',
|
||||
({ variables, value, details: { DELETE } }) => {
|
||||
if (variables.filter?.search?.length) return DELETE // evict if filtered search
|
||||
|
||||
const totalCount = isUndefined(value?.totalCount)
|
||||
? undefined
|
||||
: value.totalCount + 1
|
||||
const items = isUndefined(value?.items)
|
||||
? undefined
|
||||
: [...value.items, getObjectReference('Workspace', workspaceId)]
|
||||
|
||||
return {
|
||||
...value,
|
||||
totalCount,
|
||||
items
|
||||
}
|
||||
({ helpers: { createUpdatedValue, ref } }) => {
|
||||
return createUpdatedValue(({ update }) => {
|
||||
update('totalCount', (totalCount) => totalCount + 1)
|
||||
update('items', (items) => [...items, ref('Workspace', workspaceId)])
|
||||
})
|
||||
},
|
||||
{
|
||||
autoEvictFiltered: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@
|
||||
"@eslint/config-inspector": "^0.4.10",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/client-preset": "^4.3.0",
|
||||
"@graphql-codegen/plugin-helpers": "^5.0.4",
|
||||
"@graphql-codegen/typescript": "^4.0.9",
|
||||
"@graphql-codegen/visitor-plugin-common": "5.3.1",
|
||||
"@nuxt/devtools": "^1.3.9",
|
||||
"@nuxt/eslint": "^0.3.13",
|
||||
"@nuxtjs/tailwindcss": "^6.3.0",
|
||||
@@ -137,7 +140,7 @@
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"type-fest": "^3.5.1",
|
||||
"type-fest": "^4.24.0",
|
||||
"typescript": "^4.8.3",
|
||||
"vue-tsc": "2.0.10",
|
||||
"wait-on": "^6.0.1",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const { reduce } = require('lodash')
|
||||
|
||||
const capitalize = (str) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
const formatTsTypeName = (gqlTypeName) => {
|
||||
// Not sure why it gets converted this way in parent types
|
||||
return gqlTypeName.replace('AI', 'Ai')
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin that adds some extra generated types and type mappings to support better Apollo Cache modification utilities
|
||||
* @type {import('@graphql-codegen/plugin-helpers').PluginFunction}
|
||||
*/
|
||||
const plugin = (schema) => {
|
||||
/** @type {Record<string, import('graphql').GraphQLNamedType>} */
|
||||
const objectTypeMap = reduce(
|
||||
schema.getTypeMap(),
|
||||
(acc, type, typeName) => {
|
||||
if (type.astNode?.kind === 'ObjectTypeDefinition') {
|
||||
acc[typeName] = type
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
let output = `export type AllObjectTypes = {\n`
|
||||
for (const [typeName] of Object.entries(objectTypeMap)) {
|
||||
output += ` ${typeName}: ${formatTsTypeName(typeName)},\n`
|
||||
}
|
||||
output += `}\n`
|
||||
|
||||
for (const [typeName, type] of Object.entries(objectTypeMap)) {
|
||||
const finalTypeName = formatTsTypeName(typeName)
|
||||
|
||||
output += `export type ${finalTypeName}FieldArgs = {\n`
|
||||
for (const [fieldName, fieldDef] of Object.entries(type.getFields())) {
|
||||
const argCount = fieldDef.args.length
|
||||
const argsName = formatTsTypeName(`${finalTypeName}${capitalize(fieldName)}Args`)
|
||||
output += ` ${fieldName}: ${argCount ? argsName : '{}'},\n`
|
||||
}
|
||||
output += `}\n`
|
||||
}
|
||||
|
||||
output += `export type AllObjectFieldArgTypes = {\n`
|
||||
for (const [typeName] of Object.entries(objectTypeMap)) {
|
||||
const finalTypeName = formatTsTypeName(typeName)
|
||||
output += ` ${typeName}: ${finalTypeName}FieldArgs,\n`
|
||||
}
|
||||
output += `}\n`
|
||||
|
||||
return `${output}\n`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plugin
|
||||
}
|
||||
@@ -10205,6 +10205,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-codegen/typescript@npm:^4.0.9":
|
||||
version: 4.0.9
|
||||
resolution: "@graphql-codegen/typescript@npm:4.0.9"
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers": "npm:^5.0.4"
|
||||
"@graphql-codegen/schema-ast": "npm:^4.0.2"
|
||||
"@graphql-codegen/visitor-plugin-common": "npm:5.3.1"
|
||||
auto-bind: "npm:~4.0.0"
|
||||
tslib: "npm:~2.6.0"
|
||||
peerDependencies:
|
||||
graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
checksum: 10/304026adfe622530b8a2827569dd5bbd390177051be8c214fb79873ec64ef21793635c91657703bfd229a3d06f1a8a6f1addd8ae7eab20d1eff2efe6fb044df7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-codegen/visitor-plugin-common@npm:5.2.0, @graphql-codegen/visitor-plugin-common@npm:^5.0.0, @graphql-codegen/visitor-plugin-common@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.2.0"
|
||||
@@ -10225,6 +10240,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-codegen/visitor-plugin-common@npm:5.3.1":
|
||||
version: 5.3.1
|
||||
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.3.1"
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers": "npm:^5.0.4"
|
||||
"@graphql-tools/optimize": "npm:^2.0.0"
|
||||
"@graphql-tools/relay-operation-optimizer": "npm:^7.0.0"
|
||||
"@graphql-tools/utils": "npm:^10.0.0"
|
||||
auto-bind: "npm:~4.0.0"
|
||||
change-case-all: "npm:1.0.15"
|
||||
dependency-graph: "npm:^0.11.0"
|
||||
graphql-tag: "npm:^2.11.0"
|
||||
parse-filepath: "npm:^1.0.2"
|
||||
tslib: "npm:~2.6.0"
|
||||
peerDependencies:
|
||||
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
checksum: 10/6dd0464d9099d5aeabeb766515fc8dd2fc84bcae4cb0e3653d7f38aea716d6622d35d7cbb57a1954e6bc1cde10f4dd8c4a75ceb4e8bb8cdbba16219615666a5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-tools/apollo-engine-loader@npm:^8.0.0":
|
||||
version: 8.0.1
|
||||
resolution: "@graphql-tools/apollo-engine-loader@npm:8.0.1"
|
||||
@@ -14991,6 +15026,9 @@ __metadata:
|
||||
"@eslint/config-inspector": "npm:^0.4.10"
|
||||
"@graphql-codegen/cli": "npm:^5.0.2"
|
||||
"@graphql-codegen/client-preset": "npm:^4.3.0"
|
||||
"@graphql-codegen/plugin-helpers": "npm:^5.0.4"
|
||||
"@graphql-codegen/typescript": "npm:^4.0.9"
|
||||
"@graphql-codegen/visitor-plugin-common": "npm:5.3.1"
|
||||
"@headlessui/vue": "npm:^1.7.13"
|
||||
"@heroicons/vue": "npm:^2.0.12"
|
||||
"@jsonforms/core": "npm:^3.3.0"
|
||||
@@ -15087,7 +15125,7 @@ __metadata:
|
||||
tailwindcss: "npm:^3.4.1"
|
||||
tweetnacl-sealedbox-js: "npm:^1.2.0"
|
||||
tweetnacl-util: "npm:^0.15.1"
|
||||
type-fest: "npm:^3.5.1"
|
||||
type-fest: "npm:^4.24.0"
|
||||
typescript: "npm:^4.8.3"
|
||||
ua-parser-js: "npm:^1.0.38"
|
||||
vee-validate: "npm:^4.7.0"
|
||||
@@ -48527,6 +48565,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^4.24.0":
|
||||
version: 4.24.0
|
||||
resolution: "type-fest@npm:4.24.0"
|
||||
checksum: 10/60efd6ec71f5113ef0a0fcabe61fc722bb2520ea082bc23e4b4dfb44204234dc691560a5e837f939160d7c18b410ed8fae32ddb752d57bed009248e0f61dce6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:~1.6.18":
|
||||
version: 1.6.18
|
||||
resolution: "type-is@npm:1.6.18"
|
||||
|
||||
Reference in New Issue
Block a user