Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
fix: improve queue handling of optimistic ids + InputMapper (#381)
Browse files Browse the repository at this point in the history
* fix: use traverse to replace all instances of an optimistic id with resulting id

* fix: run tests serially

* fix: use inputMapper for optimistic ui and conflicts

* fix: input mapper has a serialize and deserialize function

* fix: export input mapper explicitly

* doc: add note about inputMapper

Co-authored-by: Wojtek Trocki <wtrocki@redhat.com>
  • Loading branch information
darahayes and wtrocki committed Mar 6, 2020
1 parent 6d736d8 commit 7ffae63
Show file tree
Hide file tree
Showing 20 changed files with 351 additions and 67 deletions.
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@
// "console": "integratedTerminal",
// "internalConsoleOptions": "neverOpen",
"port": 9229
},
{
"name": "Debug Jest Tests in offix-cache",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/packages/offix-cache",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/packages/offix-cache/node_modules/.bin/jest",
"--runInBand"
],
// "console": "integratedTerminal",
// "internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
46 changes: 46 additions & 0 deletions docs/ref-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,52 @@ The storage can be swapped depending on the platform. For example `window.locals

The options to configure how failed offline mutations are retried. See [`apollo-link-retry`](https://www.apollographql.com/docs/link/links/retry/).

#### `inputMapper`

If your mutation variables are not passed directly, for example if you use input types, an `inputMapper` is a set of functions that tells Offix how to read the mutation `variables`.

For example, if your mutations use Input types:

```js
const CREATE_TASK = gql`
mutation createTask($input: TaskInput!) {
createTask(input: $input) {
id
title
description
}
}`

client.offlineMutate({
mutation: CREATE_TASK,
variables: {
input: {
title: 'new task title',
description: 'new task description'
}
},
returnType: 'Task'
})
```

`ApolloOfflineClient` will need an additional `inputMapper` object with the following functions:

* `deserialize` - to know how to convert the `variables` object into a flat object that can be used to generate optimistic responses and cache update functions.
* `serialize` - to know how to convert the serialized object back into the correct `variables` object after performing conflict resolution.

```js
import { ApolloOfflineClient, createDefaultCacheStorage } from "offix-client";

