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

Commit

Permalink
Add setOnMissingRelation for Query and Mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuprog committed Mar 10, 2022
1 parent 48572dc commit f2882f0
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 53 deletions.
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,10 @@ When we request entities from the server, we often want to apply transformations

* transform a date string into a `Temporal` object;
* transform a foreign key to the corresponding object;
* mark an entity as to be deleted;
* transform the structure of the fetched data;
* etc.

There are two ways to transform incoming data:

1. by adding a transformer function to a specific query or mutation instance (via the `setTransformer` function).

2. by adding global transformer functions as config to the store (via the `setConfig` function).

In the example below, every incoming object with typename "Article" will have its `publishDate` data (received as a string from the server) converted to a `PlainDateTime` object.
You may add a `transformers` key in the config object. The `transformers` prop holds an object like below:

```javascript
import { store } from 'graphql-light';
Expand All @@ -212,6 +206,8 @@ store.setConfig({
});
```

In the example above, every incoming object with typename `"Article"` will have its `publishDate` (received as a string from the server) converted into a `PlainDateTime` object.

Another nice thing to do, is convert the foreign keys into objects, to allow chaining properties as if data is denormalized:

```javascript
Expand All @@ -227,19 +223,31 @@ store.setConfig({
references: {
authorId: {
type: 'Author',
field: 'author',
async handleMissing(authorId) {
await someQuery.query({},
{ fetchStrategy: FetchStrategy.NETWORK_ONLY });
}
field: 'author'
}
}
}
}
});
```

This also allows to avoid fetching what has been previously fetched. If the authors have been previously fetched, we now just specify that the authorId on an Article points to an Author. In the result of the query, a field author is added alongside authorId.
This allows to avoid fetching what has been previously fetched. If the authors have been previously fetched, we now just specify that the authorId on an Article points to an Author. In the result of the query, a field `author` is added alongside the field `authorId`.

It is assumed that the Author with id `authorId` has already been stored in the cache by a previous query. If that is not the case, you may add a callback to the query to handle missing references:

```javascript
const query = new Query(client, `...`);

const onMissingRelation = async (propName, propValue, _object, _variables, _data) => {
switch (propName) {
case 'authorId':
await otherQuery.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
break;
}
};
```

You may also use the `setTransformer` function on a query to change the fetched data before processing it.

### Delete and update entities

Expand Down Expand Up @@ -554,6 +562,8 @@ store.setConfig({ debug: false });
#### `setOptions(callback)`
#### `setOnMissingRelation(callback)`
#### `setResolver(resolver)`
#### `setTransformer(transformer)`
Expand All @@ -578,6 +588,8 @@ store.setConfig({ debug: false });

#### `setOnFetchEntity(onFetchEntity)`

#### `setOnMissingRelation(callback)`

#### `setTransformer(transformer)`

### NetworkRequest
Expand Down
13 changes: 11 additions & 2 deletions src/mutation/Mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class Mutation {
this.transformer = data => data;
this.onFetchEntity = () => undefined;
this.onFetchArrayOfEntities = () => undefined;
this.onMissingRelation = () => undefined;
}

