Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow calling readField with ReadFieldOptions. #6306

Merged
merged 1 commit into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
Comment on lines -439 to -445
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new FieldSpecifier type was important to clean up this awkward/subtle dependency between nameOrField and argsOrVars.

): 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