Skip to content

Commit

Permalink
Allow calling readField with ReadFieldOptions. (#6306)
Browse files Browse the repository at this point in the history
I decided to split these changes out from PR #6288 after all, per my
comment #6288 (comment).
  • Loading branch information
benjamn authored May 18, 2020
1 parent 0652299 commit 7429d9d
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 88 deletions.
74 changes: 74 additions & 0 deletions src/cache/inmemory/__tests__/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3145,6 +3145,80 @@ describe("type policies", function () {
);
});

it("readField can read fields with arguments", function () {
const enum Style { UPPER, LOWER, TITLE };

const cache = new InMemoryCache({
typePolicies: {
Word: {
keyFields: ["text"],

fields: {
style(_, { args, readField }) {
const text = readField<string>("text");
switch (args?.style) {
case Style.UPPER: return text?.toUpperCase();
case Style.LOWER: return text?.toLowerCase();
case Style.TITLE:
return text && (
text.charAt(0).toUpperCase() +
text.slice(1).toLowerCase()
);
}
},
upperCase(_, { readField }) {
return readField<string>({
fieldName: "style",
args: { style: Style.UPPER },
});
},
lowerCase(_, { readField }) {
return readField<string>({
fieldName: "style",
args: { style: Style.LOWER },
});
},
titleCase(_, { readField }) {
return readField<string>({
fieldName: "style",
args: { style: Style.TITLE },
});
},
},
},
},
});

cache.writeQuery({
query: gql`query { wordOfTheDay { text } }`,
data: {
wordOfTheDay: {
__typename: "Word",
text: "inveigle",
},
},
});

expect(cache.readQuery({
query: gql`
query {
wordOfTheDay {
upperCase
lowerCase
titleCase
}
}
`,
})).toEqual({
wordOfTheDay: {
__typename: "Word",
upperCase: "INVEIGLE",
lowerCase: "inveigle",
titleCase: "Inveigle",
},
});
});

