Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@urql/exchange-graphcache

Package Overview
Dependencies
Maintainers
3
Versions
298
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@urql/exchange-graphcache - npm Package Compare versions

Comparing version 0.1.0-alpha.5 to 1.0.0-beta.0

dist/types/ast/traversal.d.ts

23

dist/types/ast/index.d.ts

@@ -1,22 +0,3 @@

export * from './types';
export { getFieldArguments, normalizeVariables } from './variables';
export * from './traversal';
export * from './node';
import { DocumentNode, FieldNode, FragmentDefinitionNode, InlineFragmentNode, OperationDefinitionNode, SelectionNode } from 'graphql';
import { Fragments, VarsMap } from './types';
/** Checks whether a SelectionNode is a FieldNode */
export declare const isFieldNode: (node: SelectionNode) => node is FieldNode;
/** Checks whether a SelectionNode is an InlineFragmentNode */
export declare const isInlineFragment: (node: SelectionNode) => node is InlineFragmentNode;
/** Returns the main operation's definition */
export declare const getMainOperation: (doc: DocumentNode) => void | OperationDefinitionNode;
/** Returns the first fragment definition */
export declare const getMainFragment: (doc: DocumentNode) => void | FragmentDefinitionNode;
/** Returns a normalized form of variables with defaulted values */
export declare const getNormalizedVars: (node: OperationDefinitionNode, input?: object | null | undefined) => VarsMap;
/** Returns a mapping from fragment names to their selections */
export declare const getFragments: (doc: DocumentNode) => Fragments;
/** Returns either the field's name or the field's alias */
export declare const getFieldAlias: (node: FieldNode) => string;
/** Evaluates a fields arguments taking vars into account */
export declare const getFieldArguments: (node: FieldNode, vars: VarsMap) => VarsMap | null;
/** Checks whether a given SelectionNode should be ignored based on @skip or @include directives */
export declare const shouldInclude: (node: SelectionNode, vars: VarsMap) => boolean;

@@ -1,3 +0,2 @@

import { DefinitionNode, FragmentDefinitionNode, NamedTypeNode, NameNode, OperationDefinitionNode, SelectionSetNode } from 'graphql';
import { OperationType } from './types';
import { NamedTypeNode, NameNode, SelectionNode, SelectionSetNode, InlineFragmentNode, FieldNode, OperationDefinitionNode, FragmentDefinitionNode } from 'graphql';
/** Returns the name of a given node */

@@ -7,11 +6,14 @@ export declare const getName: (node: {

}) => string;
export declare const getOperationName: (node: OperationDefinitionNode) => "Query" | "Mutation" | "Subscription";
export declare const getFragmentTypeName: (node: FragmentDefinitionNode) => string;
/** Returns either the field's name or the field's alias */
export declare const getFieldAlias: (node: FieldNode) => string;
/** Returns the SelectionSet for a given inline or defined fragment node */
export declare const getSelectionSet: (node: {
selectionSet?: SelectionSetNode | undefined;
}) => ReadonlyArray<import("graphql").SelectionNode>;
}) => readonly SelectionNode[];
export declare const getTypeCondition: ({ typeCondition, }: {
typeCondition?: NamedTypeNode | undefined;
}) => string | null;
export declare const isOperationNode: (node: DefinitionNode) => node is OperationDefinitionNode;
export declare const isFragmentNode: (node: DefinitionNode) => node is FragmentDefinitionNode;
export declare const getOperationType: (node: OperationDefinitionNode) => OperationType;
export declare const isFieldNode: (node: SelectionNode) => node is FieldNode;
export declare const isInlineFragment: (node: SelectionNode) => node is InlineFragmentNode;
import { Exchange } from 'urql';
import { SerializedStore } from './store';
import { UpdatesConfig, ResolverConfig, OptimisticMutationConfig } from './types';
export interface CacheExchangeOpts {
initial?: SerializedStore;
updates?: UpdatesConfig;
resolvers?: ResolverConfig;
optimistic?: OptimisticMutationConfig;
}
export declare const cacheExchange: (opts: CacheExchangeOpts) => Exchange;
export * from './keys';

@@ -1,6 +0,5 @@

import { VarsMap } from '../ast';
import { SystemFields } from '../types';
import { Variables, SystemFields } from '../types';
export declare const isOperation: (typeName: string) => boolean;
export declare const keyOfEntity: (entity: SystemFields) => string | null;
export declare const keyOfField: (fieldName: string, args: VarsMap | null) => string;
export declare const keyOfField: (fieldName: string, args?: Variables | null | undefined) => string;
export declare const joinKeys: (parentKey: string, key: string) => string;
export * from './operations';
export * from './types';
export { create, serialize } from './store';
export { Store } from './store';
export { cacheExchange } from './exchange';
export { query } from './query';
export { write } from './write';
export { gc } from './gc';
export { write, writeOptimistic, writeFragment } from './write';

@@ -0,4 +1,9 @@

import { Data, Completeness, OperationRequest } from '../types';
import { Store } from '../store';
import { Request, Result } from './types';
export interface QueryResult {
completeness: Completeness;
dependencies: Set<string>;
data: null | Data;
}
/** Reads a request entirely from the store */
export declare const query: (store: Store, request: Request) => Result;
export declare const query: (store: Store, request: OperationRequest) => QueryResult;
import { FieldNode } from 'graphql';
import { Fragments, Variables } from '../types';
import { Store } from '../store';
import { Context, Request } from './types';
export declare const makeContext: (store: Store, request: Request) => void | Context;
export declare const forEachFieldNode: (ctx: Context, select: ReadonlyArray<import("graphql").SelectionNode>, cb: (node: FieldNode) => void) => void;
interface Context {
store: Store;
variables: Variables;
fragments: Fragments;
}
export declare const forEachFieldNode: (typename: string, entityKey: string, select: readonly import("graphql").SelectionNode[], ctx: Context, cb: (node: FieldNode) => void) => void;
export {};

@@ -0,7 +1,10 @@

import { Data, OperationRequest } from '../types';
import { Store } from '../store';
import { Data, Request, Result } from './types';
import { DocumentNode } from 'graphql';
export interface WriteResult {
touched: string[];
dependencies: Set<string>;
}
/** Writes a request given its response to the store */
export declare const write: (store: Store, request: Request, data: Data) => Result;
export declare const write: (store: Store, request: OperationRequest, data: Data) => WriteResult;
export declare const writeOptimistic: (store: Store, request: OperationRequest, optimisticKey: number) => WriteResult;
export declare const writeFragment: (store: Store, query: DocumentNode, data: Data) => void;

@@ -1,23 +0,29 @@

