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,72 @@
# CHANGELOG
----
**NOTE:** This changelog is no longer maintained. Changes are now tracked in
the top level [`CHANGELOG.md`](https://github.com/apollographql/apollo-client/blob/master/CHANGELOG.md).
----
### 1.1.12
- No changes.
### 1.1.11
- No changes.
### 1.1.10
- Added optional generics to cache manipulation methods (typescript).
[PR #3541](https://github.com/apollographql/apollo-client/pull/3541)
### 1.1.9
- No public facing functionality changes.
- Various internal code cleanup, tooling and dependency changes.
### 1.1.8
- Not documented
### 1.1.7
- Not documented
### 1.1.6
- Improve code coverage
- Map coverage to original source
### 1.1.3
- Dependency updates
### 1.1.2
- Dependency updates
### 1.1.1
- Dependency updates
### 1.1.0
- Add cache.writeData to base cache type & DataProxy
[PR #2818](https://github.com/apollographql/apollo-client/pull/2818)
### 1.0.3
- Dependency updates
### 1.0.2
- Package dependency updates
### 1.0.1
- Improved rollup builds
### 1.0.0
- Initial solid cache API

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2018 Meteor Development Group, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
module.exports = {
...require('../../config/jest.config.settings'),
};

View File

@@ -0,0 +1,46 @@
{
"name": "apollo-cache",
"version": "1.3.5",
"description": "Core abstract of Caching layer for Apollo Client",
"author": "James Baxley <james@meteor.com>",
"contributors": [
"James Baxley <james@meteor.com>",
"Jonas Helfer <jonas@helfer.email>",
"Sashko Stubailo <sashko@stubailo.com>",
"James Burgess <jamesmillerburgess@gmail.com>"
],
"license": "MIT",
"main": "./lib/bundle.cjs.js",
"module": "./lib/bundle.esm.js",
"typings": "./lib/index.d.ts",
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/apollographql/apollo-client.git"
},
"bugs": {
"url": "https://github.com/apollographql/apollo-client/issues"
},
"homepage": "https://github.com/apollographql/apollo-client#readme",
"scripts": {
"prepare": "npm run lint && npm run build",
"coverage": "jest --coverage",
"test": "tsc -p tsconfig.json --noEmit && jest",
"lint": "tslint -c \"../../config/tslint.json\" -p tsconfig.json src/*.ts && tslint -c \"../../config/tslint.json\" -p tsconfig.json tests/*.ts",
"prebuild": "npm run clean",
"build": "tsc -b .",
"postbuild": "npm run bundle",
"bundle": "npx rollup -c rollup.config.js",
"watch": "../../node_modules/tsc-watch/index.js --onSuccess \"npm run postbuild\"",
"clean": "rm -rf coverage/* lib/*",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"apollo-utilities": "^1.3.4",
"tslib": "^1.10.0"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
},
"gitHead": "d22394c419ff7d678afb5e7d4cd1df16ed803ead"
}

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,
},
],
},
},
],
};