it("can return existing object from merge function (issue #6245)", function () {
const cache = new InMemoryCache({
typePolicies: {
Expand Down
17 changes: 11 additions & 6 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,17 @@ export abstract class EntityStore implements NormalizedCache {
fieldName?: string,
args?: Record<string, any>,
) {
const storeFieldName = fieldName && args
? this.policies.getStoreFieldName(dataId, fieldName, args)
: fieldName;
return this.modify(dataId, storeFieldName ? {
[storeFieldName]: delModifier,
} : delModifier);
const storeObject = this.lookup(dataId);
if (storeObject) {
const typename = this.getFieldValue<string>(storeObject, "__typename");
const storeFieldName = fieldName && args
? this.policies.getStoreFieldName({ typename, fieldName, args })
: fieldName;
return this.modify(dataId, storeFieldName ? {
[storeFieldName]: delModifier,
} : delModifier);
}
return false;
}

public evict(
Expand Down
143 changes: 77 additions & 66 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ export type FieldPolicy<

type StorageType = Record<string, any>;

interface FieldSpecifier {
typename?: string;
fieldName: string;
field?: FieldNode;
args?: Record<string, any>;
variables?: Record<string, any>;
}

function argsFromFieldSpecifier(spec: FieldSpecifier) {
return spec.args !== void 0 ? spec.args :
spec.field ? argumentsObjectFromField(spec.field, spec.variables) : null;
}

export interface ReadFieldOptions extends FieldSpecifier {
from?: StoreObject | Reference;
}

export interface FieldFunctionOptions<
TArgs = Record<string, any>,
TVars = Record<string, any>,
Expand Down Expand Up @@ -133,9 +150,10 @@ export interface FieldFunctionOptions<
// to compute the argument values. Note that this function will invoke
// custom read functions for other fields, if defined. Always returns
// immutable data (enforced with Object.freeze in development).
readField<T = StoreValue>(options: ReadFieldOptions): SafeReadonly<T> | undefined;
readField<T = StoreValue>(
nameOrField: string | FieldNode,
foreignObjOrRef?: StoreObject | Reference,
fieldName: string,
from?: StoreObject | Reference,
): SafeReadonly<T> | undefined;

// A handy place to put field-specific data that you want to survive
Expand Down Expand Up @@ -436,30 +454,19 @@ export class Policies {
return false;
}

public getStoreFieldName(
typename: string | undefined,
nameOrField: string | FieldNode,
// If nameOrField is a string, argsOrVars should be an object of
// arguments. If nameOrField is a FieldNode, argsOrVars should be the
// variables to use when computing the arguments of the field.
argsOrVars: Record<string, any>,
): string {
let field: FieldNode | null;
let fieldName: string;
if (typeof nameOrField === "string") {
field = null;
fieldName = nameOrField;
} else {
field = nameOrField;
fieldName = field.name.value;
}
public getStoreFieldName(fieldSpec: FieldSpecifier): string {
const { typename, fieldName } = fieldSpec;
const policy = this.getFieldPolicy(typename, fieldName, false);
let storeFieldName: string | undefined;

let keyFn = policy && policy.keyFn;
if (keyFn && typename) {
const args = field ? argumentsObjectFromField(field, argsOrVars) : argsOrVars;
const context: Parameters<KeyArgsFunction>[1] = { typename, fieldName, field };
const context: Parameters<KeyArgsFunction>[1] = {
typename,
fieldName,
field: fieldSpec.field || null,
};
const args = argsFromFieldSpecifier(fieldSpec);
while (keyFn) {
const specifierOrString = keyFn(args, context);
if (Array.isArray(specifierOrString)) {
Expand All @@ -474,9 +481,9 @@ export class Policies {
}

if (storeFieldName === void 0) {
storeFieldName = field
? storeKeyNameFromField(field, argsOrVars)
: getStoreKeyName(fieldName, argsOrVars);
storeFieldName = fieldSpec.field
? storeKeyNameFromField(fieldSpec.field, fieldSpec.variables)
: getStoreKeyName(fieldName, argsFromFieldSpecifier(fieldSpec));
}

// Make sure custom field names start with the actual field.name.value
Expand All @@ -490,37 +497,39 @@ export class Policies {
private storageTrie = new KeyTrie<StorageType>(true);

public readField<V = StoreValue>(
objectOrReference: StoreObject | Reference,
nameOrField: string | FieldNode,
options: ReadFieldOptions,
context: ReadMergeContext,
typename = context.getFieldValue<string>(objectOrReference, "__typename"),
): SafeReadonly<V> {
invariant(
objectOrReference,
"Must provide an object or Reference when calling Policies#readField",
);

const policies = this;
const storeFieldName = typeof nameOrField === "string" ? nameOrField
: policies.getStoreFieldName(typename, nameOrField, context.variables);
): SafeReadonly<V> | undefined {
const objectOrReference = options.from;
if (!objectOrReference) return;

const nameOrField = options.field || options.fieldName;
if (!nameOrField) return;

if (options.typename === void 0) {
const typename = context.getFieldValue<string>(
objectOrReference, "__typename");
if (typename) options.typename = typename;
}

const storeFieldName = this.getStoreFieldName(options);
const fieldName = fieldNameFromStoreName(storeFieldName);
const existing = context.getFieldValue<V>(objectOrReference, storeFieldName);
const policy = policies.getFieldPolicy(typename, fieldName, false);
const policy = this.getFieldPolicy(options.typename, fieldName, false);
const read = policy && policy.read;

if (read) {
const storage = policies.storageTrie.lookup(
const storage = this.storageTrie.lookup(
isReference(objectOrReference)
? objectOrReference.__ref
: objectOrReference,
storeFieldName,
);

return read(existing, makeFieldFunctionOptions(
policies,
typename,
this,
objectOrReference,
nameOrField,
options,
storage,
context,
)) as SafeReadonly<V>;
Expand All @@ -543,14 +552,12 @@ export class Policies {
context: ReadMergeContext,
storageKeys?: [string | StoreObject, string],
): T {
const policies = this;

if (isFieldValueToBeMerged(incoming)) {
const field = incoming.__field;
const fieldName = field.name.value;
// This policy and its merge function are guaranteed to exist
// because the incoming value is a FieldValueToBeMerged object.
const { merge } = policies.getFieldPolicy(
const { merge } = this.getFieldPolicy(
incoming.__typename, fieldName, false)!;

// If storage ends up null, that just means no options.storage object
Expand All @@ -561,12 +568,11 @@ export class Policies {
// this comment then you probably have good reasons for wanting to know
// esoteric details like these, you wizard, you.
const storage = storageKeys
? policies.storageTrie.lookupArray(storageKeys)
? this.storageTrie.lookupArray(storageKeys)
: null;

incoming = merge!(existing, incoming.__value, makeFieldFunctionOptions(
policies,
incoming.__typename,
this,
// Unlike options.readField for read functions, we do not fall
// back to the current object if no foreignObjOrRef is provided,
// because it's not clear what the current object should be for
Expand All @@ -578,15 +584,18 @@ export class Policies {
// to the order in which fields are written into the cache.
// However, readField(name, ref) is useful for merge functions
// that need to deduplicate child objects and references.
null,
field,
void 0,
{ typename: incoming.__typename,
fieldName,
field,
variables: context.variables },
storage,
context,
)) as T;
}

if (Array.isArray(incoming)) {
return incoming!.map(item => policies.applyMerges(
return incoming!.map(item => this.applyMerges(
// Items in the same position in different arrays are not
// necessarily related to each other, so there is no basis for
// merging them. Passing void here means any FieldValueToBeMerged
Expand Down Expand Up @@ -619,7 +628,7 @@ export class Policies {

Object.keys(i).forEach(storeFieldName => {
const incomingValue = i[storeFieldName];
const appliedValue = policies.applyMerges(
const appliedValue = this.applyMerges(
context.getFieldValue(e, storeFieldName),
incomingValue,
context,
Expand Down Expand Up @@ -653,20 +662,17 @@ export interface ReadMergeContext {

function makeFieldFunctionOptions(
policies: Policies,
typename: string,
objectOrReference: StoreObject | Reference | null,
nameOrField: string | FieldNode,
objectOrReference: StoreObject | Reference | undefined,
fieldSpec: FieldSpecifier,
storage: StorageType | null,
context: ReadMergeContext,
): FieldFunctionOptions {
const { toReference, getFieldValue, variables } = context;
const storeFieldName = typeof nameOrField === "string" ? nameOrField :
policies.getStoreFieldName(typename, nameOrField, variables);
const storeFieldName = policies.getStoreFieldName(fieldSpec);
const fieldName = fieldNameFromStoreName(storeFieldName);
return {
args: typeof nameOrField === "string" ? null :
argumentsObjectFromField(nameOrField, variables),
field: typeof nameOrField === "string" ? null : nameOrField,
args: argsFromFieldSpecifier(fieldSpec),
field: fieldSpec.field || null,
fieldName,
storeFieldName,
variables,
Expand All @@ -675,14 +681,19 @@ function makeFieldFunctionOptions(
storage,

readField<T>(
nameOrField: string | FieldNode,
foreignObjOrRef: StoreObject | Reference,
fieldNameOrOptions: string | ReadFieldOptions,
from?: StoreObject | Reference,
) {
return policies.readField<T>(
foreignObjOrRef || objectOrReference,
nameOrField,
context,
);
const options: ReadFieldOptions =
typeof fieldNameOrOptions === "string" ? {
fieldName: fieldNameOrOptions,
from,
} : fieldNameOrOptions;

return policies.readField<T>(options.from ? options : {
...options,
from: objectOrReference,
}, context);
},

mergeObjects(existing, incoming) {
Expand Down
13 changes: 6 additions & 7 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,12 @@ export class StoreReader {
if (!shouldInclude(selection, variables)) return;

if (isField(selection)) {
let fieldValue = policies.readField(
objectOrReference,
selection,
// Since ExecContext extends ReadMergeContext, we can pass it
// here without any modifications.
context,
);
let fieldValue = policies.readField({
fieldName: selection.name.value,
field: selection,
variables: context.variables,
from: objectOrReference,
}, context);

const resultName = resultKeyNameFromField(selection);
context.path.push(resultName);
Expand Down
Loading

0 comments on commit 7429d9d

Please sign in to comment.