const client = new ApolloOfflineClient({
cache: new InMemoryCache(),
link: new HttpLink({ uri: "http://example.com/graphql" }),
inputMapper: {
deserialize: (variables) => { return variables.input },
serialize: (variables) => { return { input: variables } }
}
});
```

## offix-client-boost

### `createClient`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"main": "index.js",
"scripts": {
"analyze": "source-map-explorer packages/*/dist/*.js packages/*/dist/*/*.js --onlyMapped",
"test": "jest",
"test": "jest -w 1",
"coverage": "jest --coverage",
"cleanInstall": "lerna exec yarn install --ignore-scripts",
"bootstrap": "lerna bootstrap --no-ci",
Expand Down
19 changes: 17 additions & 2 deletions packages/offix-cache/src/createMutationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { createOptimisticResponse } from "./createOptimisticResponse";
import { CacheUpdatesQuery } from "./api/CacheUpdates";
import { getOperationFieldName, deconstructQuery } from "./utils/helperFunctions";

export interface InputMapper {
deserialize: (object: any) => any;
serialize: (object: any) => any;
}

/**
* Interface to overlay helper internals on top of mutation options.
*/
Expand Down Expand Up @@ -33,6 +38,14 @@ export interface MutationHelperOptions<T = {
* For example for `modifyObject(value: String!): Object` value will be `Object`
*/
returnType?: string;

/**
* [Modifier]
*
* Maps input objects for the cases if variables are not passed to the root
*
*/
inputMapper?: InputMapper;
}

/**
Expand Down Expand Up @@ -87,7 +100,8 @@ export const createMutationOptions = <T = {
returnType,
operationType = CacheOperation.ADD,
idField = "id",
context
context,
inputMapper
} = options;

if (returnType && !options.optimisticResponse) {
Expand All @@ -96,7 +110,8 @@ export const createMutationOptions = <T = {
variables,
returnType,
operationType,
idField
idField,
inputMapper
});
}

Expand Down
11 changes: 10 additions & 1 deletion packages/offix-cache/src/createOptimisticResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CacheOperation } from "./api/CacheOperation";
import { generateClientId } from "./utils";
import { DocumentNode } from "graphql";
import { OperationVariables } from "apollo-client";
import { InputMapper } from "./createMutationOptions";

// export type OptimisticOptions = Omit<MutationHelperOptions, keyof MutationOptions |"updateQuery" | "context">;

Expand All @@ -12,6 +13,7 @@ export interface OptimisticOptions {
returnType: string;
idField?: string;
variables?: OperationVariables;
inputMapper?: InputMapper;
}

/**
Expand Down Expand Up @@ -45,13 +47,20 @@ export const createOptimisticResponse = (options: OptimisticOptions) => {
idField = "id",
operationType
} = options;

const optimisticResponse: any = {
__typename: "Mutation"
};

let mappedVariables = variables;

if (options.inputMapper) {
mappedVariables = options.inputMapper.deserialize(variables);
}

optimisticResponse[operation] = {
__typename: returnType,
...variables,
...mappedVariables,
optimisticResponse: true
};
if (operationType === CacheOperation.ADD) {
Expand Down
4 changes: 2 additions & 2 deletions packages/offix-cache/src/utils/helperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { CacheUpdatesQuery, QueryWithVariables } from "../api/CacheUpdates";
import { CLIENT_ID_PREFIX } from "./constants";

// Returns true if ID was generated on client
export const isClientGeneratedId = (id: string) => {
return id && id.startsWith(CLIENT_ID_PREFIX);
export const isClientGeneratedId = (id: string): boolean => {
return typeof id === "string" && id.startsWith(CLIENT_ID_PREFIX);
};

// Helper method for ID generation ()
Expand Down
27 changes: 27 additions & 0 deletions packages/offix-cache/test/OptimisticResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ test("check that createNewOptimisticResponse is without id", () => {
const result = createOptimisticResponse(options);
expect(result.createItem.id).toBe(undefined);
});

test("createOptimisticResponse works with an input mapper", () => {
const options: OptimisticOptions = {
mutation: CREATE_ITEM,
operationType: CacheOperation.REFRESH,
returnType: "Test",
variables: {
a: "val1",
b: "val2",
input: {
id: "123",
name: "test"
}
},
inputMapper: {
deserialize: (vars) => vars.input,
serialize: (vars) => vars
}
};
const result = createOptimisticResponse(options);
expect(result.createItem).toStrictEqual({
__typename: "Test",
id: "123",
name: "test",
optimisticResponse: true
});
});
2 changes: 1 addition & 1 deletion packages/offix-client/integration_test/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ module.exports = function(config) {

// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
singleRun: process.env.DEBUG ? false : true,

// Concurrency level
// how many browser should be started simultaneous
Expand Down
4 changes: 3 additions & 1 deletion packages/offix-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"typescript": "3.7.5"
},
"dependencies": {
"@types/traverse": "0.6.32",
"apollo-cache-inmemory": "1.6.5",
"apollo-cache-persist": "0.1.1",
"apollo-client": "2.6.8",
Expand All @@ -48,7 +49,8 @@
"offix-cache": "0.13.2",
"offix-conflicts-client": "0.13.2",
"offix-offline": "0.13.2",
"offix-scheduler": "0.13.2"
"offix-scheduler": "0.13.2",
"traverse": "0.6.6"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
Expand Down
11 changes: 8 additions & 3 deletions packages/offix-client/src/ApolloOfflineClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ApolloClient, { MutationOptions, OperationVariables } from "apollo-client
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { OffixScheduler } from "offix-scheduler";
import { CachePersistor } from "apollo-cache-persist";
import { MutationHelperOptions, CacheUpdates, createMutationOptions } from "offix-cache";
import { MutationHelperOptions, CacheUpdates, createMutationOptions } from "offix-cache";
import { FetchResult } from "apollo-link";
import {
ApolloOperationSerializer,
Expand All @@ -19,7 +19,7 @@ import {
} from "./apollo";
import { NetworkStatus } from "offix-offline";
import { ObjectState } from "offix-conflicts-client";
import { ApolloOfflineClientOptions } from "./config/ApolloOfflineClientOptions";
import { ApolloOfflineClientOptions, InputMapper } from "./config/ApolloOfflineClientOptions";
import { ApolloOfflineClientConfig } from "./config/ApolloOfflineClientConfig";

export class ApolloOfflineClient extends ApolloClient<NormalizedCacheObject> {
Expand All @@ -40,6 +40,8 @@ export class ApolloOfflineClient extends ApolloClient<NormalizedCacheObject> {
public mutationCacheUpdates?: CacheUpdates;
// true after client is initialized
public initialized: boolean;
// mapper function for mapping mutation variables
public inputMapper?: InputMapper;

constructor(options: ApolloOfflineClientOptions) {
const config = new ApolloOfflineClientConfig(options);
Expand All @@ -48,6 +50,7 @@ export class ApolloOfflineClient extends ApolloClient<NormalizedCacheObject> {
this.initialized = false;
this.mutationCacheUpdates = config.mutationCacheUpdates;
this.conflictProvider = config.conflictProvider;
this.inputMapper = config.inputMapper;

if (config.cachePersistor) {
if (!(config.cachePersistor instanceof CachePersistor)) {
Expand Down Expand Up @@ -133,12 +136,14 @@ export class ApolloOfflineClient extends ApolloClient<NormalizedCacheObject> {

protected createOfflineMutationOptions<T = any, TVariables = OperationVariables>(
options: MutationHelperOptions<T, TVariables>): MutationOptions<T, TVariables> {
options.inputMapper = this.inputMapper;
const offlineMutationOptions = createMutationOptions<T, TVariables>(options);

offlineMutationOptions.context.conflictBase = getBaseStateFromCache(
this.cache as unknown as ApolloCacheWithData,
this.conflictProvider,
offlineMutationOptions as unknown as MutationOptions
offlineMutationOptions as unknown as MutationOptions,
this.inputMapper
);

if (!offlineMutationOptions.update && this.mutationCacheUpdates) {
Expand Down
3 changes: 2 additions & 1 deletion packages/offix-client/src/apollo/LinksBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ function createDefaultLink(config: ApolloOfflineClientConfig) {
const conflictLink = new ConflictLink({
conflictProvider: config.conflictProvider as ObjectState,
conflictListener: config.conflictListener,
conflictStrategy: config.conflictStrategy
conflictStrategy: config.conflictStrategy,
inputMapper: config.inputMapper
});

const retryLink = ApolloLink.split(isMarkedOffline, new RetryLink(config.retryOptions));
Expand Down
15 changes: 12 additions & 3 deletions packages/offix-client/src/apollo/conflicts/ConflictLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ConflictHandler
} from "offix-conflicts-client";
import { isMutation } from "../helpers";
import { InputMapper } from "../../config/ApolloOfflineClientOptions";

/**
* Represents conflict information that was returned from server
Expand Down Expand Up @@ -42,6 +43,14 @@ export interface ConflictConfig {
* The conflict resolution strategy your client should use. By default it takes client version.
*/
conflictStrategy?: ConflictResolutionStrategy;

/**
* [Modifier]
*
* Maps input objects for the cases if variables are not passed to the root
*
*/
inputMapper?: InputMapper;
}

/**
Expand All @@ -67,10 +76,10 @@ export class ConflictLink extends ApolloLink {
forward: NextLink
): Observable<FetchResult> | null {
if (isMutation(operation)) {
if (this.stater.currentState(operation.variables) !== undefined) {
const variables = this.config.inputMapper ? this.config.inputMapper.deserialize(operation.variables) : operation.variables;
if (this.stater.currentState(variables) !== undefined) {
return this.link.request(operation, forward);
}
return forward(operation);
}
return forward(operation);
}
Expand All @@ -94,7 +103,7 @@ export class ConflictLink extends ApolloLink {
});
const resolvedConflict = conflictHandler.executeStrategy();
if (resolvedConflict) {
operation.variables = resolvedConflict;
operation.variables = this.config.inputMapper ? this.config.inputMapper.serialize(resolvedConflict): resolvedConflict;
}
}
return forward(operation);
Expand Down
21 changes: 14 additions & 7 deletions packages/offix-client/src/apollo/conflicts/baseHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { MutationOptions } from "apollo-client";
import { MutationOptions, OperationVariables } from "apollo-client";
import { ApolloCache } from "apollo-cache";
import {
ObjectState,
LocalConflictError,
ConflictResolutionData
} from "offix-conflicts-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { InputMapper } from "../../config/ApolloOfflineClientOptions";

/**
* Convenience interface that specifies a few extra properties found on ApolloCache
Expand All @@ -31,15 +32,21 @@ export interface ApolloCacheWithData extends ApolloCache<NormalizedCacheObject>
export function getBaseStateFromCache(
cache: ApolloCacheWithData,
objectState: ObjectState,
mutationOptions: MutationOptions
mutationOptions: MutationOptions,
inputMapper?: InputMapper
): ConflictResolutionData {
const context = mutationOptions.context;

if (!context.conflictBase) {
// do nothing
const conflictBase = getObjectFromCache(cache, context.returnType, mutationOptions);

let mutationVariables = mutationOptions.variables as OperationVariables;
if (inputMapper) {
mutationVariables = inputMapper.deserialize(mutationVariables);
}

const conflictBase = getObjectFromCache(cache, context.returnType, mutationVariables);
if (conflictBase && Object.keys(conflictBase).length !== 0) {
if (objectState.hasConflict(mutationOptions.variables, conflictBase)) {
if (objectState.hasConflict(mutationVariables, conflictBase)) {
// 🙊 Input data is conflicted with the latest server projection
throw new LocalConflictError(conflictBase, mutationOptions.variables);
}
Expand All @@ -48,9 +55,9 @@ export function getBaseStateFromCache(
}
}

function getObjectFromCache(cache: ApolloCacheWithData, typename: string, mutationOptions: MutationOptions) {
function getObjectFromCache(cache: ApolloCacheWithData, typename: string, mutationVariables: OperationVariables) {
if (cache && cache.data) {
const idKey = cache.config.dataIdFromObject({ __typename: typename, ...mutationOptions.variables });
const idKey = cache.config.dataIdFromObject({ __typename: typename, ...mutationVariables });

if (cache.optimisticData && cache.optimisticData.parent) {
const optimisticData = cache.optimisticData.parent.data;
Expand Down

0 comments on commit 7ffae63

Please sign in to comment.