setTransformer(transformer) {
Expand All @@ -22,6 +23,10 @@ export default class Mutation {
this.onFetchArrayOfEntities = onFetchArrayOfEntities;
}

setOnMissingRelation(onMissingRelation) {
this.onMissingRelation = onMissingRelation;
}

async mutate(variables, callback = _ => true) {
let data = await this.client.request(this.queryDocument, variables || {});

Expand All @@ -32,10 +37,14 @@ export default class Mutation {
if (transformedData) {
const onFetchEntity =
(entity) => this.onFetchEntity(entity, variables, data);

const onFetchArrayOfEntities =
(propName, object) => this.setOnFetchArrayOfEntities(propName, object, variables, data);
(propName, object) => this.onFetchArrayOfEntities(propName, object, variables, data);

const onMissingRelation =
(propName, propValue, object) => this.query.onMissingRelation(propName, propValue, object, variables, data);

await store.store(transformedData, { onFetchEntity, onFetchArrayOfEntities });
await store.store(transformedData, { onFetchEntity, onFetchArrayOfEntities, onMissingRelation });
}

return data;
Expand Down
21 changes: 13 additions & 8 deletions src/query/Query.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class Query extends AbstractQuery {
this.transformer = data => data;
this.onFetchEntity = () => undefined;
this.onFetchArrayOfEntities = () => undefined;
this.onMissingRelation = () => undefined;
this.onStoreUpdate = () => undefined;
this.queriesForVars = {};
this.getOnUnobservedStrategy = _variables => OnUnobservedStrategy.PAUSE_UPDATING;
Expand All @@ -24,14 +25,6 @@ export default class Query extends AbstractQuery {
this.dependentQueries = queries;
}

setResolver(resolver) {
this.customResolver = resolver;
}

setTransformer(transformer) {
this.transformer = transformer;
}

setOnFetchEntity(onFetchEntity) {
this.onFetchEntity = onFetchEntity;
}
Expand All @@ -40,6 +33,10 @@ export default class Query extends AbstractQuery {
this.onFetchArrayOfEntities = onFetchArrayOfEntities;
}

setOnMissingRelation(onMissingRelation) {
this.onMissingRelation = onMissingRelation;
}

setOnStoreUpdate(onStoreUpdate) {
this.onStoreUpdate = onStoreUpdate;
}
Expand All @@ -52,6 +49,14 @@ export default class Query extends AbstractQuery {
this.getOptions = callback;
}

setResolver(resolver) {
this.customResolver = resolver;
}

setTransformer(transformer) {
this.transformer = transformer;
}

getQueryForVars(variables) {
const stringifiedVars = JSON.stringify(variables);
let queryForVars = this.queriesForVars[stringifiedVars];
Expand Down
5 changes: 4 additions & 1 deletion src/query/QueryForVars.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ export default class QueryForVars extends AbstractQueryForVars {
const onFetchArrayOfEntities =
(propName, object) => this.query.onFetchArrayOfEntities(propName, object, this.variables, data);

const { updatesToListenTo, denormalizedData } = await store.store(data, { onFetchEntity, onFetchArrayOfEntities });
const onMissingRelation =
(propName, propValue, object) => this.query.onMissingRelation(propName, propValue, object, this.variables, data);

const { updatesToListenTo, denormalizedData } = await store.store(data, { onFetchEntity, onFetchArrayOfEntities, onMissingRelation });
this.updatesToListenTo = updatesToListenTo;

this.strategy.setFetchedData(denormalizedData);
Expand Down
2 changes: 1 addition & 1 deletion src/store/Store.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class Store {
} =
await pipeAsync(
// pipefy(transformServerData, this),
pipefy(proxifyReferences, this),
pipefy(proxifyReferences, this, callbacks),
pipefy(normalize, this, callbacks),
pipefy(updateLinks, this),
pipefy(refreshDenormalizedData, this),
Expand Down
2 changes: 1 addition & 1 deletion src/store/middleware/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function doNormalize(store, object, getObjectFromStore, callbacks, newEntities,
} else if (isArray(propValue)) {
const array = propValue; // renaming for readability

let onFetchArray = callbacks?.onFetchArrayOfEntities?.(propName, object);
const onFetchArray = callbacks?.onFetchArrayOfEntities?.(propName, object);

if (isArrayOfEntities(array) || onFetchArray) {
array.forEach(entity => {
Expand Down
24 changes: 17 additions & 7 deletions src/store/middleware/proxifyReferences.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { hasObjectProps, isArray, isObjectLiteral } from '../../utils';
import createProxy from '../createProxy';

export default async function proxifyReferences(result, store) {
export default async function proxifyReferences(result, store, callbacks = {}) {
if (!store.config.transformers) {
return result;
}

let { denormalizedData } = result;

denormalizedData = await doProxifyReferences(denormalizedData, null, store);
denormalizedData = await doProxifyReferences(denormalizedData, null, store, callbacks);

return { ...result, denormalizedData };
}

async function doProxifyReferences(data, entity, store) {
async function doProxifyReferences(data, entity, store, callbacks) {
if (isObjectLiteral(data)) {
let object = { ...data };

Expand All @@ -34,7 +34,12 @@ async function doProxifyReferences(data, entity, store) {
if (isArray(propValue)) {
// if we do not specify the __typename, we assume the entity has been previously stored
if (propValue.length > 0 && !propValue[0].__typename) {
const { type, ensureHasFields, handleMissing } = linkData;
const { type, ensureHasFields } = linkData;

let handleMissing = linkData.handleMissing;
if (callbacks?.onMissingRelation) {
handleMissing = (value, object) => callbacks?.onMissingRelation?.(propName, value, object);
}

const incompleteEntities =
propValue
Expand Down Expand Up @@ -77,7 +82,12 @@ async function doProxifyReferences(data, entity, store) {
object[propName] = propValue.map(entity => createProxy(entity, store.getEntityById.bind(store)));
}
} else {
const { type, field, ensureHasFields, handleMissing } = linkData;
const { type, field, ensureHasFields } = linkData;

let handleMissing = linkData.handleMissing;
if (callbacks?.onMissingRelation) {
handleMissing = (value, object) => callbacks?.onMissingRelation?.(propName, value, object);
}

// we have only the reference (e.g. we have `userId` field and no `user` field)
if (!object[field]) {
Expand Down Expand Up @@ -109,7 +119,7 @@ async function doProxifyReferences(data, entity, store) {
}
}
} else {
object[propName] = await doProxifyReferences(propValue, entity, store);
object[propName] = await doProxifyReferences(propValue, entity, store, callbacks);
}
}

Expand All @@ -118,7 +128,7 @@ async function doProxifyReferences(data, entity, store) {

if (isArray(data)) {
let array = [...data];
array = await Promise.all(array.map(element => doProxifyReferences(element, entity, store)));
array = await Promise.all(array.map(element => doProxifyReferences(element, entity, store, callbacks)));

return array;
}
Expand Down
38 changes: 19 additions & 19 deletions src/store/middleware/proxifyReferences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ test('proxify references', async () => {
addressId: {
type: 'Address',
field: 'address',
async handleMissing(_addressId, _object) {
await query.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
}
}
}
}
Expand Down Expand Up @@ -58,13 +55,15 @@ test('proxify references', async () => {

test('missing reference', async () => {
store.initialize();

const onMissingRelation = (_propName, _propValue, _object, _variables, _data) => {};

store.setConfig({ transformers: {
Person: {
references: {
addressId: {
type: 'Address',
field: 'address',
handleMissing() {}
field: 'address'
}
}
}
Expand All @@ -82,7 +81,7 @@ test('missing reference', async () => {
}
});

const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store);
const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store, { onMissingRelation });

expect(transformedData.contacts.dummy.address).toBeNull();
expect(transformedData.contacts.dummy.addressId).toBeNull();
Expand Down Expand Up @@ -130,12 +129,13 @@ test('missing reference in array', async () => {
street: 'Foo'
});

const onMissingRelation = (_propName, _propValue, _object, _variables, _data) => {};

store.setConfig({ transformers: {
Person: {
references: {
addresses: {
type: 'Address',
handleMissing() {}
type: 'Address'
}
}
}
Expand All @@ -152,7 +152,7 @@ test('missing reference in array', async () => {
}
});

const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store);
const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store, { onMissingRelation });

expect(transformedData.contacts.dummy.addresses.length).toBe(1);
expect(transformedData.contacts.dummy.addresses[0].id).toBe('address2');
Expand Down Expand Up @@ -213,16 +213,16 @@ test('fetch missing reference', async () => {
}
};
const query = new Query(client, null);
const onMissingRelation = async (_propName, _propValue, _object, _variables, _data) => {
await query.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
};

store.setConfig({ transformers: {
Person: {
references: {
addressId: {
type: 'Address',
field: 'address',
async handleMissing(_addressId, _object) {
await query.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
}
field: 'address'
}
}
}
Expand All @@ -239,7 +239,7 @@ test('fetch missing reference', async () => {
}
});

const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store);
const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store, { onMissingRelation });

expect(transformedData.contacts.dummy.addressId).toBe('address1');
expect(person.contacts.dummy.addressId).toBe('address1');
Expand Down Expand Up @@ -273,15 +273,15 @@ test('fetch missing reference in array', async () => {
}
};
const query = new Query(client, null);
const onMissingRelation = async (_propName, _propValue, _object, _variables, _data) => {
await query.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
};

store.setConfig({ transformers: {
Person: {
references: {
addresses: {
type: 'Address',
async handleMissing(_addressId, _object) {
await query.query({}, { fetchStrategy: FetchStrategy.NETWORK_ONLY });
}
type: 'Address'
}
}
}
Expand All @@ -298,7 +298,7 @@ test('fetch missing reference in array', async () => {
}
});

const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store);
const { denormalizedData: transformedData } = await proxifyReferences({ denormalizedData: person }, store, { onMissingRelation });

expect(transformedData.contacts.dummy.addresses.length).toBe(2);
expect(transformedData.contacts.dummy.addresses[0].id).toBe('address1');
Expand Down

0 comments on commit f2882f0

Please sign in to comment.