Project files

This commit is contained in:
2023-11-09 18:47:11 +01:00
parent 695abe054b
commit c415135aae
8554 changed files with 858111 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[
`writing data with no query converts a JavaScript object to a query correctly arrays 1`
] = `
"query GeneratedClientQuery {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;
exports[
`writing data with no query converts a JavaScript object to a query correctly basic 1`
] = `
"query GeneratedClientQuery {
number
bool
bool2
undef
nullField
str
}
"
`;
exports[
`writing data with no query converts a JavaScript object to a query correctly fragments 1`
] = `
"fragment GeneratedClientQuery on __FakeType {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;
exports[
`writing data with no query converts a JavaScript object to a query correctly nested 1`
] = `
"query GeneratedClientQuery {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;

View File

@@ -0,0 +1,149 @@
import gql from 'graphql-tag';
import { ApolloCache as Cache } from '../cache';
class TestCache extends Cache {}
describe('abstract cache', () => {
describe('transformDocument', () => {
it('returns the document', () => {
const test = new TestCache();
expect(test.transformDocument('a')).toBe('a');
});
});
describe('transformForLink', () => {
it('returns the document', () => {
const test = new TestCache();
expect(test.transformForLink('a')).toBe('a');
});
});
describe('readQuery', () => {
it('runs the read method', () => {
const test = new TestCache();
test.read = jest.fn();
test.readQuery({});
expect(test.read).toBeCalled();
});
it('defaults optimistic to false', () => {
const test = new TestCache();
test.read = ({ optimistic }) => optimistic;
expect(test.readQuery({})).toBe(false);
expect(test.readQuery({}, true)).toBe(true);
});
});
describe('readFragment', () => {
it('runs the read method', () => {
const test = new TestCache();
test.read = jest.fn();
const fragment = {
id: 'frag',
fragment: gql`
fragment a on b {
name
}
`,
};
test.readFragment(fragment);
expect(test.read).toBeCalled();
});
it('defaults optimistic to false', () => {
const test = new TestCache();
test.read = ({ optimistic }) => optimistic;
const fragment = {
id: 'frag',
fragment: gql`
fragment a on b {
name
}
`,
};
expect(test.readFragment(fragment)).toBe(false);
expect(test.readFragment(fragment, true)).toBe(true);
});
});
describe('writeQuery', () => {
it('runs the write method', () => {
const test = new TestCache();
test.write = jest.fn();
test.writeQuery({});
expect(test.write).toBeCalled();
});
});
describe('writeFragment', () => {
it('runs the write method', () => {
const test = new TestCache();
test.write = jest.fn();
const fragment = {
id: 'frag',
fragment: gql`
fragment a on b {
name
}
`,
};
test.writeFragment(fragment);
expect(test.write).toBeCalled();
});
});
describe('writeData', () => {
it('either writes a fragment or a query', () => {
const test = new TestCache();
test.read = jest.fn();
test.writeFragment = jest.fn();
test.writeQuery = jest.fn();
test.writeData({});
expect(test.writeQuery).toBeCalled();
test.writeData({ id: 1 });
expect(test.read).toBeCalled();
expect(test.writeFragment).toBeCalled();
// Edge case for falsey id
test.writeData({ id: 0 });
expect(test.read).toHaveBeenCalledTimes(2);
expect(test.writeFragment).toHaveBeenCalledTimes(2);
});
it('suppresses read errors', () => {
const test = new TestCache();
test.read = () => {
throw new Error();
};
test.writeFragment = jest.fn();
expect(() => test.writeData({ id: 1 })).not.toThrow();
expect(test.writeFragment).toBeCalled();
});
it('reads __typename from typenameResult or defaults to __ClientData', () => {
const test = new TestCache();
test.read = () => ({ __typename: 'a' });
let res;
test.writeFragment = obj =>
(res = obj.fragment.definitions[0].typeCondition.name.value);
test.writeData({ id: 1 });
expect(res).toBe('a');
test.read = () => ({});
test.writeData({ id: 1 });
expect(res).toBe('__ClientData');
});
});
});

View File

@@ -0,0 +1,76 @@
import { print } from 'graphql/language/printer';
import { queryFromPojo, fragmentFromPojo } from '../utils';
describe('writing data with no query', () => {
describe('converts a JavaScript object to a query correctly', () => {
it('basic', () => {
expect(
print(
queryFromPojo({
number: 5,
bool: true,
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
}),
),
).toMatchSnapshot();
});
it('nested', () => {
expect(
print(
queryFromPojo({
number: 5,
bool: true,
nested: {
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
}),
),
).toMatchSnapshot();
});
it('arrays', () => {
expect(
print(
queryFromPojo({
number: [5],
bool: [[true]],
nested: [
{
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
],
}),
),
).toMatchSnapshot();
});
it('fragments', () => {
expect(
print(
fragmentFromPojo({
number: [5],
bool: [[true]],
nested: [
{
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
],
}),
),
).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,150 @@
import { DocumentNode } from 'graphql';
import { getFragmentQueryDocument } from 'apollo-utilities';
import { DataProxy, Cache } from './types';
import { justTypenameQuery, queryFromPojo, fragmentFromPojo } from './utils';
export type Transaction<T> = (c: ApolloCache<T>) => void;
export abstract class ApolloCache<TSerialized> implements DataProxy {
// required to implement
// core API
public abstract read<T, TVariables = any>(
query: Cache.ReadOptions<TVariables>,
): T | null;
public abstract write<TResult = any, TVariables = any>(
write: Cache.WriteOptions<TResult, TVariables>,
): void;
public abstract diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T>;
public abstract watch(watch: Cache.WatchOptions): () => void;
public abstract evict<TVariables = any>(
query: Cache.EvictOptions<TVariables>,
): Cache.EvictionResult;
public abstract reset(): Promise<void>;
// intializer / offline / ssr API
/**
* Replaces existing state in the cache (if any) with the values expressed by
* `serializedState`.
*
* Called when hydrating a cache (server side rendering, or offline storage),
* and also (potentially) during hot reloads.
*/
public abstract restore(
serializedState: TSerialized,
): ApolloCache<TSerialized>;
/**
* Exposes the cache's complete state, in a serializable format for later restoration.
*/
public abstract extract(optimistic?: boolean): TSerialized;
// optimistic API
public abstract removeOptimistic(id: string): void;
// transactional API
public abstract performTransaction(
transaction: Transaction<TSerialized>,
): void;
public abstract recordOptimisticTransaction(
transaction: Transaction<TSerialized>,
id: string,
): void;
// optional API
public transformDocument(document: DocumentNode): DocumentNode {
return document;
}
// experimental
public transformForLink(document: DocumentNode): DocumentNode {
return document;
}
// DataProxy API
/**
*
* @param options
* @param optimistic
*/
public readQuery<QueryType, TVariables = any>(
options: DataProxy.Query<TVariables>,
optimistic: boolean = false,
): QueryType | null {
return this.read({
query: options.query,
variables: options.variables,
optimistic,
});
}
public readFragment<FragmentType, TVariables = any>(
options: DataProxy.Fragment<TVariables>,
optimistic: boolean = false,
): FragmentType | null {
return this.read({
query: getFragmentQueryDocument(options.fragment, options.fragmentName),
variables: options.variables,
rootId: options.id,
optimistic,
});
}
public writeQuery<TData = any, TVariables = any>(
options: Cache.WriteQueryOptions<TData, TVariables>,
): void {
this.write({
dataId: 'ROOT_QUERY',
result: options.data,
query: options.query,
variables: options.variables,
});
}
public writeFragment<TData = any, TVariables = any>(
options: Cache.WriteFragmentOptions<TData, TVariables>,
): void {
this.write({
dataId: options.id,
result: options.data,
variables: options.variables,
query: getFragmentQueryDocument(options.fragment, options.fragmentName),
});
}
public writeData<TData = any>({
id,
data,
}: Cache.WriteDataOptions<TData>): void {
if (typeof id !== 'undefined') {
let typenameResult = null;
// Since we can't use fragments without having a typename in the store,
// we need to make sure we have one.
// To avoid overwriting an existing typename, we need to read it out first
// and generate a fake one if none exists.
try {
typenameResult = this.read<any>({
rootId: id,
optimistic: false,
query: justTypenameQuery,
});
} catch (e) {
// Do nothing, since an error just means no typename exists
}
// tslint:disable-next-line
const __typename =
(typenameResult && typenameResult.__typename) || '__ClientData';
// Add a type here to satisfy the inmemory cache
const dataToWrite = Object.assign({ __typename }, data);
this.writeFragment({
id,
fragment: fragmentFromPojo(dataToWrite, __typename),
data: dataToWrite,
});
} else {
this.writeQuery({ query: queryFromPojo(data), data });
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './cache';
export * from './types';

View File

@@ -0,0 +1,40 @@
import { DataProxy } from './DataProxy';
export namespace Cache {
export type WatchCallback = (newData: any) => void;
export interface EvictionResult {
success: Boolean;
}
export interface ReadOptions<TVariables = any>
extends DataProxy.Query<TVariables> {
rootId?: string;
previousResult?: any;
optimistic: boolean;
}
export interface WriteOptions<TResult = any, TVariables = any>
extends DataProxy.Query<TVariables> {
dataId: string;
result: TResult;
}
export interface DiffOptions extends ReadOptions {
returnPartialData?: boolean;
}
export interface WatchOptions extends ReadOptions {
callback: WatchCallback;
}
export interface EvictOptions<TVariables = any>
extends DataProxy.Query<TVariables> {
rootId?: string;
}
export import DiffResult = DataProxy.DiffResult;
export import WriteQueryOptions = DataProxy.WriteQueryOptions;
export import WriteFragmentOptions = DataProxy.WriteFragmentOptions;
export import WriteDataOptions = DataProxy.WriteDataOptions;
export import Fragment = DataProxy.Fragment;
}

View File

@@ -0,0 +1,127 @@
import { DocumentNode } from 'graphql'; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved
export namespace DataProxy {
export interface Query<TVariables> {
/**
* The GraphQL query shape to be used constructed using the `gql` template
* string tag from `graphql-tag`. The query will be used to determine the
* shape of the data to be read.
*/
query: DocumentNode;
/**
* Any variables that the GraphQL query may depend on.
*/
variables?: TVariables;
}
export interface Fragment<TVariables> {
/**
* The root id to be used. This id should take the same form as the
* value returned by your `dataIdFromObject` function. If a value with your
* id does not exist in the store, `null` will be returned.
*/
id: string;
/**
* A GraphQL document created using the `gql` template string tag from
* `graphql-tag` with one or more fragments which will be used to determine
* the shape of data to read. If you provide more than one fragment in this
* document then you must also specify `fragmentName` to select a single.
*/
fragment: DocumentNode;
/**
* The name of the fragment in your GraphQL document to be used. If you do
* not provide a `fragmentName` and there is only one fragment in your
* `fragment` document then that fragment will be used.
*/
fragmentName?: string;
/**
* Any variables that your GraphQL fragments depend on.
*/
variables?: TVariables;
}
export interface WriteQueryOptions<TData, TVariables>
extends Query<TVariables> {
/**
* The data you will be writing to the store.
*/
data: TData;
}
export interface WriteFragmentOptions<TData, TVariables>
extends Fragment<TVariables> {
/**
* The data you will be writing to the store.
*/
data: TData;
}
export interface WriteDataOptions<TData> {
/**
* The data you will be writing to the store.
* It also takes an optional id property.
* The id is used to write a fragment to an existing object in the store.
*/
data: TData;
id?: string;
}
export type DiffResult<T> = {
result?: T;
complete?: boolean;
};
}
/**
* A proxy to the normalized data living in our store. This interface allows a
* user to read and write denormalized data which feels natural to the user
* whilst in the background this data is being converted into the normalized
* store format.
*/
export interface DataProxy {
/**
* Reads a GraphQL query from the root query id.
*/
readQuery<QueryType, TVariables = any>(
options: DataProxy.Query<TVariables>,
optimistic?: boolean,
): QueryType | null;
/**
* Reads a GraphQL fragment from any arbitrary id. If there is more than
* one fragment in the provided document then a `fragmentName` must be
* provided to select the correct fragment.
*/
readFragment<FragmentType, TVariables = any>(
options: DataProxy.Fragment<TVariables>,
optimistic?: boolean,
): FragmentType | null;
/**
* Writes a GraphQL query to the root query id.
*/
writeQuery<TData = any, TVariables = any>(
options: DataProxy.WriteQueryOptions<TData, TVariables>,
): void;
/**
* Writes a GraphQL fragment to any arbitrary id. If there is more than
* one fragment in the provided document then a `fragmentName` must be
* provided to select the correct fragment.
*/
writeFragment<TData = any, TVariables = any>(
options: DataProxy.WriteFragmentOptions<TData, TVariables>,
): void;
/**
* Sugar for writeQuery & writeFragment.
* Writes data to the store without passing in a query.
* If you supply an id, the data will be written as a fragment to an existing object.
* Otherwise, the data is written to the root of the store.
*/
writeData<TData = any>(options: DataProxy.WriteDataOptions<TData>): void;
}

View File

@@ -0,0 +1,2 @@
export * from './DataProxy';
export * from './Cache';

View File

@@ -0,0 +1,123 @@
import {
DocumentNode,
OperationDefinitionNode,
SelectionSetNode,
FieldNode,
FragmentDefinitionNode,
} from 'graphql';
export function queryFromPojo(obj: any): DocumentNode {
const op: OperationDefinitionNode = {
kind: 'OperationDefinition',
operation: 'query',
name: {
kind: 'Name',
value: 'GeneratedClientQuery',
},
selectionSet: selectionSetFromObj(obj),
};
const out: DocumentNode = {
kind: 'Document',
definitions: [op],
};
return out;
}
export function fragmentFromPojo(obj: any, typename?: string): DocumentNode {
const frag: FragmentDefinitionNode = {
kind: 'FragmentDefinition',
typeCondition: {
kind: 'NamedType',
name: {
kind: 'Name',
value: typename || '__FakeType',
},
},
name: {
kind: 'Name',
value: 'GeneratedClientQuery',
},
selectionSet: selectionSetFromObj(obj),
};
const out: DocumentNode = {
kind: 'Document',
definitions: [frag],
};
return out;
}
function selectionSetFromObj(obj: any): SelectionSetNode {
if (
typeof obj === 'number' ||
typeof obj === 'boolean' ||
typeof obj === 'string' ||
typeof obj === 'undefined' ||
obj === null
) {
// No selection set here
return null;
}
if (Array.isArray(obj)) {
// GraphQL queries don't include arrays
return selectionSetFromObj(obj[0]);
}
// Now we know it's an object
const selections: FieldNode[] = [];
Object.keys(obj).forEach(key => {
const nestedSelSet: SelectionSetNode = selectionSetFromObj(obj[key]);
const field: FieldNode = {
kind: 'Field',
name: {
kind: 'Name',
value: key,
},
selectionSet: nestedSelSet || undefined,
};
selections.push(field);
});
const selectionSet: SelectionSetNode = {
kind: 'SelectionSet',
selections,
};
return selectionSet;
}
export const justTypenameQuery: DocumentNode = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: null,
variableDefinitions: null,
directives: [],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
alias: null,
name: {
kind: 'Name',
value: '__typename',
},
arguments: [],
directives: [],
selectionSet: null,
},
],
},
},
],
};