import { Entity, Link, Links, Records } from '../types';
export interface Store {
records: Records;
links: Links;
import { DocumentNode } from 'graphql';
import * as Pessimism from 'pessimism';
import { EntityField, Link, ResolverConfig, DataField, SystemFields, Variables, Data, UpdatesConfig, OptimisticMutationConfig } from '../types';
export declare const initStoreState: (optimisticKey: number | null) => void;
export declare const clearStoreState: () => void;
export declare const getCurrentDependencies: () => Set<string>;
export declare const addDependency: (dependency: string) => void;
export declare class Store {
records: Pessimism.Map<EntityField>;
links: Pessimism.Map<Link>;
resolvers: ResolverConfig;
updates: UpdatesConfig;
optimisticMutations: OptimisticMutationConfig;
constructor(resolvers?: ResolverConfig, updates?: UpdatesConfig, optimisticMutations?: OptimisticMutationConfig);
clearOptimistic(optimisticKey: number): void;
getRecord(fieldKey: string): EntityField;
removeRecord(fieldKey: string): Pessimism.Map<EntityField>;
writeRecord(field: EntityField, fieldKey: string): Pessimism.Map<EntityField>;
getField(entityKey: string, fieldName: string, args?: Variables): EntityField;
writeField(field: EntityField, entityKey: string, fieldName: string, args?: Variables): Pessimism.Map<EntityField>;
getLink(key: string): undefined | Link;
removeLink(key: string): Pessimism.Map<Link<string>>;
writeLink(link: Link, key: string): Pessimism.Map<Link<string>>;
resolveValueOrLink(fieldKey: string): DataField;
resolve(entity: SystemFields, field: string, args?: Variables): DataField;
hasField(key: string): boolean;
updateQuery(dataQuery: DocumentNode, updater: (data: Data | null) => Data): void;
writeFragment(dataFragment: DocumentNode, data: Data): void;
}
export interface SerializedStore {
records: {
[link: string]: Entity;
};
links: {
[link: string]: Link;
};
}
/** Creates a new Store with an optional initial, serialized state */
export declare const create: (initial?: SerializedStore | undefined) => Store;
/** Serializes a given Store to a plain JSON structure */
export declare const serialize: (store: Store) => SerializedStore;
export declare const find: (store: Store, key: string) => Entity | null;
export declare const findOrCreate: (store: Store, key: string) => Entity;
export declare const remove: (store: Store, key: string) => void;
export declare const setLink: (store: Store, key: string, link: Link) => void;
export declare const removeLink: (store: Store, key: string) => void;
export declare const readLink: (store: Store, key: string) => string | void | (string | null)[] | null;

@@ -1,6 +0,14 @@

/** A scalar is any fieldValue without a type. It can also include lists of scalars and embedded objects, which are simply represented as empty object type. */
export declare type Scalar = {} | string | number | null;
export declare type NullPrototype = {
[K in keyof ObjectConstructor]: never;
};
import { DocumentNode, FragmentDefinitionNode, SelectionNode } from 'graphql';
import { Store } from './store';
export declare type NullArray<T> = Array<null | T>;
export declare type SelectionSet = ReadonlyArray<SelectionNode>;
export interface Fragments {
[fragmentName: string]: void | FragmentDefinitionNode;
}
export declare type Primitive = null | number | boolean | string;
export interface ScalarObject {
__typename?: never;
[key: string]: any;
}
export declare type Scalar = Primitive | ScalarObject;
export interface SystemFields {

@@ -11,13 +19,35 @@ __typename: string;

}
export interface EntityFields {
[fieldName: string]: Scalar;
export declare type EntityField = undefined | Scalar | Scalar[];
export declare type DataField = Scalar | Scalar[] | Data | NullArray<Data>;
export interface DataFields {
[fieldName: string]: DataField;
}
/** Every Entity must have a typeName. It might have some ID fields of which `id` and `_id` are recognised by default. Every other fieldValue is a scalar. */
export declare type Entity = NullPrototype & SystemFields & EntityFields;
/** A link is a key or array of keys referencing other entities in the Records Map. It may be or contain `null`. */
export declare type Link = null | string | Array<string | null>;
/** A link can be resolved into the entities it points to. The resulting structure is a ResolvedLink */
export declare type ResolvedLink = null | Entity | Array<Entity | null>;
export declare type Records = Map<string, Entity>;
export declare type Links = Map<string, Link>;
export declare type Embedded = Map<string, Scalar>;
export declare type Data = SystemFields & DataFields;
export declare type Link<Key = string> = null | Key | NullArray<Key>;
export declare type ResolvedLink = Link<Data>;
export interface Variables {
[name: string]: Scalar | Scalar[] | Variables | NullArray<Variables>;
}
export interface OperationRequest {
query: DocumentNode;
variables?: object;
}
export interface ResolveInfo {
fragments: Fragments;
variables: Variables;
}
export declare type Resolver = (parent: Data, args: Variables, cache: Store, info: ResolveInfo) => DataField;
export interface ResolverConfig {
[typeName: string]: {
[fieldName: string]: Resolver;
};
}
export declare type UpdateResolver<T = any> = (result: T, args: Variables, cache: Store, info: ResolveInfo) => void;
export interface UpdatesConfig {
[mutationFieldName: string]: UpdateResolver;
}
export declare type OptimisticMutationResolver = (vars: Variables, cache: Store, info: ResolveInfo) => null | Data | NullArray<Data>;
export interface OptimisticMutationConfig {
[mutationFieldName: string]: OptimisticMutationResolver;
}
export declare type Completeness = 'EMPTY' | 'PARTIAL' | 'FULL';
{
"name": "@urql/exchange-graphcache",
"version": "0.1.0-alpha.5",
"description": "A normalizing GraphQL cache configurable by defining exceptions to the rule",
"main": "dist/graphcache.js",
"module": "dist/graphcache.es.js",
"version": "1.0.0-beta.0",
"description": "A normalized and configurable cache exchange for urql",
"repository": "https://github.com/kitten/urql-exchange-graphcache",
"bugs": {
"url": "https://github.com/FormidableLabs/urql-exchange-graphcache/issues"
},
"homepage": "https://github.com/FormidableLabs/urql-exchange-graphcache",
"main": "dist/urql-exchange-graphcache.js",
"module": "dist/urql-exchange-graphcache.es.js",
"types": "dist/types/index.d.ts",

@@ -11,17 +16,13 @@ "source": "src/index.ts",

"scripts": {
"prebuild": "rimraf dist",
"build": "run-p build:types build:bundle",
"build:clean": "rimraf dist",
"build:types": "tsc -d --emitDeclarationOnly --outDir dist/types",
"build:bundle": "microbundle --format es,cjs --no-compress",
"build:prune": "rimraf dist/types/**/*.test.d.ts",
"postbuild:bundle": "terser dist/graphcache.es.js -o dist/graphcache.es.min.js",
"clean": "rimraf ./dist ./node_modules/.cache",
"build": "rollup -c rollup.config.js",
"watch": "rollup -w -c rollup.config.js",
"check": "tsc --noEmit",
"test": "jest",
"bundlesize": "bundlesize",
"lint": "tslint --project .",
"check-formatting": "prettier --write src/**/*.{ts,tsx}",
"prepublishOnly": "run-s build build:prune"
"coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx",
"prepublishOnly": "run-s clean test build",
"codecov": "codecov"
},
"author": "Phil Plückthun <phil@kitten.sh>",
"repository": "https://github.com/kitten/graphcache.git",
"author": "Formidable",
"license": "MIT",

@@ -60,3 +61,3 @@ "prettier": {

"*.{ts,tsx}": [
"tslint --fix",
"eslint --fix",
"prettier --write",

@@ -71,44 +72,62 @@ "git add"

},
"bundlesize": [
{
"path": "./dist/*.min.js",
"maxSize": "10 kB"
}
],
"dependencies": {
"fast-json-stable-stringify": "^2.0.0",
"wonka": "^2.0.1"
"pessimism": "^1.0.1",
"wonka": "^3.2.0"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0",
"urql": "^1.0.4"
"urql": ">= 1.1.3"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-object-assign": "^7.2.0",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@testing-library/react": "^8.0.7",
"@types/enzyme": "3.10.3",
"@types/fast-json-stable-stringify": "^2.0.0",
"@types/graphql": "^14.0.7",
"@types/jest": "^23.3.13",
"@types/react": "^16.8.5",
"@types/react-dom": "^16.8.2",
"bundlesize": "^0.17.0",
"coveralls": "^3.0.0",
"graphql": "^14.1.1",
"@types/graphql": "^14.2.3",
"@types/jest": "^24.0.17",
"@types/react": "16.8.24",
"@types/react-test-renderer": "16.8.3",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-plugin-closure-elimination": "^1.3.0",
"babel-plugin-transform-async-to-promises": "^0.8.14",
"codecov": "^3.5.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.4.0",
"eslint": "^6.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.6.1",
"graphql": "^14.4.2",
"graphql-tag": "^2.10.1",
"husky": "^1.2.0",
"jest": "^23.6.0",
"lint-staged": "^8.1.0",
"microbundle": "^0.9.0",
"husky": "^3.0.2",
"jest": "^24.8.0",
"lint-staged": "^9.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.15.2",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"prettier": "^1.17.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-hooks-testing-library": "^0.6.0",
"react-is": "^16.8.6",
"react-ssr-prepass": "^1.0.6",
"react-test-renderer": "^16.8.6",
"rimraf": "^2.6.2",
"terser": "^3.16.1",
"ts-jest": "^23.10.5",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.16.0",
"tslint-react": "^3.6.0",
"typescript": "^3.1.6",
"urql": "^1.0.4"
"rollup": "^1.19.3",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-buble": "^0.19.8",
"rollup-plugin-commonjs": "^10.0.2",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.1.1",
"rollup-plugin-typescript2": "^0.22.1",
"terser": "^4.1.3",
"ts-jest": "^24.0.2",
"typescript": "^3.5.3",
"urql": "^1.3.0"
}
}

@@ -1,126 +0,3 @@

export * from './types';
export { getFieldArguments, normalizeVariables } from './variables';
export * from './traversal';
export * from './node';
import {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
InlineFragmentNode,
OperationDefinitionNode,
SelectionNode,
} from 'graphql';
import { getName, isFragmentNode, isOperationNode } from './node';
import { Fragments, VarsMap } from './types';
import { evaluateValueNode } from './value';
/** Checks whether a SelectionNode is a FieldNode */
export const isFieldNode = (node: SelectionNode): node is FieldNode =>
node.kind === 'Field';
/** Checks whether a SelectionNode is an InlineFragmentNode */
export const isInlineFragment = (
node: SelectionNode
): node is InlineFragmentNode => node.kind === 'InlineFragment';
/** Returns the main operation's definition */
export const getMainOperation = (
doc: DocumentNode
): OperationDefinitionNode | void => {
return doc.definitions.find(isOperationNode) as OperationDefinitionNode;
};
/** Returns the first fragment definition */
export const getMainFragment = (
doc: DocumentNode
): FragmentDefinitionNode | void => {
return doc.definitions.find(isFragmentNode) as FragmentDefinitionNode;
};
/** Returns a normalized form of variables with defaulted values */
export const getNormalizedVars = (
node: OperationDefinitionNode,
input?: null | object
): VarsMap => {
if (node.variableDefinitions === undefined) {
return {};
}
const args: VarsMap = input ? (input as VarsMap) : {};
return node.variableDefinitions.reduce((vars, def) => {
const name = getName(def.variable);
let value = args[name];
if (value === undefined) {
if (def.defaultValue !== undefined) {
value = evaluateValueNode(def.defaultValue, args);
} else {
return vars;
}
}
vars[name] = value;
return vars;
}, {});
};
/** Returns a mapping from fragment names to their selections */
export const getFragments = (doc: DocumentNode): Fragments =>
doc.definitions.filter(isFragmentNode).reduce((map: Fragments, node) => {
map[getName(node)] = node;
return map;
}, {});
/** Returns either the field's name or the field's alias */
export const getFieldAlias = (node: FieldNode): string =>
node.alias !== undefined ? node.alias.value : getName(node);
/** Evaluates a fields arguments taking vars into account */
export const getFieldArguments = (
node: FieldNode,
vars: VarsMap
): null | VarsMap => {
if (node.arguments === undefined || node.arguments.length === 0) {
return null;
}
return node.arguments.reduce((args, arg) => {
args[getName(arg)] = evaluateValueNode(arg.value, vars);
return args;
}, {});
};
/** Checks whether a given SelectionNode should be ignored based on @skip or @include directives */
export const shouldInclude = (node: SelectionNode, vars: VarsMap): boolean => {
if (node.directives === undefined) {
return true;
}
// Finds any @include or @skip directive that forces the node to be skipped
return !node.directives.some(directive => {
const name = getName(directive);
// Ignore other directives
const isInclude = name === 'include';
if (!isInclude && name !== 'skip') {
return false;
}
// Get the first argument and expect it to be named "if"
const firstArg =
directive.arguments !== undefined ? directive.arguments[0] : null;
if (firstArg === null) {
return false;
} else if (getName(firstArg) !== 'if') {
return false;
}
const value = evaluateValueNode(firstArg.value, vars);
if (typeof value !== 'boolean' && value !== null) {
return false;
}
// Return whether this directive forces us to skip
// `@include(if: false)` or `@skip(if: true)`
return isInclude ? !value : !!value;
});
};
import {
DefinitionNode,
FragmentDefinitionNode,
NamedTypeNode,
NameNode,
SelectionNode,
SelectionSetNode,
InlineFragmentNode,
FieldNode,
OperationDefinitionNode,
SelectionSetNode,
FragmentDefinitionNode,
Kind,
} from 'graphql';
import { OperationType, SelectionSet } from './types';
import { SelectionSet } from '../types';

@@ -15,2 +18,20 @@ /** Returns the name of a given node */

export const getOperationName = (node: OperationDefinitionNode) => {
switch (node.operation) {
case 'query':
return 'Query';
case 'mutation':
return 'Mutation';
case 'subscription':
return 'Subscription';
}
};
export const getFragmentTypeName = (node: FragmentDefinitionNode): string =>
node.typeCondition.name.value;
/** Returns either the field's name or the field's alias */
export const getFieldAlias = (node: FieldNode): string =>
node.alias !== undefined ? node.alias.value : getName(node);
/** Returns the SelectionSet for a given inline or defined fragment node */

@@ -29,21 +50,7 @@ export const getSelectionSet = (node: {

export const isOperationNode = (
node: DefinitionNode
): node is OperationDefinitionNode => node.kind === 'OperationDefinition';
export const isFieldNode = (node: SelectionNode): node is FieldNode =>
node.kind === Kind.FIELD;
export const isFragmentNode = (
node: DefinitionNode
): node is FragmentDefinitionNode => node.kind === 'FragmentDefinition';
export const getOperationType = (
node: OperationDefinitionNode
): OperationType => {
switch (node.operation) {
case 'query':
return 'Query';
case 'mutation':
return 'Mutation';
case 'subscription':
return 'Subscription';
}
};
export const isInlineFragment = (
node: SelectionNode
): node is InlineFragmentNode => node.kind === Kind.INLINE_FRAGMENT;

@@ -8,9 +8,16 @@ import {

} from 'urql';
import { filter, map, merge, pipe, share, tap } from 'wonka';
import { query, write, writeOptimistic } from './operations';
import { Store } from './store';
import { query, write } from './operations';
import { create, SerializedStore } from './store';
import {
Completeness,
UpdatesConfig,
ResolverConfig,
OptimisticMutationConfig,
} from './types';
type OperationResultWithMeta = OperationResult & {
isComplete: boolean;
completeness: Completeness;
};

@@ -34,21 +41,19 @@

// Returns whether an operation is a query
const isQueryOperation = (op: Operation): boolean => {
const isQueryOperation = (op: Operation): boolean =>
op.operationName === 'query';
// Returns whether an operation is a mutation
const isMutationOperation = (op: Operation): boolean =>
op.operationName === 'mutation';
// Returns whether an operation can potentially be read from cache
const isCacheableQuery = (op: Operation): boolean => {
const policy = getRequestPolicy(op);
return (
op.operationName === 'query' &&
(policy === 'cache-and-network' ||
policy === 'cache-first' ||
policy === 'cache-only')
);
return isQueryOperation(op) && policy !== 'network-only';
};
// Returns whether an operation is handled by this exchange
const isCacheableQuery = (op: Operation): boolean => {
// Returns whether an operation potentially triggers an optimistic update
const isOptimisticMutation = (op: Operation): boolean => {
const policy = getRequestPolicy(op);
return (
isQueryOperation(op) &&
(policy === 'cache-and-network' ||
policy === 'cache-first' ||
policy === 'cache-only')
);
return isMutationOperation(op) && policy !== 'network-only';
};

@@ -69,3 +74,5 @@

export interface CacheExchangeOpts {
initial?: SerializedStore;
updates?: UpdatesConfig;
resolvers?: ResolverConfig;
optimistic?: OptimisticMutationConfig;
}

@@ -77,3 +84,4 @@

}) => {
const store = create(opts.initial);
const store = new Store(opts.resolvers, opts.updates, opts.optimistic);
const optimisticKeys = new Set();
const ops: OperationMap = new Map();

@@ -87,3 +95,3 @@ const deps = Object.create(null) as DependentOperations;

triggerOp: Operation,
dependencies: string[]
dependencies: Set<string>
) => {

@@ -111,4 +119,16 @@ const pendingOperations = new Set<number>();

// This executes an optimistic update for mutations and registers it if necessary
const optimisticUpdate = (operation: Operation) => {
if (isOptimisticMutation(operation)) {
const { key } = operation;
const { dependencies } = writeOptimistic(store, operation, key);
if (dependencies.size !== 0) {
optimisticKeys.add(key);
processDependencies(operation, dependencies);
}
}
};
// This updates the known dependencies for the passed operation
const updateDependencies = (op: Operation, dependencies: string[]) => {
const updateDependencies = (op: Operation, dependencies: Set<string>) => {
dependencies.forEach(dep => {

@@ -135,3 +155,3 @@ const keys = deps[dep] || (deps[dep] = []);

const res = query(store, operation);
const isComplete = policy === 'cache-only' || res.isComplete;
const isComplete = policy === 'cache-only' || res.completeness === 'FULL';
if (isComplete) {

@@ -143,3 +163,3 @@ updateDependencies(operation, res.dependencies);

operation,
isComplete,
completeness: isComplete ? 'FULL' : 'EMPTY',
data: res.data,

@@ -150,16 +170,19 @@ };

// Take any OperationResult and update the cache with it
const updateCacheWithResult = ({
error,
data,
operation,
}: OperationResult) => {
if (
(error === undefined || error.networkError === undefined) &&
data !== null &&
data !== undefined
) {
const { dependencies } = write(store, operation, data);
const updateCacheWithResult = ({ data, operation }: OperationResult) => {
let dependencies;
if (data !== null && data !== undefined) {
dependencies = write(store, operation, data).dependencies;
}
// Clear old optimistic values from the store
const { key } = operation;
if (optimisticKeys.has(key)) {
optimisticKeys.delete(key);
store.clearOptimistic(key);
}
if (dependencies !== undefined) {
// Update operations that depend on the updated data (except the current one)
processDependencies(operation, dependencies);
// Update this operation's dependencies if it's a query

@@ -176,2 +199,3 @@ if (isQueryOperation(operation)) {

map(addTypeNames),
tap(optimisticUpdate),
share

@@ -191,3 +215,3 @@ );

cache$,
filter(res => !res.isComplete),
filter(res => res.completeness !== 'FULL'),
map(res => res.operation)

@@ -200,3 +224,3 @@ );

cache$,
filter(res => res.isComplete),
filter(res => res.completeness === 'FULL'),
tap(({ operation }) => {

@@ -203,0 +227,0 @@ const policy = getRequestPolicy(operation);

import stringify from 'fast-json-stable-stringify';
import { VarsMap } from '../ast';
import { SystemFields } from '../types';
import { Variables, SystemFields } from '../types';

@@ -25,6 +24,6 @@ export const isOperation = (typeName: string) =>

export const keyOfField = (fieldName: string, args: null | VarsMap) =>
args !== null ? `${fieldName}(${stringify(args)})` : fieldName;
export const keyOfField = (fieldName: string, args?: null | Variables) =>
args ? `${fieldName}(${stringify(args)})` : fieldName;
export const joinKeys = (parentKey: string, key: string) =>
`${parentKey}.${key}`;
export * from './operations';
export * from './types';
export { create, serialize } from './store';
export { Store } from './store';
export { cacheExchange } from './exchange';
export { query } from './query';
export { write } from './write';
export { gc } from './gc';
export { write, writeOptimistic, writeFragment } from './write';
import {
getFragments,
getMainOperation,
getSelectionSet,
normalizeVariables,
getName,
getFieldArguments,
getFieldAlias,
getFieldArguments,
getName,
getSelectionSet,
SelectionSet,
} from '../ast';
import { joinKeys, keyOfField } from '../helpers';
import { find, readLink, Store } from '../store';
import { Entity, Link } from '../types';
import {
Fragments,
Variables,
Data,
DataField,
Link,
SelectionSet,
Completeness,
OperationRequest,
} from '../types';
import { forEachFieldNode, makeContext } from './shared';
import { Context, Data, Request, Result } from './types';
import {
Store,
addDependency,
getCurrentDependencies,
initStoreState,
clearStoreState,
} from '../store';
import { forEachFieldNode } from './shared';
import { joinKeys, keyOfEntity, keyOfField } from '../helpers';
export interface QueryResult {
completeness: Completeness;
dependencies: Set<string>;
data: null | Data;
}
interface Context {
result: QueryResult;
store: Store;
variables: Variables;
fragments: Fragments;
}
/** Reads a request entirely from the store */
export const query = (store: Store, request: Request): Result => {
const ctx = makeContext(store, request);
if (ctx === undefined) {
return { isComplete: false, dependencies: [] };
}
export const query = (store: Store, request: OperationRequest): QueryResult => {
initStoreState(0);
const select = getSelectionSet(ctx.operation);
const data = readEntity(ctx, 'Query', select, Object.create(null));
const operation = getMainOperation(request.query);
const root: Data = Object.create(null);
const result: QueryResult = {
completeness: 'FULL',
dependencies: getCurrentDependencies(),
data: root,
};
return {
data,
isComplete: ctx.isComplete,
dependencies: ctx.dependencies,
const ctx: Context = {
variables: normalizeVariables(operation, request.variables),
fragments: getFragments(request.query),
result,
store,
};
result.data = readSelection(ctx, 'Query', getSelectionSet(operation), root);
clearStoreState();
return result;
};
const readEntity = (
const readSelection = (
ctx: Context,
key: string,
entityKey: string,
select: SelectionSet,
data: Data
): Data | null => {
const { store } = ctx;
const entity = find(store, key);
if (entity === null) {
// Cache Incomplete: A missing entity for a key means it wasn't cached
ctx.isComplete = false;
const isQuery = entityKey === 'Query';
if (!isQuery) addDependency(entityKey);
const { store, variables } = ctx;
// Get the __typename field for a given entity to check that it exists
const typename = store.getField(entityKey, '__typename');
if (typeof typename !== 'string') {
ctx.result.completeness = 'EMPTY';
return null;
} else if (key !== 'Query') {
ctx.dependencies.push(key);
}
return readSelection(ctx, entity, key, select, data);
};
data.__typename = typename;
const readSelection = (
ctx: Context,
entity: Entity,
key: string,
select: SelectionSet,
data: Data
): Data => {
data.__typename = entity.__typename as string;
forEachFieldNode(ctx, select, node => {
const { store, vars } = ctx;
forEachFieldNode(typename, entityKey, select, ctx, node => {
// Derive the needed data from our node.
const fieldName = getName(node);
// The field's key can include arguments if it has any
const fieldKey = keyOfField(fieldName, getFieldArguments(node, vars));
const fieldValue = entity[fieldKey];
const fieldArgs = getFieldArguments(node, variables);
const fieldAlias = getFieldAlias(node);
const childFieldKey = joinKeys(key, fieldKey);
if (key === 'Query') {
ctx.dependencies.push(childFieldKey);
}
const fieldKey = joinKeys(entityKey, keyOfField(fieldName, fieldArgs));
const fieldValue = store.getRecord(fieldKey);
if (fieldValue === undefined) {
// Cache Incomplete: A missing field means it wasn't cached
ctx.isComplete = false;
data[fieldAlias] = null;
} else if (node.selectionSet === undefined || fieldValue !== null) {
data[fieldAlias] = fieldValue;
if (isQuery) addDependency(fieldKey);
const resolvers = store.resolvers[typename];
if (resolvers !== undefined && resolvers.hasOwnProperty(fieldName)) {
// We have a resolver for this field.
const resolverValue = resolvers[fieldName](
data,
fieldArgs || {},
store,
ctx
);
if (node.selectionSet === undefined) {
// If it doesn't have a selection set we have resolved a property.
// We assume that a resolver for scalar values implies that this
// field is always present, so completeness won't be set to EMPTY here
data[fieldAlias] = resolverValue !== undefined ? resolverValue : null;
} else {
// When it has a selection set we are resolving an entity with a
// subselection. This can either be a list or an object.
const fieldSelect = getSelectionSet(node);
data[fieldAlias] = resolveResolverResult(
ctx,
resolverValue,
fieldKey,
fieldSelect,
data[fieldAlias] as Data | Data[]
);
}
} else if (node.selectionSet === undefined) {
// The field is a scalar and can be retrieved directly
if (fieldValue === undefined) {
// Cache Incomplete: A missing field means it wasn't cached
ctx.result.completeness = 'EMPTY';
data[fieldAlias] = null;
} else {
// Not dealing with null means it's a regular property.
data[fieldAlias] = fieldValue;
}
} else {
// null values mean that a field might be linked to other entities
const fieldSelect = getSelectionSet(node);
const link = readLink(store, childFieldKey);
const link = store.getLink(fieldKey);
// Cache Incomplete: A missing link for a field means it's not cached
if (link === undefined) {
ctx.isComplete = false;
data[fieldAlias] = null;
if (typeof fieldValue === 'object' && fieldValue !== null) {
// The entity on the field was invalid and can still be recovered
data[fieldAlias] = fieldValue;
} else {
ctx.result.completeness = 'EMPTY';
data[fieldAlias] = null;
}
} else {
const prevData = data[fieldAlias] as Data;
data[fieldAlias] = readField(ctx, link, fieldSelect, prevData);
data[fieldAlias] = resolveLink(ctx, link, fieldSelect, prevData);
}

@@ -99,7 +162,53 @@ }

const readField = (
const resolveResolverResult = (
ctx: Context,
link: Link,
result: DataField,
key: string,
select: SelectionSet,
prevData: void | Data | Data[]
) => {
// When we are dealing with a list we have to call this method again.
if (Array.isArray(result)) {
// @ts-ignore: Link cannot be expressed as a recursive type
return result.map((childResult, index) => {
const data = prevData !== undefined ? prevData[index] : undefined;
const indexKey = joinKeys(key, `${index}`);
return resolveResolverResult(ctx, childResult, indexKey, select, data);
});
} else if (result === null) {
return null;
} else if (isDataOrKey(result)) {
// We don't need to read the entity after exiting a resolver
// we can just go on and read the selection further.
const data = prevData === undefined ? Object.create(null) : prevData;
const childKey =
(typeof result === 'string' ? result : keyOfEntity(result)) || key;
const selectionResult = readSelection(ctx, childKey, select, data);
if (selectionResult !== null && typeof result === 'object') {
for (key in result) {
if (key !== '__typename' && result.hasOwnProperty(key)) {
selectionResult[key] = result[key];
}
}
}
return selectionResult;
}
if (process.env.NODE_ENV !== 'production') {
console.warn(
'Expected to receive a Link or an Entity from a Resolver but got a Scalar.'
);
}
ctx.result.completeness = 'EMPTY';
return null;
};
const resolveLink = (
ctx: Context,
link: Link | Link[],
select: SelectionSet,
prevData: void | Data | Data[]
): null | Data | Data[] => {

@@ -110,3 +219,3 @@ if (Array.isArray(link)) {

const data = prevData !== undefined ? prevData[index] : undefined;
return readField(ctx, childLink, select, data);
return resolveLink(ctx, childLink, select, data);
});

@@ -117,4 +226,10 @@ } else if (link === null) {

const data = prevData === undefined ? Object.create(null) : prevData;
return readEntity(ctx, link, select, data);
return readSelection(ctx, link, select, data);
}
};
const isDataOrKey = (x: any): x is string | Data =>
typeof x === 'string' ||
(typeof x === 'object' &&
x !== null &&
typeof (x as any).__typename === 'string');

@@ -1,42 +0,51 @@

import { FieldNode } from 'graphql';
import { FieldNode, InlineFragmentNode, FragmentDefinitionNode } from 'graphql';
import { Fragments, Variables, SelectionSet } from '../types';
import { Store } from '../store';
import { joinKeys, keyOfField } from '../helpers';
import {
getFragments,
getMainOperation,
getName,
getNormalizedVars,
getSelectionSet,
getTypeCondition,
getFieldArguments,
shouldInclude,
isFieldNode,
isInlineFragment,
SelectionSet,
shouldInclude,
getSelectionSet,
getName,
} from '../ast';
import { Store } from '../store';
import { Context, Request } from './types';
interface Context {
store: Store;
variables: Variables;
fragments: Fragments;
}
export const makeContext = (store: Store, request: Request): void | Context => {
const { query, variables } = request;
const operation = getMainOperation(query);
if (operation === undefined) {
return;
const isFragmentMatching = (
node: InlineFragmentNode | FragmentDefinitionNode,
typename: string,
entityKey: string,
ctx: Context
) => {
if (typename === getTypeCondition(node)) {
return true;
}
const dependencies = [];
const fragments = getFragments(query);
const vars = getNormalizedVars(operation, variables);
const isComplete = true;
return { dependencies, isComplete, operation, fragments, vars, store };
// This is a heuristic for now, but temporary until schema awareness becomes a thing
return !getSelectionSet(node).some(node => {
if (!isFieldNode(node)) return false;
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldKey = keyOfField(fieldName, fieldArgs);
return !ctx.store.hasField(joinKeys(entityKey, fieldKey));
});
};
export const forEachFieldNode = (
typename: string,
entityKey: string,
select: SelectionSet,
ctx: Context,
select: SelectionSet,
cb: (node: FieldNode) => void
) => {
const { vars, fragments } = ctx;
select.forEach(node => {
if (!shouldInclude(node, vars)) {
if (!shouldInclude(node, ctx.variables)) {
// Directives instruct this node to be skipped

@@ -46,9 +55,11 @@ return;

// A fragment is either referred to by FragmentSpread or inline
const def = isInlineFragment(node) ? node : fragments[getName(node)];
if (def !== undefined) {
const fragmentSelect = getSelectionSet(def);
// TODO: Check for getTypeCondition(def) to match
// Recursively process the fragments' selection sets
forEachFieldNode(ctx, fragmentSelect, cb);
const fragmentNode = isInlineFragment(node)
? node
: ctx.fragments[getName(node)];
if (
fragmentNode !== undefined &&
isFragmentMatching(fragmentNode, typename, entityKey, ctx)
) {
const fragmentSelect = getSelectionSet(fragmentNode);
forEachFieldNode(typename, entityKey, fragmentSelect, ctx, cb);
}

@@ -55,0 +66,0 @@ } else if (getName(node) !== '__typename') {

import {
getFieldAlias,
getFragments,
getMainOperation,
getSelectionSet,
normalizeVariables,
getFragmentTypeName,
getName,
getOperationName,
getFieldArguments,
getName,
getOperationType,
getSelectionSet,
SelectionSet,
} from '../ast';
import {
NullArray,
Fragments,
Variables,
Data,
Link,
Scalar,
SelectionSet,
OperationRequest,
} from '../types';
import {
Store,
addDependency,
getCurrentDependencies,
initStoreState,
clearStoreState,
} from '../store';
import { forEachFieldNode } from './shared';
import { joinKeys, keyOfEntity, keyOfField } from '../helpers';
import { findOrCreate, removeLink, setLink, Store } from '../store';
import { Entity, Link, Scalar } from '../types';
import { DocumentNode, FragmentDefinitionNode } from 'graphql';
import { forEachFieldNode, makeContext } from './shared';
import { Context, Data, Request, Result } from './types';
export interface WriteResult {
touched: string[];
dependencies: Set<string>;
}
interface Context {
result: WriteResult;
store: Store;
variables: Variables;
fragments: Fragments;
}
/** Writes a request given its response to the store */
export const write = (store: Store, request: Request, data: Data): Result => {
const ctx = makeContext(store, request);
if (ctx === undefined) {
return { isComplete: false, dependencies: [] };
}
export const write = (
store: Store,
request: OperationRequest,
data: Data
): WriteResult => {
initStoreState(0);
const { operation } = ctx;
const operation = getMainOperation(request.query);
const result: WriteResult = { dependencies: getCurrentDependencies() };
const ctx: Context = {
variables: normalizeVariables(operation, request.variables),
fragments: getFragments(request.query),
result,
store,
};
const select = getSelectionSet(operation);
const operationType = getOperationType(operation);
const operationName = getOperationName(operation);
if (typeof data.__typename !== 'string') {
data.__typename = operationType;
if (operationName === 'Query') {
writeSelection(ctx, operationName, select, data);
} else {
writeRoot(ctx, operationName, select, data);
}
if (operationType === 'Query') {
writeEntity(ctx, operationType, data, select);
} else {
writeRoot(ctx, data, select);
clearStoreState();
return result;
};
export const writeOptimistic = (
store: Store,
request: OperationRequest,
optimisticKey: number
): WriteResult => {
initStoreState(optimisticKey);
const operation = getMainOperation(request.query);
const result: WriteResult = { dependencies: getCurrentDependencies() };
const ctx: Context = {
variables: normalizeVariables(operation, request.variables),
fragments: getFragments(request.query),
result,
store,
};
const operationName = getOperationName(operation);
if (operationName === 'Mutation') {
const select = getSelectionSet(operation);
forEachFieldNode(operationName, operationName, select, ctx, node => {
if (node.selectionSet !== undefined) {
const fieldName = getName(node);
const resolver = ctx.store.optimisticMutations[fieldName];
if (resolver !== undefined) {
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldSelect = getSelectionSet(node);
const resolverValue = resolver(fieldArgs || {}, ctx.store, ctx);
if (!isScalar(resolverValue)) {
writeRootField(ctx, resolverValue, fieldSelect);
}
}
}
});
}
return { isComplete: true, dependencies: ctx.dependencies };
clearStoreState();
return result;
};
const writeEntity = (
ctx: Context,
key: string,
data: Data,
select: SelectionSet
export const writeFragment = (
store: Store,
query: DocumentNode,
data: Data
) => {
const { store } = ctx;
const entity = findOrCreate(store, key);
if (key !== 'Query') {
ctx.dependencies.push(key);
const fragments = getFragments(query);
const names = Object.keys(fragments);
const fragment = fragments[names[0]] as FragmentDefinitionNode;
if (process.env.NODE_ENV !== 'production' && fragment === undefined) {
throw new Error('At least one fragment must be passed to writeFragment.');
}
writeSelection(ctx, entity, key, data, select);
const select = getSelectionSet(fragment);
const fieldName = getFragmentTypeName(fragment);
const writeData = { ...data, __typename: fieldName } as Data;
const entityKey = keyOfEntity(writeData) as string;
if (process.env.NODE_ENV !== 'production' && !entityKey) {
throw new Error(
`You have to pass an "id" or "_id" with your writeFragment data.`
);
}
const ctx: Context = {
variables: {}, // TODO: Should we support variables?
fragments,
result: { dependencies: getCurrentDependencies() },
store,
};
writeSelection(ctx, entityKey, select, writeData);
};

@@ -62,38 +153,31 @@

ctx: Context,
entity: Entity,
key: string,
data: Data,
select: SelectionSet
entityKey: string,
select: SelectionSet,
data: Data
) => {
entity.__typename = data.__typename;
const isQuery = entityKey === 'Query';
if (!isQuery) addDependency(entityKey);
forEachFieldNode(ctx, select, node => {
const { store, vars } = ctx;
const { store, variables } = ctx;
const typename = data.__typename;
store.writeField(typename, entityKey, '__typename');
forEachFieldNode(typename, entityKey, select, ctx, node => {
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, variables);
const fieldKey = joinKeys(entityKey, keyOfField(fieldName, fieldArgs));
const fieldValue = data[getFieldAlias(node)];
// The field's key can include arguments if it has any
const fieldKey = keyOfField(fieldName, getFieldArguments(node, vars));
const childFieldKey = joinKeys(key, fieldKey);
if (key === 'Query' && fieldName !== '__typename') {
ctx.dependencies.push(childFieldKey);
}
if (
node.selectionSet === undefined ||
fieldValue === null ||
isScalar(fieldValue)
) {
if (isQuery) addDependency(fieldKey);
if (node.selectionSet === undefined) {
// This is a leaf node, so we're setting the field's value directly
entity[fieldKey] = fieldValue;
// Remove any links that might've existed before for this field
removeLink(store, childFieldKey);
} else {
// Ensure that this key exists on the entity and that previous values are thrown away
entity[fieldKey] = null;
store.writeRecord(fieldValue, fieldKey);
} else if (!isScalar(fieldValue)) {
// Process the field and write links for the child entities that have been written
const { selections: fieldSelect } = node.selectionSet;
const link = writeField(ctx, childFieldKey, fieldValue, fieldSelect);
setLink(store, childFieldKey, link);
const link = writeField(ctx, fieldKey, fieldSelect, fieldValue);
store.writeLink(link, fieldKey);
store.removeRecord(fieldKey);
} else {
// This is a rare case for invalid entities
store.writeRecord(fieldValue, fieldKey);
}

@@ -106,4 +190,4 @@ });

parentFieldKey: string,
data: Data | Data[] | null,
select: SelectionSet
select: SelectionSet,
data: null | Data | NullArray<Data>
): Link => {

@@ -115,3 +199,3 @@ if (Array.isArray(data)) {

// Recursively write array data
const links = writeField(ctx, indexKey, item, select);
const links = writeField(ctx, indexKey, select, item);
// Link cannot be expressed as a recursive type

@@ -124,6 +208,5 @@ return links as string | null;

// Write entity to key that falls back to the given parentFieldKey
const entityKey = keyOfEntity(data);
const key = entityKey !== null ? entityKey : parentFieldKey;
writeEntity(ctx, key, data, select);
writeSelection(ctx, key, select, data);
return key;

@@ -133,5 +216,13 @@ };

// This is like writeSelection but assumes no parent entity exists
const writeRoot = (ctx: Context, data: Data, select: SelectionSet) => {
forEachFieldNode(ctx, select, node => {
const fieldValue = data[getFieldAlias(node)];
const writeRoot = (
ctx: Context,
typename: string,
select: SelectionSet,
data: Data
) => {
forEachFieldNode(typename, typename, select, ctx, node => {
const fieldName = getName(node);
const fieldAlias = getFieldAlias(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldValue = data[fieldAlias];

@@ -146,2 +237,11 @@ if (

}
if (typename === 'Mutation') {
// We run side-effect updates after the default, normalized updates
// so that the data is already available in-store if necessary
const updater = ctx.store.updates[fieldName];
if (updater !== undefined) {
updater(data, fieldArgs || {}, ctx.store, ctx);
}
}
});

@@ -153,3 +253,3 @@ };

ctx: Context,
data: Data | Data[] | null,
data: null | Data | NullArray<Data>,
select: SelectionSet

@@ -166,3 +266,3 @@ ) => {

if (entityKey !== null) {
writeEntity(ctx, entityKey, data, select);
writeSelection(ctx, entityKey, select, data);
}

@@ -174,3 +274,3 @@ };

// doesn't have a __typename field
const isScalar = (x: Scalar | Data | Array<Scalar | Data>): x is Scalar => {
const isScalar = (x: any): x is Scalar | Scalar[] => {
if (Array.isArray(x)) {

@@ -177,0 +277,0 @@ return x.some(isScalar);

@@ -1,63 +0,179 @@

import { Entity, Link, Links, Records } from '../types';
import { assignObjectToMap, objectOfMap } from './utils';
import { DocumentNode } from 'graphql';
import * as Pessimism from 'pessimism';
export interface Store {
records: Records;
links: Links;
}
import {
EntityField,
Link,
ResolverConfig,
DataField,
SystemFields,
Variables,
Data,
UpdatesConfig,
OptimisticMutationConfig,
} from '../types';
export interface SerializedStore {
records: { [link: string]: Entity };
links: { [link: string]: Link };
import { keyOfEntity, joinKeys, keyOfField } from '../helpers';
import { query, write, writeFragment } from '../operations';
interface Ref<T> {
current: null | T;
}
/** Creates a new Store with an optional initial, serialized state */
export const create = (initial?: SerializedStore): Store => {
const records: Records = new Map();
const links: Links = new Map();
const currentDependencies: Ref<Set<string>> = { current: null };
const currentOptimisticKey: Ref<number> = { current: null };
if (initial !== undefined) {
assignObjectToMap(records, initial.records);
assignObjectToMap(links, initial.links);
// Resolve a ref value or throw when we're outside of a store run
const refValue = <T>(ref: Ref<T>): T => {
if (ref.current === null) {
// TODO: Add invariant and warning with some production transpilation
throw new Error(
'The cache may only be mutated during operations or as part of its config.'
);
}
return { records, links };
return ref.current;
};
/** Serializes a given Store to a plain JSON structure */
export const serialize = (store: Store): SerializedStore => {
const records = objectOfMap(store.records);
const links = objectOfMap(store.links);
return { records, links };
// Initialise a store run by resetting its internal state
export const initStoreState = (optimisticKey: null | number) => {
currentDependencies.current = new Set();
currentOptimisticKey.current = optimisticKey;
};
export const find = (store: Store, key: string): Entity | null => {
const entity = store.records.get(key);
return entity !== undefined ? entity : null;
// Finalise a store run by clearing its internal state
export const clearStoreState = () => {
currentDependencies.current = null;
currentOptimisticKey.current = null;
};
export const findOrCreate = (store: Store, key: string): Entity => {
const entity = find(store, key);
if (entity !== null) {
return entity;
}
export const getCurrentDependencies = () => refValue(currentDependencies);
const record: Entity = Object.create(null);
store.records.set(key, record);
return record;
// Add a dependency to the internal store state
export const addDependency = (dependency: string) => {
refValue(currentDependencies).add(dependency);
};
export const remove = (store: Store, key: string) => {
store.records.delete(key);
const mapSet = <T>(map: Pessimism.Map<T>, key: string, value: T) => {
const optimisticKey = refValue(currentOptimisticKey);
return Pessimism.setOptimistic(map, key, value, optimisticKey);
};
export const setLink = (store: Store, key: string, link: Link): void => {
store.links.set(key, link);
// Used to remove a value from a Map optimistially (possible by setting it to undefined)
const mapRemove = <T>(map: Pessimism.Map<T>, key: string) => {
const optimisticKey = refValue(currentOptimisticKey);
return optimisticKey
? Pessimism.setOptimistic(map, key, undefined, optimisticKey)
: Pessimism.remove(map, key);
};
export const removeLink = (store: Store, key: string): void => {
store.links.delete(key);
};
export class Store {
records: Pessimism.Map<EntityField>;
links: Pessimism.Map<Link>;
export const readLink = (store: Store, key: string): void | Link =>
store.links.get(key);
resolvers: ResolverConfig;
updates: UpdatesConfig;
optimisticMutations: OptimisticMutationConfig;
constructor(
resolvers?: ResolverConfig,
updates?: UpdatesConfig,
optimisticMutations?: OptimisticMutationConfig
) {
this.records = Pessimism.make();
this.links = Pessimism.make();
this.resolvers = resolvers || {};
this.updates = updates || {};
this.optimisticMutations = optimisticMutations || {};
}
clearOptimistic(optimisticKey: number) {
this.records = Pessimism.clearOptimistic(this.records, optimisticKey);
this.links = Pessimism.clearOptimistic(this.links, optimisticKey);
}
getRecord(fieldKey: string): EntityField {
return Pessimism.get(this.records, fieldKey);
}
removeRecord(fieldKey: string) {
return (this.records = mapRemove(this.records, fieldKey));
}
writeRecord(field: EntityField, fieldKey: string) {
return (this.records = mapSet(this.records, fieldKey, field));
}
getField(
entityKey: string,
fieldName: string,
args?: Variables
): EntityField {
const fieldKey = joinKeys(entityKey, keyOfField(fieldName, args));
return this.getRecord(fieldKey);
}
writeField(
field: EntityField,
entityKey: string,
fieldName: string,
args?: Variables
) {
const fieldKey = joinKeys(entityKey, keyOfField(fieldName, args));
return (this.records = mapSet(this.records, fieldKey, field));
}
getLink(key: string): undefined | Link {
return Pessimism.get(this.links, key);
}
removeLink(key: string) {
return (this.links = mapRemove(this.links, key));
}
writeLink(link: Link, key: string) {
return (this.links = mapSet(this.links, key, link));
}
resolveValueOrLink(fieldKey: string): DataField {
const fieldValue = this.getRecord(fieldKey);
// Undefined implies a link OR incomplete data.
// A value will imply that we are just fetching a field like date.
if (fieldValue !== undefined) return fieldValue;
// This can be an array OR a string OR undefined again
const link = this.getLink(fieldKey);
return link ? link : null;
}
resolve(entity: SystemFields, field: string, args?: Variables): DataField {
if (typeof entity === 'string') {
addDependency(entity);
return this.resolveValueOrLink(joinKeys(entity, keyOfField(field, args)));
} else {
// This gives us __typename:key
const entityKey = keyOfEntity(entity);
if (entityKey === null) return null;
addDependency(entityKey);
return this.resolveValueOrLink(
joinKeys(entityKey, keyOfField(field, args))
);
}
}
hasField(key: string): boolean {
return this.getRecord(key) !== undefined || this.getLink(key) !== undefined;
}
updateQuery(
dataQuery: DocumentNode,
updater: (data: Data | null) => Data
): void {
const { data } = query(this, { query: dataQuery });
write(this, { query: dataQuery }, updater(data));
}
writeFragment(dataFragment: DocumentNode, data: Data): void {
writeFragment(this, dataFragment, data);
}
}

@@ -1,6 +0,23 @@

/** A scalar is any fieldValue without a type. It can also include lists of scalars and embedded objects, which are simply represented as empty object type. */
export type Scalar = {} | string | number | null;
import { DocumentNode, FragmentDefinitionNode, SelectionNode } from 'graphql';
import { Store } from './store';
export type NullPrototype = { [K in keyof ObjectConstructor]: never };
// Helper types
export type NullArray<T> = Array<null | T>;
// GraphQL helper types
export type SelectionSet = ReadonlyArray<SelectionNode>;
export interface Fragments {
[fragmentName: string]: void | FragmentDefinitionNode;
}
// Scalar types are not entities as part of response data
export type Primitive = null | number | boolean | string;
export interface ScalarObject {
__typename?: never;
[key: string]: any;
}
export type Scalar = Primitive | ScalarObject;
export interface SystemFields {

@@ -12,17 +29,64 @@ __typename: string;

export interface EntityFields {
[fieldName: string]: Scalar;
export type EntityField = undefined | Scalar | Scalar[];
export type DataField = Scalar | Scalar[] | Data | NullArray<Data>;
export interface DataFields {
[fieldName: string]: DataField;
}
/** Every Entity must have a typeName. It might have some ID fields of which `id` and `_id` are recognised by default. Every other fieldValue is a scalar. */
export type Entity = NullPrototype & SystemFields & EntityFields;
export type Data = SystemFields & DataFields;
export type Link<Key = string> = null | Key | NullArray<Key>;
export type ResolvedLink = Link<Data>;
/** A link is a key or array of keys referencing other entities in the Records Map. It may be or contain `null`. */
export type Link = null | string | Array<string | null>;
export interface Variables {
[name: string]: Scalar | Scalar[] | Variables | NullArray<Variables>;
}
/** A link can be resolved into the entities it points to. The resulting structure is a ResolvedLink */
export type ResolvedLink = null | Entity | Array<Entity | null>;
// This is an input operation
export interface OperationRequest {
query: DocumentNode;
variables?: object;
}
export type Records = Map<string, Entity>;
export type Links = Map<string, Link>;
export type Embedded = Map<string, Scalar>;
export interface ResolveInfo {
fragments: Fragments;
variables: Variables;
}
// Cache resolvers are user-defined to overwrite an entity field result
export type Resolver = (
parent: Data,
args: Variables,
cache: Store,
info: ResolveInfo
) => DataField;
export interface ResolverConfig {
[typeName: string]: {
[fieldName: string]: Resolver;
};
}
export type UpdateResolver<T = any> = (
result: T,
args: Variables,
cache: Store,
info: ResolveInfo
) => void;
export interface UpdatesConfig {
[mutationFieldName: string]: UpdateResolver;
}
export type OptimisticMutationResolver = (
vars: Variables,
cache: Store,
info: ResolveInfo
) => null | Data | NullArray<Data>;
export interface OptimisticMutationConfig {
[mutationFieldName: string]: OptimisticMutationResolver;
}
// Completeness of the query result
export type Completeness = 'EMPTY' | 'PARTIAL' | 'FULL';
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc