Skip to content

Commit

Permalink
feat(graphcache): add logger interface (#3444)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Dec 2, 2023
1 parent d6e435c commit 1fee86f
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-files-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Add optional `logger` to the options, this allows you to filter out warnings or disable them all together
1 change: 1 addition & 0 deletions docs/api/graphcache.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ options and returns an [`Exchange`](./core.md#exchange).
| `optimistic` | A mapping of mutation fields to resolvers that may be used to provide _Graphcache_ with an optimistic result for a given mutation field that should be applied to the cached data temporarily. |
| `schema` | A serialized GraphQL schema that is used by _Graphcache_ to resolve partial data, interfaces, and enums. The schema also used to provide helpful warnings for [schema awareness](../graphcache/schema-awareness.md). |
| `storage` | A persisted storage interface that may be provided to preserve cache data for [offline support](../graphcache/offline.md). |
| `logger` | A function that will be invoked for warning/debug/... logs |

The `@urql/exchange-graphcache` package also exports the `offlineExchange`; which is identical to
the `cacheExchange` but activates [offline support](../graphcache/offline.md) when the `storage` option is passed.
Expand Down
15 changes: 10 additions & 5 deletions exchanges/graphcache/src/ast/schemaPredicates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ describe('SchemaPredicates', () => {

it('should indicate nullability', () => {
expect(
SchemaPredicates.isFieldNullable(schema, 'Todo', 'text')
SchemaPredicates.isFieldNullable(schema, 'Todo', 'text', undefined)
).toBeFalsy();
expect(
SchemaPredicates.isFieldNullable(schema, 'Todo', 'complete')
SchemaPredicates.isFieldNullable(schema, 'Todo', 'complete', undefined)
).toBeTruthy();
expect(
SchemaPredicates.isFieldNullable(schema, 'Todo', 'author')
SchemaPredicates.isFieldNullable(schema, 'Todo', 'author', undefined)
).toBeTruthy();
});

Expand All @@ -89,15 +89,20 @@ describe('SchemaPredicates', () => {

it('should throw if a requested type does not exist', () => {
expect(() =>
SchemaPredicates.isFieldNullable(schema, 'SomeInvalidType', 'complete')
SchemaPredicates.isFieldNullable(
schema,
'SomeInvalidType',
'complete',
undefined
)
).toThrow(
'The type `SomeInvalidType` is not an object in the defined schema, but the GraphQL document is traversing it.\nhttps://bit.ly/2XbVrpR#3'
);
});

it('should warn in console if a requested field does not exist', () => {
expect(
SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof')
SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof', undefined)
).toBeFalsy();

expect(console.warn).toBeCalledTimes(1);
Expand Down
68 changes: 43 additions & 25 deletions exchanges/graphcache/src/ast/schemaPredicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,28 @@ import type {
UpdatesConfig,
ResolverConfig,
OptimisticMutationConfig,
Logger,
} from '../types';

const BUILTIN_NAME = '__';

export const isFieldNullable = (
schema: SchemaIntrospector,
typename: string,
fieldName: string
fieldName: string,
logger: Logger | undefined
): boolean => {
const field = getField(schema, typename, fieldName);
const field = getField(schema, typename, fieldName, logger);
return !!field && field.type.kind !== 'NON_NULL';
};

export const isListNullable = (
schema: SchemaIntrospector,
typename: string,
fieldName: string
fieldName: string,
logger: Logger | undefined
): boolean => {
const field = getField(schema, typename, fieldName);
const field = getField(schema, typename, fieldName, logger);
if (!field) return false;
const ofType =
field.type.kind === 'NON_NULL' ? field.type.ofType : field.type;
Expand All @@ -40,11 +43,12 @@ export const isListNullable = (
export const isFieldAvailableOnType = (
schema: SchemaIntrospector,
typename: string,
fieldName: string
fieldName: string,
logger: Logger | undefined
): boolean =>
fieldName.indexOf(BUILTIN_NAME) === 0 ||
typename.indexOf(BUILTIN_NAME) === 0 ||
!!getField(schema, typename, fieldName);
!!getField(schema, typename, fieldName, logger);

export const isInterfaceOfType = (
schema: SchemaIntrospector,
Expand All @@ -70,7 +74,8 @@ export const isInterfaceOfType = (
const getField = (
schema: SchemaIntrospector,
typename: string,
fieldName: string
fieldName: string,
logger: Logger | undefined
) => {
if (
fieldName.indexOf(BUILTIN_NAME) === 0 ||
Expand All @@ -90,7 +95,8 @@ const getField = (
'`, ' +
'but the GraphQL document expects it to exist.\n' +
'Traversal will continue, however this may lead to undefined behavior!',
4
4,
logger
);
}

Expand Down Expand Up @@ -124,7 +130,8 @@ function expectAbstractType(schema: SchemaIntrospector, typename: string) {

export function expectValidKeyingConfig(
schema: SchemaIntrospector,
keys: KeyingConfig
keys: KeyingConfig,
logger: Logger | undefined
): void {
if (process.env.NODE_ENV !== 'production') {
for (const key in keys) {
Expand All @@ -133,7 +140,8 @@ export function expectValidKeyingConfig(
'Invalid Object type: The type `' +
key +
'` is not an object in the defined schema, but the `keys` option is referencing it.',
20
20,
logger
);
}
}
Expand All @@ -142,7 +150,8 @@ export function expectValidKeyingConfig(

export function expectValidUpdatesConfig(
schema: SchemaIntrospector,
updates: UpdatesConfig
updates: UpdatesConfig,
logger: Logger | undefined
): void {
if (process.env.NODE_ENV === 'production') {
return;
Expand Down Expand Up @@ -175,7 +184,8 @@ export function expectValidUpdatesConfig(
typename +
'` is not an object in the defined schema, but the `updates` config is referencing it.' +
addition,
21
21,
logger
);
}

Expand All @@ -188,35 +198,40 @@ export function expectValidUpdatesConfig(
'` on `' +
typename +
'` is not in the defined schema, but the `updates` config is referencing it.',
22
22,
logger
);
}
}
}
}

function warnAboutResolver(name: string): void {
function warnAboutResolver(name: string, logger: Logger | undefined): void {
warn(
`Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`,
23
23,
logger
);
}

function warnAboutAbstractResolver(
name: string,
kind: 'UNION' | 'INTERFACE'
kind: 'UNION' | 'INTERFACE',
logger: Logger | undefined
): void {
warn(
`Invalid resolver: \`${name}\` does not match to a concrete type in the schema, but the \`resolvers\` option is referencing it. Implement the resolver for the types that ${
kind === 'UNION' ? 'make up the union' : 'implement the interface'
} instead.`,
26
26,
logger
);
}

export function expectValidResolversConfig(
schema: SchemaIntrospector,
resolvers: ResolverConfig
resolvers: ResolverConfig,
logger: Logger | undefined
): void {
if (process.env.NODE_ENV === 'production') {
return;
Expand All @@ -230,30 +245,31 @@ export function expectValidResolversConfig(
).fields();
for (const resolverQuery in resolvers.Query || {}) {
if (!validQueries[resolverQuery]) {
warnAboutResolver('Query.' + resolverQuery);
warnAboutResolver('Query.' + resolverQuery, logger);
}
}
} else {
warnAboutResolver('Query');
warnAboutResolver('Query', logger);
}
} else {
if (!schema.types!.has(key)) {
warnAboutResolver(key);
warnAboutResolver(key, logger);
} else if (
schema.types!.get(key)!.kind === 'INTERFACE' ||
schema.types!.get(key)!.kind === 'UNION'
) {
warnAboutAbstractResolver(
key,
schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION'
schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION',
logger
);
} else {
const validTypeProperties = (
schema.types!.get(key) as SchemaObject
).fields();
for (const resolverProperty in resolvers[key] || {}) {
if (!validTypeProperties[resolverProperty]) {
warnAboutResolver(key + '.' + resolverProperty);
warnAboutResolver(key + '.' + resolverProperty, logger);
}
}
}
Expand All @@ -263,7 +279,8 @@ export function expectValidResolversConfig(

export function expectValidOptimisticMutationsConfig(
schema: SchemaIntrospector,
optimisticMutations: OptimisticMutationConfig
optimisticMutations: OptimisticMutationConfig,
logger: Logger | undefined
): void {
if (process.env.NODE_ENV === 'production') {
return;
Expand All @@ -277,7 +294,8 @@ export function expectValidOptimisticMutationsConfig(
if (!validMutations[mutation]) {
warn(
`Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`,
24
24,
logger
);
}
}
Expand Down
13 changes: 11 additions & 2 deletions exchanges/graphcache/src/helpers/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ExecutableDefinitionNode,
InlineFragmentNode,
} from '@0no-co/graphql.web';
import type { Logger } from '../types';
import { Kind } from '@0no-co/graphql.web';

export type ErrorCode =
Expand Down Expand Up @@ -89,9 +90,17 @@ export function invariant(
}
}

export function warn(message: string, code: ErrorCode) {
export function warn(
message: string,
code: ErrorCode,
logger: Logger | undefined
) {
if (!cache.has(message)) {
console.warn(message + getDebugOutput() + helpUrl + code);
if (logger) {
logger('warn', message + getDebugOutput() + helpUrl + code);
} else {
console.warn(message + getDebugOutput() + helpUrl + code);
}
cache.add(message);
}
}
Loading

0 comments on commit 1fee86f

Please sign in to comment.