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,8 @@
[ignore]
[include]
[libs]
[options]
suppress_comment= \\(.\\|\n\\)*\\$ExpectError

View File

@@ -0,0 +1,106 @@
# 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.0.16
- Removed unnecessary whitespace from error message
[Issue #3398](https://github.com/apollographql/apollo-client/issues/3398)
[PR #3593](https://github.com/apollographql/apollo-client/pull/3593)
### 1.0.15
- No changes
### 1.0.14
- Store key names generated by `getStoreKeyName` now leverage a more
deterministic approach to handling JSON based strings. This prevents store
key names from differing when using `args` like
`{ prop1: 'value1', prop2: 'value2' }` and
`{ prop2: 'value2', prop1: 'value1' }`.
[PR #2869](https://github.com/apollographql/apollo-client/pull/2869)
- Avoid needless `hasOwnProperty` check in `deepFreeze`.
[PR #3545](https://github.com/apollographql/apollo-client/pull/3545)
### 1.0.13
- Make `maybeDeepFreeze` a little more defensive, by always using
`Object.prototype.hasOwnProperty` (to avoid cases where the object being
frozen doesn't have its own `hasOwnProperty`).
[Issue #3426](https://github.com/apollographql/apollo-client/issues/3426)
[PR #3418](https://github.com/apollographql/apollo-client/pull/3418)
- Remove certain small internal caches to prevent memory leaks when using SSR.
[PR #3444](https://github.com/apollographql/apollo-client/pull/3444)
### 1.0.12
- Not documented
### 1.0.11
- `toIdValue` helper now takes an object with `id` and `typename` properties
as the preferred interface
[PR #3159](https://github.com/apollographql/apollo-client/pull/3159)
- Map coverage to original source
- Don't `deepFreeze` in development/test environments if ES6 symbols are
polyfilled
[PR #3082](https://github.com/apollographql/apollo-client/pull/3082)
- Added ability to include or ignore fragments in `getDirectivesFromDocument`
[PR #3010](https://github.com/apollographql/apollo-client/pull/3010)
### 1.0.9
- Dependency updates
- Added getDirectivesFromDocument utility function
[PR #2974](https://github.com/apollographql/apollo-client/pull/2974)
### 1.0.8
- Add client, rest, and export directives to list of known directives
[PR #2949](https://github.com/apollographql/apollo-client/pull/2949)
### 1.0.7
- Fix typo in error message for invalid argument being passed to @skip or
@include directives
[PR #2867](https://github.com/apollographql/apollo-client/pull/2867)
### 1.0.6
- Update `getStoreKeyName` to support custom directives
### 1.0.5
- Package dependency updates
### 1.0.4
- Package dependency updates
### 1.0.3
- Package dependency updates
### 1.0.2
- Improved rollup builds
### 1.0.1
- Added config to remove selection set of directive matches test
### 1.0.0
- Added utilities from hermes cache
- Added removeDirectivesFromDocument to allow cleaning of client only
directives
- Added hasDirectives to recurse the AST and return a boolean for an array of
directive names
- Improved performance of common store actions by memoizing addTypename and
removeConnectionDirective

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,48 @@
{
"name": "apollo-utilities",
"version": "1.3.4",
"description": "Utilities for working with GraphQL ASTs",
"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",
"test": "tsc -p tsconfig.json --noEmit && jest",
"coverage": "jest --coverage",
"lint": "tslint -c \"../../config/tslint.json\" -p tsconfig.json src/*.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"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
},
"dependencies": {
"@wry/equality": "^0.1.2",
"fast-json-stable-stringify": "^2.0.0",
"ts-invariant": "^0.4.0",
"tslib": "^1.10.0"
},
"gitHead": "d22394c419ff7d678afb5e7d4cd1df16ed803ead"
}

View File

@@ -0,0 +1,274 @@
import gql from 'graphql-tag';
import { cloneDeep } from 'lodash';
import { shouldInclude, hasDirectives } from '../directives';
import { getQueryDefinition } from '../getFromAST';
describe('hasDirective', () => {
it('should allow searching the ast for a directive', () => {
const query = gql`
query Simple {
field @live
}
`;
expect(hasDirectives(['live'], query)).toBe(true);
expect(hasDirectives(['defer'], query)).toBe(false);
});
it('works for all operation types', () => {
const query = gql`
{
field @live {
subField {
hello @live
}
}
}
`;
const mutation = gql`
mutation Directive {
mutate {
field {
subField {
hello @live
}
}
}
}
`;
const subscription = gql`
subscription LiveDirective {
sub {
field {
subField {
hello @live
}
}
}
}
`;
[query, mutation, subscription].forEach(x => {
expect(hasDirectives(['live'], x)).toBe(true);
expect(hasDirectives(['defer'], x)).toBe(false);
});
});
it('works for simple fragments', () => {
const query = gql`
query Simple {
...fieldFragment
}
fragment fieldFragment on Field {
foo @live
}
`;
expect(hasDirectives(['live'], query)).toBe(true);
expect(hasDirectives(['defer'], query)).toBe(false);
});
it('works for nested fragments', () => {
const query = gql`
query Simple {
...fieldFragment1
}
fragment fieldFragment1 on Field {
bar {
baz {
...nestedFragment
}
}
}
fragment nestedFragment on Field {
foo @live
}
`;
expect(hasDirectives(['live'], query)).toBe(true);
expect(hasDirectives(['defer'], query)).toBe(false);
});
});
describe('shouldInclude', () => {
it('should should not include a skipped field', () => {
const query = gql`
query {
fortuneCookie @skip(if: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, {})).toBe(true);
});
it('should include an included field', () => {
const query = gql`
query {
fortuneCookie @include(if: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(shouldInclude(field, {})).toBe(true);
});
it('should not include a not include: false field', () => {
const query = gql`
query {
fortuneCookie @include(if: false)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, {})).toBe(true);
});
it('should include a skip: false field', () => {
const query = gql`
query {
fortuneCookie @skip(if: false)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(shouldInclude(field, {})).toBe(true);
});
it('should not include a field if skip: true and include: true', () => {
const query = gql`
query {
fortuneCookie @skip(if: true) @include(if: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, {})).toBe(true);
});
it('should not include a field if skip: true and include: false', () => {
const query = gql`
query {
fortuneCookie @skip(if: true) @include(if: false)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, {})).toBe(true);
});
it('should include a field if skip: false and include: true', () => {
const query = gql`
query {
fortuneCookie @skip(if: false) @include(if: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(shouldInclude(field, {})).toBe(true);
});
it('should not include a field if skip: false and include: false', () => {
const query = gql`
query {
fortuneCookie @skip(if: false) @include(if: false)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, {})).toBe(true);
});
it('should leave the original query unmodified', () => {
const query = gql`
query {
fortuneCookie @skip(if: false) @include(if: false)
}
`;
const queryClone = cloneDeep(query);
const field = getQueryDefinition(query).selectionSet.selections[0];
shouldInclude(field, {});
expect(query).toEqual(queryClone);
});
it('does not throw an error on an unsupported directive', () => {
const query = gql`
query {
fortuneCookie @dosomething(if: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(() => {
shouldInclude(field, {});
}).not.toThrow();
});
it('throws an error on an invalid argument for the skip directive', () => {
const query = gql`
query {
fortuneCookie @skip(nothing: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(() => {
shouldInclude(field, {});
}).toThrow();
});
it('throws an error on an invalid argument for the include directive', () => {
const query = gql`
query {
fortuneCookie @include(nothing: true)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(() => {
shouldInclude(field, {});
}).toThrow();
});
it('throws an error on an invalid variable name within a directive argument', () => {
const query = gql`
query {
fortuneCookie @include(if: $neverDefined)
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(() => {
shouldInclude(field, {});
}).toThrow();
});
it('evaluates variables on skip fields', () => {
const query = gql`
query($shouldSkip: Boolean) {
fortuneCookie @skip(if: $shouldSkip)
}
`;
const variables = {
shouldSkip: true,
};
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, variables)).toBe(true);
});
it('evaluates variables on include fields', () => {
const query = gql`
query($shouldSkip: Boolean) {
fortuneCookie @include(if: $shouldInclude)
}
`;
const variables = {
shouldInclude: false,
};
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(!shouldInclude(field, variables)).toBe(true);
});
it('throws an error if the value of the argument is not a variable or boolean', () => {
const query = gql`
query {
fortuneCookie @include(if: "string")
}
`;
const field = getQueryDefinition(query).selectionSet.selections[0];
expect(() => {
shouldInclude(field, {});
}).toThrow();
});
});

View File

@@ -0,0 +1,327 @@
import { print } from 'graphql/language/printer';
import gql from 'graphql-tag';
import { disableFragmentWarnings } from 'graphql-tag';
// Turn off warnings for repeated fragment names
disableFragmentWarnings();
import { getFragmentQueryDocument } from '../fragments';
describe('getFragmentQueryDocument', () => {
it('will throw an error if there is an operation', () => {
expect(() =>
getFragmentQueryDocument(
gql`
{
a
b
c
}
`,
),
).toThrowError(
'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.',
);
expect(() =>
getFragmentQueryDocument(
gql`
query {
a
b
c
}
`,
),
).toThrowError(
'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.',
);
expect(() =>
getFragmentQueryDocument(
gql`
query Named {
a
b
c
}
`,
),
).toThrowError(
"Found a query operation named 'Named'. No operations are allowed when using a fragment as a query. Only fragments are allowed.",
);
expect(() =>
getFragmentQueryDocument(
gql`
mutation Named {
a
b
c
}
`,
),
).toThrowError(
"Found a mutation operation named 'Named'. No operations are allowed when using a fragment as a query. " +
'Only fragments are allowed.',
);
expect(() =>
getFragmentQueryDocument(
gql`
subscription Named {
a
b
c
}
`,
),
).toThrowError(
"Found a subscription operation named 'Named'. No operations are allowed when using a fragment as a query. " +
'Only fragments are allowed.',
);
});
it('will throw an error if there is not exactly one fragment but no `fragmentName`', () => {
expect(() => {
getFragmentQueryDocument(gql`
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
}
`);
}).toThrowError(
'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.',
);
expect(() => {
getFragmentQueryDocument(gql`
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
}
fragment baz on Baz {
g
h
i
}
`);
}).toThrowError(
'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.',
);
expect(() => {
getFragmentQueryDocument(gql`
scalar Foo
`);
}).toThrowError(
'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.',
);
});
it('will create a query document where the single fragment is spread in the root query', () => {
expect(
print(
getFragmentQueryDocument(gql`
fragment foo on Foo {
a
b
c
}
`),
),
).toEqual(
print(gql`
{
...foo
}
fragment foo on Foo {
a
b
c
}
`),
);
});
it('will create a query document where the named fragment is spread in the root query', () => {
expect(
print(
getFragmentQueryDocument(
gql`
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`,
'foo',
),
),
).toEqual(
print(gql`
{
...foo
}
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`),
);
expect(
print(
getFragmentQueryDocument(
gql`
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`,
'bar',
),
),
).toEqual(
print(gql`
{
...bar
}
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`),
);
expect(
print(
getFragmentQueryDocument(
gql`
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`,
'baz',
),
),
).toEqual(
print(gql`
{
...baz
}
fragment foo on Foo {
a
b
c
}
fragment bar on Bar {
d
e
f
...foo
}
fragment baz on Baz {
g
h
i
...foo
...bar
}
`),
);
});
});

View File

@@ -0,0 +1,316 @@
import { print } from 'graphql/language/printer';
import gql from 'graphql-tag';
import { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql';
import {
checkDocument,
getFragmentDefinitions,
getQueryDefinition,
getMutationDefinition,
createFragmentMap,
FragmentMap,
getDefaultValues,
getOperationName,
} from '../getFromAST';
describe('AST utility functions', () => {
it('should correctly check a document for correctness', () => {
const multipleQueries = gql`
query {
author {
firstName
lastName
}
}
query {
author {
address
}
}
`;
expect(() => {
checkDocument(multipleQueries);
}).toThrow();
const namedFragment = gql`
query {
author {
...authorDetails
}
}
fragment authorDetails on Author {
firstName
lastName
}
`;
expect(() => {
checkDocument(namedFragment);
}).not.toThrow();
});
it('should get fragment definitions from a document containing a single fragment', () => {
const singleFragmentDefinition = gql`
query {
author {
...authorDetails
}
}
fragment authorDetails on Author {
firstName
lastName
}
`;
const expectedDoc = gql`
fragment authorDetails on Author {
firstName
lastName
}
`;
const expectedResult: FragmentDefinitionNode[] = [
expectedDoc.definitions[0] as FragmentDefinitionNode,
];
const actualResult = getFragmentDefinitions(singleFragmentDefinition);
expect(actualResult.length).toEqual(expectedResult.length);
expect(print(actualResult[0])).toBe(print(expectedResult[0]));
});
it('should get fragment definitions from a document containing a multiple fragments', () => {
const multipleFragmentDefinitions = gql`
query {
author {
...authorDetails
...moreAuthorDetails
}
}
fragment authorDetails on Author {
firstName
lastName
}
fragment moreAuthorDetails on Author {
address
}
`;
const expectedDoc = gql`
fragment authorDetails on Author {
firstName
lastName
}
fragment moreAuthorDetails on Author {
address
}
`;
const expectedResult: FragmentDefinitionNode[] = [
expectedDoc.definitions[0] as FragmentDefinitionNode,
expectedDoc.definitions[1] as FragmentDefinitionNode,
];
const actualResult = getFragmentDefinitions(multipleFragmentDefinitions);
expect(actualResult.map(print)).toEqual(expectedResult.map(print));
});
it('should get the correct query definition out of a query containing multiple fragments', () => {
const queryWithFragments = gql`
fragment authorDetails on Author {
firstName
lastName
}
fragment moreAuthorDetails on Author {
address
}
query {
author {
...authorDetails
...moreAuthorDetails
}
}
`;
const expectedDoc = gql`
query {
author {
...authorDetails
...moreAuthorDetails
}
}
`;
const expectedResult: OperationDefinitionNode = expectedDoc
.definitions[0] as OperationDefinitionNode;
const actualResult = getQueryDefinition(queryWithFragments);
expect(print(actualResult)).toEqual(print(expectedResult));
});
it('should throw if we try to get the query definition of a document with no query', () => {
const mutationWithFragments = gql`
fragment authorDetails on Author {
firstName
lastName
}
mutation {
createAuthor(firstName: "John", lastName: "Smith") {
...authorDetails
}
}
`;
expect(() => {
getQueryDefinition(mutationWithFragments);
}).toThrow();
});
it('should get the correct mutation definition out of a mutation with multiple fragments', () => {
const mutationWithFragments = gql`
mutation {
createAuthor(firstName: "John", lastName: "Smith") {
...authorDetails
}
}
fragment authorDetails on Author {
firstName
lastName
}
`;
const expectedDoc = gql`
mutation {
createAuthor(firstName: "John", lastName: "Smith") {
...authorDetails
}
}
`;
const expectedResult: OperationDefinitionNode = expectedDoc
.definitions[0] as OperationDefinitionNode;
const actualResult = getMutationDefinition(mutationWithFragments);
expect(print(actualResult)).toEqual(print(expectedResult));
});
it('should create the fragment map correctly', () => {
const fragments = getFragmentDefinitions(gql`
fragment authorDetails on Author {
firstName
lastName
}
fragment moreAuthorDetails on Author {
address
}
`);
const fragmentMap = createFragmentMap(fragments);
const expectedTable: FragmentMap = {
authorDetails: fragments[0],
moreAuthorDetails: fragments[1],
};
expect(fragmentMap).toEqual(expectedTable);
});
it('should return an empty fragment map if passed undefined argument', () => {
expect(createFragmentMap(undefined)).toEqual({});
});
it('should get the operation name out of a query', () => {
const query = gql`
query nameOfQuery {
fortuneCookie
}
`;
const operationName = getOperationName(query);
expect(operationName).toEqual('nameOfQuery');
});
it('should get the operation name out of a mutation', () => {
const query = gql`
mutation nameOfMutation {
fortuneCookie
}
`;
const operationName = getOperationName(query);
expect(operationName).toEqual('nameOfMutation');
});
it('should return null if the query does not have an operation name', () => {
const query = gql`
{
fortuneCookie
}
`;
const operationName = getOperationName(query);
expect(operationName).toEqual(null);
});
it('should throw if type definitions found in document', () => {
const queryWithTypeDefination = gql`
fragment authorDetails on Author {
firstName
lastName
}
query($search: AuthorSearchInputType) {
author(search: $search) {
...authorDetails
}
}
input AuthorSearchInputType {
firstName: String
}
`;
expect(() => {
getQueryDefinition(queryWithTypeDefination);
}).toThrowError(
'Schema type definitions not allowed in queries. Found: "InputObjectTypeDefinition"',
);
});
describe('getDefaultValues', () => {
it('will create an empty variable object if no default values are provided', () => {
const basicQuery = gql`
query people($first: Int, $second: String) {
allPeople(first: $first) {
people {
name
}
}
}
`;
expect(getDefaultValues(getQueryDefinition(basicQuery))).toEqual({});
});
it('will create a variable object based on the definition node with default values', () => {
const basicQuery = gql`
query people($first: Int = 1, $second: String!) {
allPeople(first: $first) {
people {
name
}
}
}
`;
const complexMutation = gql`
mutation complexStuff(
$test: Input = { key1: ["value", "value2"], key2: { key3: 4 } }
) {
complexStuff(test: $test) {
people {
name
}
}
}
`;
expect(getDefaultValues(getQueryDefinition(basicQuery))).toEqual({
first: 1,
});
expect(getDefaultValues(getMutationDefinition(complexMutation))).toEqual({
test: { key1: ['value', 'value2'], key2: { key3: 4 } },
});
});
});
});

View File

@@ -0,0 +1,23 @@
import { getStoreKeyName } from '../storeUtils';
describe('getStoreKeyName', () => {
it(
'should return a deterministic version of the store key name no matter ' +
'which order the args object properties are in',
() => {
const validStoreKeyName =
'someField({"prop1":"value1","prop2":"value2"})';
let generatedStoreKeyName = getStoreKeyName('someField', {
prop1: 'value1',
prop2: 'value2',
});
expect(generatedStoreKeyName).toEqual(validStoreKeyName);
generatedStoreKeyName = getStoreKeyName('someField', {
prop2: 'value2',
prop1: 'value1',
});
expect(generatedStoreKeyName).toEqual(validStoreKeyName);
},
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
declare module 'fast-json-stable-stringify';

View File

@@ -0,0 +1,127 @@
// Provides the methods that allow QueryManager to handle the `skip` and
// `include` directives within GraphQL.
import {
FieldNode,
SelectionNode,
VariableNode,
BooleanValueNode,
DirectiveNode,
DocumentNode,
ArgumentNode,
ValueNode,
} from 'graphql';
import { visit } from 'graphql/language/visitor';
import { invariant } from 'ts-invariant';
import { argumentsObjectFromField } from './storeUtils';
export type DirectiveInfo = {
[fieldName: string]: { [argName: string]: any };
};
export function getDirectiveInfoFromField(
field: FieldNode,
variables: Object,
): DirectiveInfo {
if (field.directives && field.directives.length) {
const directiveObj: DirectiveInfo = {};
field.directives.forEach((directive: DirectiveNode) => {
directiveObj[directive.name.value] = argumentsObjectFromField(
directive,
variables,
);
});
return directiveObj;
}
return null;
}
export function shouldInclude(
selection: SelectionNode,
variables: { [name: string]: any } = {},
): boolean {
return getInclusionDirectives(
selection.directives,
).every(({ directive, ifArgument }) => {
let evaledValue: boolean = false;
if (ifArgument.value.kind === 'Variable') {
evaledValue = variables[(ifArgument.value as VariableNode).name.value];
invariant(
evaledValue !== void 0,
`Invalid variable referenced in @${directive.name.value} directive.`,
);
} else {
evaledValue = (ifArgument.value as BooleanValueNode).value;
}
return directive.name.value === 'skip' ? !evaledValue : evaledValue;
});
}
export function getDirectiveNames(doc: DocumentNode) {
const names: string[] = [];
visit(doc, {
Directive(node) {
names.push(node.name.value);
},
});
return names;
}
export function hasDirectives(names: string[], doc: DocumentNode) {
return getDirectiveNames(doc).some(
(name: string) => names.indexOf(name) > -1,
);
}
export function hasClientExports(document: DocumentNode) {
return (
document &&
hasDirectives(['client'], document) &&
hasDirectives(['export'], document)
);
}
export type InclusionDirectives = Array<{
directive: DirectiveNode;
ifArgument: ArgumentNode;
}>;
function isInclusionDirective({ name: { value } }: DirectiveNode): boolean {
return value === 'skip' || value === 'include';
}
export function getInclusionDirectives(
directives: ReadonlyArray<DirectiveNode>,
): InclusionDirectives {
return directives ? directives.filter(isInclusionDirective).map(directive => {
const directiveArguments = directive.arguments;
const directiveName = directive.name.value;
invariant(
directiveArguments && directiveArguments.length === 1,
`Incorrect number of arguments for the @${directiveName} directive.`,
);
const ifArgument = directiveArguments[0];
invariant(
ifArgument.name && ifArgument.name.value === 'if',
`Invalid argument for the @${directiveName} directive.`,
);
const ifValue: ValueNode = ifArgument.value;
// means it has to be a variable value if this is a valid @skip or @include directive
invariant(
ifValue &&
(ifValue.kind === 'Variable' || ifValue.kind === 'BooleanValue'),
`Argument for the @${directiveName} directive must be a variable or a boolean value.`,
);
return { directive, ifArgument };
}) : [];
}

View File

@@ -0,0 +1,92 @@
import { DocumentNode, FragmentDefinitionNode } from 'graphql';
import { invariant, InvariantError } from 'ts-invariant';
/**
* Returns a query document which adds a single query operation that only
* spreads the target fragment inside of it.
*
* So for example a document of:
*
* ```graphql
* fragment foo on Foo { a b c }
* ```
*
* Turns into:
*
* ```graphql
* { ...foo }
*
* fragment foo on Foo { a b c }
* ```
*
* The target fragment will either be the only fragment in the document, or a
* fragment specified by the provided `fragmentName`. If there is more than one
* fragment, but a `fragmentName` was not defined then an error will be thrown.
*/
export function getFragmentQueryDocument(
document: DocumentNode,
fragmentName?: string,
): DocumentNode {
let actualFragmentName = fragmentName;
// Build an array of all our fragment definitions that will be used for
// validations. We also do some validations on the other definitions in the
// document while building this list.
const fragments: Array<FragmentDefinitionNode> = [];
document.definitions.forEach(definition => {
// Throw an error if we encounter an operation definition because we will
// define our own operation definition later on.
if (definition.kind === 'OperationDefinition') {
throw new InvariantError(
`Found a ${definition.operation} operation${
definition.name ? ` named '${definition.name.value}'` : ''
}. ` +
'No operations are allowed when using a fragment as a query. Only fragments are allowed.',
);
}
// Add our definition to the fragments array if it is a fragment
// definition.
if (definition.kind === 'FragmentDefinition') {
fragments.push(definition);
}
});
// If the user did not give us a fragment name then let us try to get a
// name from a single fragment in the definition.
if (typeof actualFragmentName === 'undefined') {
invariant(
fragments.length === 1,
`Found ${
fragments.length
} fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`,
);
actualFragmentName = fragments[0].name.value;
}
// Generate a query document with an operation that simply spreads the
// fragment inside of it.
const query: DocumentNode = {
...document,
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: {
kind: 'Name',
value: actualFragmentName,
},
},
],
},
},
...document.definitions,
],
};
return query;
}

View File

@@ -0,0 +1,233 @@
import {
DocumentNode,
OperationDefinitionNode,
FragmentDefinitionNode,
ValueNode,
} from 'graphql';
import { invariant, InvariantError } from 'ts-invariant';
import { assign } from './util/assign';
import { valueToObjectRepresentation, JsonValue } from './storeUtils';
export function getMutationDefinition(
doc: DocumentNode,
): OperationDefinitionNode {
checkDocument(doc);
let mutationDef: OperationDefinitionNode | null = doc.definitions.filter(
definition =>
definition.kind === 'OperationDefinition' &&
definition.operation === 'mutation',
)[0] as OperationDefinitionNode;
invariant(mutationDef, 'Must contain a mutation definition.');
return mutationDef;
}
// Checks the document for errors and throws an exception if there is an error.
export function checkDocument(doc: DocumentNode) {
invariant(
doc && doc.kind === 'Document',
`Expecting a parsed GraphQL document. Perhaps you need to wrap the query \
string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`,
);
const operations = doc.definitions
.filter(d => d.kind !== 'FragmentDefinition')
.map(definition => {
if (definition.kind !== 'OperationDefinition') {
throw new InvariantError(
`Schema type definitions not allowed in queries. Found: "${
definition.kind
}"`,
);
}
return definition;
});
invariant(
operations.length <= 1,
`Ambiguous GraphQL document: contains ${operations.length} operations`,
);
return doc;
}
export function getOperationDefinition(
doc: DocumentNode,
): OperationDefinitionNode | undefined {
checkDocument(doc);
return doc.definitions.filter(
definition => definition.kind === 'OperationDefinition',
)[0] as OperationDefinitionNode;
}
export function getOperationDefinitionOrDie(
document: DocumentNode,
): OperationDefinitionNode {
const def = getOperationDefinition(document);
invariant(def, `GraphQL document is missing an operation`);
return def;
}
export function getOperationName(doc: DocumentNode): string | null {
return (
doc.definitions
.filter(
definition =>
definition.kind === 'OperationDefinition' && definition.name,
)
.map((x: OperationDefinitionNode) => x.name.value)[0] || null
);
}
// Returns the FragmentDefinitions from a particular document as an array
export function getFragmentDefinitions(
doc: DocumentNode,
): FragmentDefinitionNode[] {
return doc.definitions.filter(
definition => definition.kind === 'FragmentDefinition',
) as FragmentDefinitionNode[];
}
export function getQueryDefinition(doc: DocumentNode): OperationDefinitionNode {
const queryDef = getOperationDefinition(doc) as OperationDefinitionNode;
invariant(
queryDef && queryDef.operation === 'query',
'Must contain a query definition.',
);
return queryDef;
}
export function getFragmentDefinition(
doc: DocumentNode,
): FragmentDefinitionNode {
invariant(
doc.kind === 'Document',
`Expecting a parsed GraphQL document. Perhaps you need to wrap the query \
string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`,
);
invariant(
doc.definitions.length <= 1,
'Fragment must have exactly one definition.',
);
const fragmentDef = doc.definitions[0] as FragmentDefinitionNode;
invariant(
fragmentDef.kind === 'FragmentDefinition',
'Must be a fragment definition.',
);
return fragmentDef as FragmentDefinitionNode;
}
/**
* Returns the first operation definition found in this document.
* If no operation definition is found, the first fragment definition will be returned.
* If no definitions are found, an error will be thrown.
*/
export function getMainDefinition(
queryDoc: DocumentNode,
): OperationDefinitionNode | FragmentDefinitionNode {
checkDocument(queryDoc);
let fragmentDefinition;
for (let definition of queryDoc.definitions) {
if (definition.kind === 'OperationDefinition') {
const operation = (definition as OperationDefinitionNode).operation;
if (
operation === 'query' ||
operation === 'mutation' ||
operation === 'subscription'
) {
return definition as OperationDefinitionNode;
}
}
if (definition.kind === 'FragmentDefinition' && !fragmentDefinition) {
// we do this because we want to allow multiple fragment definitions
// to precede an operation definition.
fragmentDefinition = definition as FragmentDefinitionNode;
}
}
if (fragmentDefinition) {
return fragmentDefinition;
}
throw new InvariantError(
'Expected a parsed GraphQL query with a query, mutation, subscription, or a fragment.',
);
}
/**
* This is an interface that describes a map from fragment names to fragment definitions.
*/
export interface FragmentMap {
[fragmentName: string]: FragmentDefinitionNode;
}
// Utility function that takes a list of fragment definitions and makes a hash out of them
// that maps the name of the fragment to the fragment definition.
export function createFragmentMap(
fragments: FragmentDefinitionNode[] = [],
): FragmentMap {
const symTable: FragmentMap = {};
fragments.forEach(fragment => {
symTable[fragment.name.value] = fragment;
});
return symTable;
}
export function getDefaultValues(
definition: OperationDefinitionNode | undefined,
): { [key: string]: JsonValue } {
if (
definition &&
definition.variableDefinitions &&
definition.variableDefinitions.length
) {
const defaultValues = definition.variableDefinitions
.filter(({ defaultValue }) => defaultValue)
.map(
({ variable, defaultValue }): { [key: string]: JsonValue } => {
const defaultValueObj: { [key: string]: JsonValue } = {};
valueToObjectRepresentation(
defaultValueObj,
variable.name,
defaultValue as ValueNode,
);
return defaultValueObj;
},
);
return assign({}, ...defaultValues);
}
return {};
}
/**
* Returns the names of all variables declared by the operation.
*/
export function variablesInOperation(
operation: OperationDefinitionNode,
): Set<string> {
const names = new Set<string>();
if (operation.variableDefinitions) {
for (const definition of operation.variableDefinitions) {
names.add(definition.variable.name.value);
}
}
return names;
}

View File

@@ -0,0 +1,220 @@
// @flow
import type {
DirectiveNode,
StringValueNode,
BooleanValueNode,
IntValueNode,
FloatValueNode,
EnumValueNode,
VariableNode,
FieldNode,
FragmentDefinitionNode,
OperationDefinitionNode,
SelectionNode,
DocumentNode,
DefinitionNode,
ValueNode,
NameNode,
} from 'graphql';
declare module 'apollo-utilities' {
// storeUtils
declare export interface IdValue {
type: 'id';
id: string;
generated: boolean;
typename: string | void;
}
declare export interface JsonValue {
type: 'json';
json: any;
}
declare export type ListValue = Array<?IdValue>;
declare export type StoreValue =
| number
| string
| Array<string>
| IdValue
| ListValue
| JsonValue
| null
| void
| Object;
declare export type ScalarValue =
| StringValueNode
| BooleanValueNode
| EnumValueNode;
declare export type NumberValue = IntValueNode | FloatValueNode;
declare type isValueFn = (value: ValueNode) => boolean;
declare export type isScalarValue = isValueFn;
declare export type isNumberValue = isValueFn;
declare export type isStringValue = isValueFn;
declare export type isBooleanValue = isValueFn;
declare export type isIntValue = isValueFn;
declare export type isFloatValue = isValueFn;
declare export type isVariable = isValueFn;
declare export type isObjectValue = isValueFn;
declare export type isListValue = isValueFn;
declare export type isEnumValue = isValueFn;
declare export function valueToObjectRepresentation(
argObj: any,
name: NameNode,
value: ValueNode,
variables?: Object,
): any;
declare export function storeKeyNameFromField(
field: FieldNode,
variables?: Object,
): string;
declare export type Directives = {
[directiveName: string]: {
[argName: string]: any,
},
};
declare export function getStoreKeyName(
fieldName: string,
args?: Object,
directives?: Directives,
): string;
declare export function argumentsObjectFromField(
field: FieldNode | DirectiveNode,
variables: Object,
): ?Object;
declare export function resultKeyNameFromField(field: FieldNode): string;
declare export function isField(selection: SelectionNode): boolean;
declare export function isInlineFragment(selection: SelectionNode): boolean;
declare export function isIdValue(idObject: StoreValue): boolean;
declare export type IdConfig = {
id: string,
typename: string | void,
};
declare export function toIdValue(
id: string | IdConfig,
generated?: boolean,
): IdValue;
declare export function isJsonValue(jsonObject: StoreValue): boolean;
declare export type VariableValue = (node: VariableNode) => any;
declare export function valueFromNode(
node: ValueNode,
onVariable: VariableValue,
): any;
// getFromAST
declare export function getMutationDefinition(
doc: DocumentNode,
): OperationDefinitionNode;
declare export function checkDocument(doc: DocumentNode): void;
declare export function getOperationDefinition(
doc: DocumentNode,
): ?OperationDefinitionNode;
declare export function getOperationDefinitionOrDie(
document: DocumentNode,
): OperationDefinitionNode;
declare export function getOperationName(doc: DocumentNode): ?string;
declare export function getFragmentDefinitions(
doc: DocumentNode,
): Array<FragmentDefinitionNode>;
declare export function getQueryDefinition(
doc: DocumentNode,
): OperationDefinitionNode;
declare export function getFragmentDefinition(
doc: DocumentNode,
): FragmentDefinitionNode;
declare export function getMainDefinition(
queryDoc: DocumentNode,
): OperationDefinitionNode | FragmentDefinitionNode;
declare export interface FragmentMap {
[fragmentName: string]: FragmentDefinitionNode;
}
declare export function createFragmentMap(
fragments: Array<FragmentDefinitionNode>,
): FragmentMap;
declare export function getDefaultValues(
definition: ?OperationDefinitionNode,
): { [key: string]: JsonValue };
// fragments
declare export function getFragmentQueryDocument(
document: DocumentNode,
fragmentName?: string,
): DocumentNode;
declare export function variablesInOperation(
operation: OperationDefinitionNode,
): Set<string>;
// directives
declare type DirectiveInfo = {
[fieldName: string]: { [argName: string]: any },
};
declare export function getDirectiveInfoFromField(
field: FieldNode,
object: Object,
): ?DirectiveInfo;
declare export function shouldInclude(
selection: SelectionNode,
variables: { [name: string]: any },
): boolean;
declare export function flattenSelections(
selection: SelectionNode,
): Array<SelectionNode>;
declare export function getDirectiveNames(doc: DocumentNode): Array<string>;
declare export function hasDirectives(
names: Array<string>,
doc: DocumentNode,
): boolean;
// transform
declare export type RemoveDirectiveConfig = {
name?: string,
test?: (directive: DirectiveNode) => boolean,
};
declare export function removeDirectivesFromDocument(
directives: Array<RemoveDirectiveConfig>,
doc: DocumentNode,
): DocumentNode;
declare export function addTypenameToDocument(doc: DocumentNode): void;
declare export function removeConnectionDirectiveFromDocument(
doc: DocumentNode,
): DocumentNode;
}

View File

@@ -0,0 +1,16 @@
export * from './directives';
export * from './fragments';
export * from './getFromAST';
export * from './transform';
export * from './storeUtils';
export * from './util/assign';
export * from './util/canUse';
export * from './util/cloneDeep';
export * from './util/environment';
export * from './util/errorHandling';
export * from './util/isEqual';
export * from './util/maybeDeepFreeze';
export * from './util/mergeDeep';
export * from './util/warnOnce';
export * from './util/stripSymbols';
export * from './util/mergeDeep';

View File

@@ -0,0 +1,340 @@
import {
DirectiveNode,
FieldNode,
IntValueNode,
FloatValueNode,
StringValueNode,
BooleanValueNode,
ObjectValueNode,
ListValueNode,
EnumValueNode,
NullValueNode,
VariableNode,
InlineFragmentNode,
ValueNode,
SelectionNode,
NameNode,
} from 'graphql';
import stringify from 'fast-json-stable-stringify';
import { InvariantError } from 'ts-invariant';
export interface IdValue {
type: 'id';
id: string;
generated: boolean;
typename: string | undefined;
}
export interface JsonValue {
type: 'json';
json: any;
}
export type ListValue = Array<null | IdValue>;
export type StoreValue =
| number
| string
| string[]
| IdValue
| ListValue
| JsonValue
| null
| undefined
| void
| Object;
export type ScalarValue = StringValueNode | BooleanValueNode | EnumValueNode;
export function isScalarValue(value: ValueNode): value is ScalarValue {
return ['StringValue', 'BooleanValue', 'EnumValue'].indexOf(value.kind) > -1;
}
export type NumberValue = IntValueNode | FloatValueNode;
export function isNumberValue(value: ValueNode): value is NumberValue {
return ['IntValue', 'FloatValue'].indexOf(value.kind) > -1;
}
function isStringValue(value: ValueNode): value is StringValueNode {
return value.kind === 'StringValue';
}
function isBooleanValue(value: ValueNode): value is BooleanValueNode {
return value.kind === 'BooleanValue';
}
function isIntValue(value: ValueNode): value is IntValueNode {
return value.kind === 'IntValue';
}
function isFloatValue(value: ValueNode): value is FloatValueNode {
return value.kind === 'FloatValue';
}
function isVariable(value: ValueNode): value is VariableNode {
return value.kind === 'Variable';
}
function isObjectValue(value: ValueNode): value is ObjectValueNode {
return value.kind === 'ObjectValue';
}
function isListValue(value: ValueNode): value is ListValueNode {
return value.kind === 'ListValue';
}
function isEnumValue(value: ValueNode): value is EnumValueNode {
return value.kind === 'EnumValue';
}
function isNullValue(value: ValueNode): value is NullValueNode {
return value.kind === 'NullValue';
}
export function valueToObjectRepresentation(
argObj: any,
name: NameNode,
value: ValueNode,
variables?: Object,
) {
if (isIntValue(value) || isFloatValue(value)) {
argObj[name.value] = Number(value.value);
} else if (isBooleanValue(value) || isStringValue(value)) {
argObj[name.value] = value.value;
} else if (isObjectValue(value)) {
const nestedArgObj = {};
value.fields.map(obj =>
valueToObjectRepresentation(nestedArgObj, obj.name, obj.value, variables),
);
argObj[name.value] = nestedArgObj;
} else if (isVariable(value)) {
const variableValue = (variables || ({} as any))[value.name.value];
argObj[name.value] = variableValue;
} else if (isListValue(value)) {
argObj[name.value] = value.values.map(listValue => {
const nestedArgArrayObj = {};
valueToObjectRepresentation(
nestedArgArrayObj,
name,
listValue,
variables,
);
return (nestedArgArrayObj as any)[name.value];
});
} else if (isEnumValue(value)) {
argObj[name.value] = (value as EnumValueNode).value;
} else if (isNullValue(value)) {
argObj[name.value] = null;
} else {
throw new InvariantError(
`The inline argument "${name.value}" of kind "${(value as any).kind}"` +
'is not supported. Use variables instead of inline arguments to ' +
'overcome this limitation.',
);
}
}
export function storeKeyNameFromField(
field: FieldNode,
variables?: Object,
): string {
let directivesObj: any = null;
if (field.directives) {
directivesObj = {};
field.directives.forEach(directive => {
directivesObj[directive.name.value] = {};
if (directive.arguments) {
directive.arguments.forEach(({ name, value }) =>
valueToObjectRepresentation(
directivesObj[directive.name.value],
name,
value,
variables,
),
);
}
});
}
let argObj: any = null;
if (field.arguments && field.arguments.length) {
argObj = {};
field.arguments.forEach(({ name, value }) =>
valueToObjectRepresentation(argObj, name, value, variables),
);
}
return getStoreKeyName(field.name.value, argObj, directivesObj);
}
export type Directives = {
[directiveName: string]: {
[argName: string]: any;
};
};
const KNOWN_DIRECTIVES: string[] = [
'connection',
'include',
'skip',
'client',
'rest',
'export',
];
export function getStoreKeyName(
fieldName: string,
args?: Object,
directives?: Directives,
): string {
if (
directives &&
directives['connection'] &&
directives['connection']['key']
) {
if (
directives['connection']['filter'] &&
(directives['connection']['filter'] as string[]).length > 0
) {
const filterKeys = directives['connection']['filter']
? (directives['connection']['filter'] as string[])
: [];
filterKeys.sort();
const queryArgs = args as { [key: string]: any };
const filteredArgs = {} as { [key: string]: any };
filterKeys.forEach(key => {
filteredArgs[key] = queryArgs[key];
});
return `${directives['connection']['key']}(${JSON.stringify(
filteredArgs,
)})`;
} else {
return directives['connection']['key'];
}
}
let completeFieldName: string = fieldName;
if (args) {
// We can't use `JSON.stringify` here since it's non-deterministic,
// and can lead to different store key names being created even though
// the `args` object used during creation has the same properties/values.
const stringifiedArgs: string = stringify(args);
completeFieldName += `(${stringifiedArgs})`;
}
if (directives) {
Object.keys(directives).forEach(key => {
if (KNOWN_DIRECTIVES.indexOf(key) !== -1) return;
if (directives[key] && Object.keys(directives[key]).length) {
completeFieldName += `@${key}(${JSON.stringify(directives[key])})`;
} else {
completeFieldName += `@${key}`;
}
});
}
return completeFieldName;
}
export function argumentsObjectFromField(
field: FieldNode | DirectiveNode,
variables: Object,
): Object {
if (field.arguments && field.arguments.length) {
const argObj: Object = {};
field.arguments.forEach(({ name, value }) =>
valueToObjectRepresentation(argObj, name, value, variables),
);
return argObj;
}
return null;
}
export function resultKeyNameFromField(field: FieldNode): string {
return field.alias ? field.alias.value : field.name.value;
}
export function isField(selection: SelectionNode): selection is FieldNode {
return selection.kind === 'Field';
}
export function isInlineFragment(
selection: SelectionNode,
): selection is InlineFragmentNode {
return selection.kind === 'InlineFragment';
}
export function isIdValue(idObject: StoreValue): idObject is IdValue {
return idObject &&
(idObject as IdValue | JsonValue).type === 'id' &&
typeof (idObject as IdValue).generated === 'boolean';
}
export type IdConfig = {
id: string;
typename: string | undefined;
};
export function toIdValue(
idConfig: string | IdConfig,
generated = false,
): IdValue {
return {
type: 'id',
generated,
...(typeof idConfig === 'string'
? { id: idConfig, typename: undefined }
: idConfig),
};
}
export function isJsonValue(jsonObject: StoreValue): jsonObject is JsonValue {
return (
jsonObject != null &&
typeof jsonObject === 'object' &&
(jsonObject as IdValue | JsonValue).type === 'json'
);
}
function defaultValueFromVariable(node: VariableNode) {
throw new InvariantError(`Variable nodes are not supported by valueFromNode`);
}
export type VariableValue = (node: VariableNode) => any;
/**
* Evaluate a ValueNode and yield its value in its natural JS form.
*/
export function valueFromNode(
node: ValueNode,
onVariable: VariableValue = defaultValueFromVariable,
): any {
switch (node.kind) {
case 'Variable':
return onVariable(node);
case 'NullValue':
return null;
case 'IntValue':
return parseInt(node.value, 10);
case 'FloatValue':
return parseFloat(node.value);
case 'ListValue':
return node.values.map(v => valueFromNode(v, onVariable));
case 'ObjectValue': {
const value: { [key: string]: any } = {};
for (const field of node.fields) {
value[field.name.value] = valueFromNode(field.value, onVariable);
}
return value;
}
default:
return node.value;
}
}

View File

@@ -0,0 +1,542 @@
import {
DocumentNode,
SelectionNode,
SelectionSetNode,
OperationDefinitionNode,
FieldNode,
DirectiveNode,
FragmentDefinitionNode,
ArgumentNode,
FragmentSpreadNode,
VariableDefinitionNode,
VariableNode,
} from 'graphql';
import { visit } from 'graphql/language/visitor';
import {
checkDocument,
getOperationDefinition,
getFragmentDefinition,
getFragmentDefinitions,
createFragmentMap,
FragmentMap,
getMainDefinition,
} from './getFromAST';
import { filterInPlace } from './util/filterInPlace';
import { invariant } from 'ts-invariant';
import { isField, isInlineFragment } from './storeUtils';
export type RemoveNodeConfig<N> = {
name?: string;
test?: (node: N) => boolean;
remove?: boolean;
};
export type GetNodeConfig<N> = {
name?: string;
test?: (node: N) => boolean;
};
export type RemoveDirectiveConfig = RemoveNodeConfig<DirectiveNode>;
export type GetDirectiveConfig = GetNodeConfig<DirectiveNode>;
export type RemoveArgumentsConfig = RemoveNodeConfig<ArgumentNode>;
export type GetFragmentSpreadConfig = GetNodeConfig<FragmentSpreadNode>;
export type RemoveFragmentSpreadConfig = RemoveNodeConfig<FragmentSpreadNode>;
export type RemoveFragmentDefinitionConfig = RemoveNodeConfig<
FragmentDefinitionNode
>;
export type RemoveVariableDefinitionConfig = RemoveNodeConfig<
VariableDefinitionNode
>;
const TYPENAME_FIELD: FieldNode = {
kind: 'Field',
name: {
kind: 'Name',
value: '__typename',
},
};
function isEmpty(
op: OperationDefinitionNode | FragmentDefinitionNode,
fragments: FragmentMap,
): boolean {
return op.selectionSet.selections.every(
selection =>
selection.kind === 'FragmentSpread' &&
isEmpty(fragments[selection.name.value], fragments),
);
}
function nullIfDocIsEmpty(doc: DocumentNode) {
return isEmpty(
getOperationDefinition(doc) || getFragmentDefinition(doc),
createFragmentMap(getFragmentDefinitions(doc)),
)
? null
: doc;
}
function getDirectiveMatcher(
directives: (RemoveDirectiveConfig | GetDirectiveConfig)[],
) {
return function directiveMatcher(directive: DirectiveNode) {
return directives.some(
dir =>
(dir.name && dir.name === directive.name.value) ||
(dir.test && dir.test(directive)),
);
};
}
export function removeDirectivesFromDocument(
directives: RemoveDirectiveConfig[],
doc: DocumentNode,
): DocumentNode | null {
const variablesInUse: Record<string, boolean> = Object.create(null);
let variablesToRemove: RemoveArgumentsConfig[] = [];
const fragmentSpreadsInUse: Record<string, boolean> = Object.create(null);
let fragmentSpreadsToRemove: RemoveFragmentSpreadConfig[] = [];
let modifiedDoc = nullIfDocIsEmpty(
visit(doc, {
Variable: {
enter(node, _key, parent) {
// Store each variable that's referenced as part of an argument
// (excluding operation definition variables), so we know which
// variables are being used. If we later want to remove a variable
// we'll fist check to see if it's being used, before continuing with
// the removal.
if (
(parent as VariableDefinitionNode).kind !== 'VariableDefinition'
) {
variablesInUse[node.name.value] = true;
}
},
},
Field: {
enter(node) {
if (directives && node.directives) {
// If `remove` is set to true for a directive, and a directive match
// is found for a field, remove the field as well.
const shouldRemoveField = directives.some(
directive => directive.remove,
);
if (
shouldRemoveField &&
node.directives &&
node.directives.some(getDirectiveMatcher(directives))
) {
if (node.arguments) {
// Store field argument variables so they can be removed
// from the operation definition.
node.arguments.forEach(arg => {
if (arg.value.kind === 'Variable') {
variablesToRemove.push({
name: (arg.value as VariableNode).name.value,
});
}
});
}
if (node.selectionSet) {
// Store fragment spread names so they can be removed from the
// docuemnt.
getAllFragmentSpreadsFromSelectionSet(node.selectionSet).forEach(
frag => {
fragmentSpreadsToRemove.push({
name: frag.name.value,
});
},
);
}
// Remove the field.
return null;
}
}
},
},
FragmentSpread: {
enter(node) {
// Keep track of referenced fragment spreads. This is used to
// determine if top level fragment definitions should be removed.
fragmentSpreadsInUse[node.name.value] = true;
},
},
Directive: {
enter(node) {
// If a matching directive is found, remove it.
if (getDirectiveMatcher(directives)(node)) {
return null;
}
},
},
}),
);
// If we've removed fields with arguments, make sure the associated
// variables are also removed from the rest of the document, as long as they
// aren't being used elsewhere.
if (
modifiedDoc &&
filterInPlace(variablesToRemove, v => !variablesInUse[v.name]).length
) {
modifiedDoc = removeArgumentsFromDocument(variablesToRemove, modifiedDoc);
}
// If we've removed selection sets with fragment spreads, make sure the
// associated fragment definitions are also removed from the rest of the
// document, as long as they aren't being used elsewhere.
if (
modifiedDoc &&
filterInPlace(fragmentSpreadsToRemove, fs => !fragmentSpreadsInUse[fs.name])
.length
) {
modifiedDoc = removeFragmentSpreadFromDocument(
fragmentSpreadsToRemove,
modifiedDoc,
);
}
return modifiedDoc;
}
export function addTypenameToDocument(doc: DocumentNode): DocumentNode {
return visit(checkDocument(doc), {
SelectionSet: {
enter(node, _key, parent) {
// Don't add __typename to OperationDefinitions.
if (
parent &&
(parent as OperationDefinitionNode).kind === 'OperationDefinition'
) {
return;
}
// No changes if no selections.
const { selections } = node;
if (!selections) {
return;
}
// If selections already have a __typename, or are part of an
// introspection query, do nothing.
const skip = selections.some(selection => {
return (
isField(selection) &&
(selection.name.value === '__typename' ||
selection.name.value.lastIndexOf('__', 0) === 0)
);
});
if (skip) {
return;
}
// If this SelectionSet is @export-ed as an input variable, it should
// not have a __typename field (see issue #4691).
const field = parent as FieldNode;
if (
isField(field) &&
field.directives &&
field.directives.some(d => d.name.value === 'export')
) {
return;
}
// Create and return a new SelectionSet with a __typename Field.
return {
...node,
selections: [...selections, TYPENAME_FIELD],
};
},
},
});
}
const connectionRemoveConfig = {
test: (directive: DirectiveNode) => {
const willRemove = directive.name.value === 'connection';
if (willRemove) {
if (
!directive.arguments ||
!directive.arguments.some(arg => arg.name.value === 'key')
) {
invariant.warn(
'Removing an @connection directive even though it does not have a key. ' +
'You may want to use the key parameter to specify a store key.',
);
}
}
return willRemove;
},
};
export function removeConnectionDirectiveFromDocument(doc: DocumentNode) {
return removeDirectivesFromDocument(
[connectionRemoveConfig],
checkDocument(doc),
);
}
function hasDirectivesInSelectionSet(
directives: GetDirectiveConfig[],
selectionSet: SelectionSetNode,
nestedCheck = true,
): boolean {
return (
selectionSet &&
selectionSet.selections &&
selectionSet.selections.some(selection =>
hasDirectivesInSelection(directives, selection, nestedCheck),
)
);
}
function hasDirectivesInSelection(
directives: GetDirectiveConfig[],
selection: SelectionNode,
nestedCheck = true,
): boolean {
if (!isField(selection)) {
return true;
}
if (!selection.directives) {
return false;
}
return (
selection.directives.some(getDirectiveMatcher(directives)) ||
(nestedCheck &&
hasDirectivesInSelectionSet(
directives,
selection.selectionSet,
nestedCheck,
))
);
}
export function getDirectivesFromDocument(
directives: GetDirectiveConfig[],
doc: DocumentNode,
): DocumentNode {
checkDocument(doc);
let parentPath: string;
return nullIfDocIsEmpty(
visit(doc, {
SelectionSet: {
enter(node, _key, _parent, path) {
const currentPath = path.join('-');
if (
!parentPath ||
currentPath === parentPath ||
!currentPath.startsWith(parentPath)
) {
if (node.selections) {
const selectionsWithDirectives = node.selections.filter(
selection => hasDirectivesInSelection(directives, selection),
);
if (hasDirectivesInSelectionSet(directives, node, false)) {
parentPath = currentPath;
}
return {
...node,
selections: selectionsWithDirectives,
};
} else {
return null;
}
}
},
},
}),
);
}
function getArgumentMatcher(config: RemoveArgumentsConfig[]) {
return function argumentMatcher(argument: ArgumentNode) {
return config.some(
(aConfig: RemoveArgumentsConfig) =>
argument.value &&
argument.value.kind === 'Variable' &&
argument.value.name &&
(aConfig.name === argument.value.name.value ||
(aConfig.test && aConfig.test(argument))),
);
};
}
export function removeArgumentsFromDocument(
config: RemoveArgumentsConfig[],
doc: DocumentNode,
): DocumentNode {
const argMatcher = getArgumentMatcher(config);
return nullIfDocIsEmpty(
visit(doc, {
OperationDefinition: {
enter(node) {
return {
...node,
// Remove matching top level variables definitions.
variableDefinitions: node.variableDefinitions.filter(
varDef =>
!config.some(arg => arg.name === varDef.variable.name.value),
),
};
},
},
Field: {
enter(node) {
// If `remove` is set to true for an argument, and an argument match
// is found for a field, remove the field as well.
const shouldRemoveField = config.some(argConfig => argConfig.remove);
if (shouldRemoveField) {
let argMatchCount = 0;
node.arguments.forEach(arg => {
if (argMatcher(arg)) {
argMatchCount += 1;
}
});
if (argMatchCount === 1) {
return null;
}
}
},
},
Argument: {
enter(node) {
// Remove all matching arguments.
if (argMatcher(node)) {
return null;
}
},
},
}),
);
}
export function removeFragmentSpreadFromDocument(
config: RemoveFragmentSpreadConfig[],
doc: DocumentNode,
): DocumentNode {
function enter(
node: FragmentSpreadNode | FragmentDefinitionNode,
): null | void {
if (config.some(def => def.name === node.name.value)) {
return null;
}
}
return nullIfDocIsEmpty(
visit(doc, {
FragmentSpread: { enter },
FragmentDefinition: { enter },
}),
);
}
function getAllFragmentSpreadsFromSelectionSet(
selectionSet: SelectionSetNode,
): FragmentSpreadNode[] {
const allFragments: FragmentSpreadNode[] = [];
selectionSet.selections.forEach(selection => {
if (
(isField(selection) || isInlineFragment(selection)) &&
selection.selectionSet
) {
getAllFragmentSpreadsFromSelectionSet(selection.selectionSet).forEach(
frag => allFragments.push(frag),
);
} else if (selection.kind === 'FragmentSpread') {
allFragments.push(selection);
}
});
return allFragments;
}
// If the incoming document is a query, return it as is. Otherwise, build a
// new document containing a query operation based on the selection set
// of the previous main operation.
export function buildQueryFromSelectionSet(
document: DocumentNode,
): DocumentNode {
const definition = getMainDefinition(document);
const definitionOperation = (<OperationDefinitionNode>definition).operation;
if (definitionOperation === 'query') {
// Already a query, so return the existing document.
return document;
}
// Build a new query using the selection set of the main operation.
const modifiedDoc = visit(document, {
OperationDefinition: {
enter(node) {
return {
...node,
operation: 'query',
};
},
},
});
return modifiedDoc;
}
// Remove fields / selection sets that include an @client directive.
export function removeClientSetsFromDocument(
document: DocumentNode,
): DocumentNode | null {
checkDocument(document);
let modifiedDoc = removeDirectivesFromDocument(
[
{
test: (directive: DirectiveNode) => directive.name.value === 'client',
remove: true,
},
],
document,
);
// After a fragment definition has had its @client related document
// sets removed, if the only field it has left is a __typename field,
// remove the entire fragment operation to prevent it from being fired
// on the server.
if (modifiedDoc) {
modifiedDoc = visit(modifiedDoc, {
FragmentDefinition: {
enter(node) {
if (node.selectionSet) {
const isTypenameOnly = node.selectionSet.selections.every(
selection =>
isField(selection) && selection.name.value === '__typename',
);
if (isTypenameOnly) {
return null;
}
}
},
},
});
}
return modifiedDoc;
}

View File

@@ -0,0 +1,47 @@
import { assign } from '../assign';
describe('assign', () => {
it('will merge many objects together', () => {
expect(assign({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
expect(assign({ a: 1 }, { b: 2 }, { c: 3 })).toEqual({
a: 1,
b: 2,
c: 3,
});
expect(assign({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 })).toEqual({
a: 1,
b: 2,
c: 3,
d: 4,
});
});
it('will merge many objects together shallowly', () => {
expect(assign({ x: { a: 1 } }, { x: { b: 2 } })).toEqual({ x: { b: 2 } });
expect(assign({ x: { a: 1 } }, { x: { b: 2 } }, { x: { c: 3 } })).toEqual({
x: { c: 3 },
});
expect(
assign(
{ x: { a: 1 } },
{ x: { b: 2 } },
{ x: { c: 3 } },
{ x: { d: 4 } },
),
).toEqual({ x: { d: 4 } });
});
it('will mutate and return the source objects', () => {
const source1 = { a: 1 };
const source2 = { a: 1 };
const source3 = { a: 1 };
expect(assign(source1, { b: 2 })).toEqual(source1);
expect(assign(source2, { b: 2 }, { c: 3 })).toEqual(source2);
expect(assign(source3, { b: 2 }, { c: 3 }, { d: 4 })).toEqual(source3);
expect(source1).toEqual({ a: 1, b: 2 });
expect(source2).toEqual({ a: 1, b: 2, c: 3 });
expect(source3).toEqual({ a: 1, b: 2, c: 3, d: 4 });
});
});

View File

@@ -0,0 +1,70 @@
import { cloneDeep } from '../cloneDeep';
describe('cloneDeep', () => {
it('will clone primitive values', () => {
expect(cloneDeep(undefined)).toEqual(undefined);
expect(cloneDeep(null)).toEqual(null);
expect(cloneDeep(true)).toEqual(true);
expect(cloneDeep(false)).toEqual(false);
expect(cloneDeep(-1)).toEqual(-1);
expect(cloneDeep(+1)).toEqual(+1);
expect(cloneDeep(0.5)).toEqual(0.5);
expect(cloneDeep('hello')).toEqual('hello');
expect(cloneDeep('world')).toEqual('world');
});
it('will clone objects', () => {
const value1 = {};
const value2 = { a: 1, b: 2, c: 3 };
const value3 = { x: { a: 1, b: 2, c: 3 }, y: { a: 1, b: 2, c: 3 } };
const clonedValue1 = cloneDeep(value1);
const clonedValue2 = cloneDeep(value2);
const clonedValue3 = cloneDeep(value3);
expect(clonedValue1).toEqual(value1);
expect(clonedValue2).toEqual(value2);
expect(clonedValue3).toEqual(value3);
expect(clonedValue1).toEqual(value1);
expect(clonedValue2).toEqual(value2);
expect(clonedValue3).toEqual(value3);
expect(clonedValue3.x).toEqual(value3.x);
expect(clonedValue3.y).toEqual(value3.y);
});
it('will clone arrays', () => {
const value1: Array<number> = [];
const value2 = [1, 2, 3];
const value3 = [[1, 2, 3], [1, 2, 3]];
const clonedValue1 = cloneDeep(value1);
const clonedValue2 = cloneDeep(value2);
const clonedValue3 = cloneDeep(value3);
expect(clonedValue1).toEqual(value1);
expect(clonedValue2).toEqual(value2);
expect(clonedValue3).toEqual(value3);
expect(clonedValue1).toEqual(value1);
expect(clonedValue2).toEqual(value2);
expect(clonedValue3).toEqual(value3);
expect(clonedValue3[0]).toEqual(value3[0]);
expect(clonedValue3[1]).toEqual(value3[1]);
});
it('should not attempt to follow circular references', () => {
const someObject = {
prop1: 'value1',
anotherObject: null,
};
const anotherObject = {
someObject,
};
someObject.anotherObject = anotherObject;
let chk;
expect(() => {
chk = cloneDeep(someObject);
}).not.toThrow();
});
});

View File

@@ -0,0 +1,70 @@
import { isEnv, isProduction, isDevelopment, isTest } from '../environment';
describe('environment', () => {
let keepEnv: string | undefined;
beforeEach(() => {
// save the NODE_ENV
keepEnv = process.env.NODE_ENV;
});
afterEach(() => {
// restore the NODE_ENV
process.env.NODE_ENV = keepEnv;
});
describe('isEnv', () => {
it(`should match when there's a value`, () => {
['production', 'development', 'test'].forEach(env => {
process.env.NODE_ENV = env;
expect(isEnv(env)).toBe(true);
});
});
it(`should treat no proces.env.NODE_ENV as it'd be in development`, () => {
delete process.env.NODE_ENV;
expect(isEnv('development')).toBe(true);
});
});
describe('isProduction', () => {
it('should return true if in production', () => {
process.env.NODE_ENV = 'production';
expect(isProduction()).toBe(true);
});
it('should return false if not in production', () => {
process.env.NODE_ENV = 'test';
expect(!isProduction()).toBe(true);
});
});
describe('isTest', () => {
it('should return true if in test', () => {
process.env.NODE_ENV = 'test';
expect(isTest()).toBe(true);
});
it('should return true if not in test', () => {
process.env.NODE_ENV = 'development';
expect(!isTest()).toBe(true);
});
});
describe('isDevelopment', () => {
it('should return true if in development', () => {
process.env.NODE_ENV = 'development';
expect(isDevelopment()).toBe(true);
});
it('should return true if not in development and environment is defined', () => {
process.env.NODE_ENV = 'test';
expect(!isDevelopment()).toBe(true);
});
it('should make development as the default environment', () => {
delete process.env.NODE_ENV;
expect(isDevelopment()).toBe(true);
});
});
});

View File

@@ -0,0 +1,174 @@
import { isEqual } from '../isEqual';
describe('isEqual', () => {
it('should return true for equal primitive values', () => {
expect(isEqual(undefined, undefined)).toBe(true);
expect(isEqual(null, null)).toBe(true);
expect(isEqual(true, true)).toBe(true);
expect(isEqual(false, false)).toBe(true);
expect(isEqual(-1, -1)).toBe(true);
expect(isEqual(+1, +1)).toBe(true);
expect(isEqual(42, 42)).toBe(true);
expect(isEqual(0, 0)).toBe(true);
expect(isEqual(0.5, 0.5)).toBe(true);
expect(isEqual('hello', 'hello')).toBe(true);
expect(isEqual('world', 'world')).toBe(true);
});
it('should return false for not equal primitive values', () => {
expect(!isEqual(undefined, null)).toBe(true);
expect(!isEqual(null, undefined)).toBe(true);
expect(!isEqual(true, false)).toBe(true);
expect(!isEqual(false, true)).toBe(true);
expect(!isEqual(-1, +1)).toBe(true);
expect(!isEqual(+1, -1)).toBe(true);
expect(!isEqual(42, 42.00000000000001)).toBe(true);
expect(!isEqual(0, 0.5)).toBe(true);
expect(!isEqual('hello', 'world')).toBe(true);
expect(!isEqual('world', 'hello')).toBe(true);
});
it('should return false when comparing primitives with objects', () => {
expect(!isEqual({}, null)).toBe(true);
expect(!isEqual(null, {})).toBe(true);
expect(!isEqual({}, true)).toBe(true);
expect(!isEqual(true, {})).toBe(true);
expect(!isEqual({}, 42)).toBe(true);
expect(!isEqual(42, {})).toBe(true);
expect(!isEqual({}, 'hello')).toBe(true);
expect(!isEqual('hello', {})).toBe(true);
});
it('should correctly compare shallow objects', () => {
expect(isEqual({}, {})).toBe(true);
expect(isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true);
expect(!isEqual({ a: 1, b: 2, c: 3 }, { a: 3, b: 2, c: 1 })).toBe(true);
expect(!isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(true);
expect(!isEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(true);
});
it('should correctly compare deep objects', () => {
expect(isEqual({ x: {} }, { x: {} })).toBe(true);
expect(
isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2, c: 3 } }),
).toBe(true);
expect(
!isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 3, b: 2, c: 1 } }),
).toBe(true);
expect(!isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2 } })).toBe(
true,
);
expect(!isEqual({ x: { a: 1, b: 2 } }, { x: { a: 1, b: 2, c: 3 } })).toBe(
true,
);
});
it('should correctly compare deep objects without object prototype ', () => {
// Solves https://github.com/apollographql/apollo-client/issues/2132
const objNoProto = Object.create(null);
objNoProto.a = { b: 2, c: [3, 4] };
objNoProto.e = Object.create(null);
objNoProto.e.f = 5;
expect(isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: { f: 5 } })).toBe(
true,
);
expect(!isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: { f: 6 } })).toBe(
true,
);
expect(!isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: null })).toBe(
true,
);
expect(!isEqual(objNoProto, { a: { b: 2, c: [3] }, e: { f: 5 } })).toBe(
true,
);
expect(!isEqual(objNoProto, null)).toBe(true);
});
it('should correctly handle modified prototypes', () => {
Array.prototype.foo = null;
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
expect(!isEqual([1, 2, 3], [1, 2, 4])).toBe(true);
delete Array.prototype.foo;
});
describe('comparing objects with circular refs', () => {
// copied with slight modification from lodash test suite
it('should compare objects with circular references', () => {
const object1 = {},
object2 = {};
object1.a = object1;
object2.a = object2;
expect(isEqual(object1, object2)).toBe(true);
object1.b = 0;
object2.b = Object(0);
expect(isEqual(object1, object2)).toBe(true);
object1.c = Object(1);
object2.c = Object(2);
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2, c: 3 };
object1.b = object1;
object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 };
expect(isEqual(object1, object2)).toBe(false);
});
it('should have transitive equivalence for circular references of objects', () => {
const object1 = {},
object2 = { a: object1 },
object3 = { a: object2 };
object1.a = object1;
expect(isEqual(object1, object2)).toBe(true);
expect(isEqual(object2, object3)).toBe(true);
expect(isEqual(object1, object3)).toBe(true);
});
it('should compare objects with multiple circular references', () => {
const array1 = [{}],
array2 = [{}];
(array1[0].a = array1).push(array1);
(array2[0].a = array2).push(array2);
expect(isEqual(array1, array2)).toBe(true);
array1[0].b = 0;
array2[0].b = Object(0);
expect(isEqual(array1, array2)).toBe(true);
array1[0].c = Object(1);
array2[0].c = Object(2);
expect(isEqual(array1, array2)).toBe(false);
});
it('should compare objects with complex circular references', () => {
const object1 = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
const object2 = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
object1.foo.b.c.d = object1;
object1.bar.b = object1.foo.b;
object2.foo.b.c.d = object2;
object2.bar.b = object2.foo.b;
expect(isEqual(object1, object2)).toBe(true);
});
});
});

View File

@@ -0,0 +1,17 @@
import { maybeDeepFreeze } from '../maybeDeepFreeze';
describe('maybeDeepFreeze', () => {
it('should deep freeze', () => {
const foo: any = { bar: undefined };
maybeDeepFreeze(foo);
expect(() => (foo.bar = 1)).toThrow();
expect(foo.bar).toBeUndefined();
});
it('should properly freeze objects without hasOwnProperty', () => {
const foo = Object.create(null);
foo.bar = undefined;
maybeDeepFreeze(foo);
expect(() => (foo.bar = 1)).toThrow();
});
});

View File

@@ -0,0 +1,139 @@
import { mergeDeep, mergeDeepArray } from '../mergeDeep';
describe('mergeDeep', function() {
it('should return an object if first argument falsy', function() {
expect(mergeDeep()).toEqual({});
expect(mergeDeep(null)).toEqual({});
expect(mergeDeep(null, { foo: 42 })).toEqual({ foo: 42 });
});
it('should preserve identity for single arguments', function() {
const arg = Object.create(null);
expect(mergeDeep(arg)).toBe(arg);
});
it('should preserve identity when merging non-conflicting objects', function() {
const a = { a: { name: 'ay' } };
const b = { b: { name: 'bee' } };
const c = mergeDeep(a, b);
expect(c.a).toBe(a.a);
expect(c.b).toBe(b.b);
expect(c).toEqual({
a: { name: 'ay' },
b: { name: 'bee' },
});
});
it('should shallow-copy conflicting fields', function() {
const a = { conflict: { fromA: [1, 2, 3] } };
const b = { conflict: { fromB: [4, 5] } };
const c = mergeDeep(a, b);
expect(c.conflict).not.toBe(a.conflict);
expect(c.conflict).not.toBe(b.conflict);
expect(c.conflict.fromA).toBe(a.conflict.fromA);
expect(c.conflict.fromB).toBe(b.conflict.fromB);
expect(c).toEqual({
conflict: {
fromA: [1, 2, 3],
fromB: [4, 5],
},
});
});
it('should resolve conflicts among more than two objects', function() {
const sources = [];
for (let i = 0; i < 100; ++i) {
sources.push({
['unique' + i]: { value: i },
conflict: {
['from' + i]: { value: i },
nested: {
['nested' + i]: { value: i },
},
},
});
}
const merged = mergeDeep(...sources);
sources.forEach((source, i) => {
expect(merged['unique' + i].value).toBe(i);
expect(source['unique' + i]).toBe(merged['unique' + i]);
expect(merged.conflict).not.toBe(source.conflict);
expect(merged.conflict['from' + i].value).toBe(i);
expect(merged.conflict['from' + i]).toBe(source.conflict['from' + i]);
expect(merged.conflict.nested).not.toBe(source.conflict.nested);
expect(merged.conflict.nested['nested' + i].value).toBe(i);
expect(merged.conflict.nested['nested' + i]).toBe(
source.conflict.nested['nested' + i],
);
});
});
it('can merge array elements', function() {
const a = [{ a: 1 }, { a: 'ay' }, 'a'];
const b = [{ b: 2 }, { b: 'bee' }, 'b'];
const c = [{ c: 3 }, { c: 'cee' }, 'c'];
const d = { 1: { d: 'dee' } };
expect(mergeDeep(a, b, c, d)).toEqual([
{ a: 1, b: 2, c: 3 },
{ a: 'ay', b: 'bee', c: 'cee', d: 'dee' },
'c',
]);
});
it('lets the last conflicting value win', function() {
expect(mergeDeep('a', 'b', 'c')).toBe('c');
expect(
mergeDeep(
{ a: 'a', conflict: 1 },
{ b: 'b', conflict: 2 },
{ c: 'c', conflict: 3 },
),
).toEqual({
a: 'a',
b: 'b',
c: 'c',
conflict: 3,
});
expect(mergeDeep(
['a', ['b', 'c'], 'd'],
[/*empty*/, ['B'], 'D'],
)).toEqual(
['a', ['B', 'c'], 'D'],
);
expect(mergeDeep(
['a', ['b', 'c'], 'd'],
['A', [/*empty*/, 'C']],
)).toEqual(
['A', ['b', 'C'], 'd'],
);
});
it('mergeDeep returns the intersection of its argument types', function() {
const abc = mergeDeep({ str: "hi", a: 1 }, { a: 3, b: 2 }, { b: 1, c: 2 });
// The point of this test is that the following lines type-check without
// resorting to any `any` loopholes:
expect(abc.str.slice(0)).toBe("hi");
expect(abc.a * 2).toBe(6);
expect(abc.b - 0).toBe(1);
expect(abc.c / 2).toBe(1);
});
it('mergeDeepArray returns the supertype of its argument types', function() {
class F {
check() { return "ok" };
}
const fs: F[] = [new F, new F, new F];
// Although mergeDeepArray doesn't have the same tuple type awareness as
// mergeDeep, it does infer that F should be the return type here:
expect(mergeDeepArray(fs).check()).toBe("ok");
});
});

View File

@@ -0,0 +1,15 @@
import { stripSymbols } from '../stripSymbols';
interface SymbolConstructor {
(description?: string | number): symbol;
}
declare const Symbol: SymbolConstructor;
describe('stripSymbols', () => {
it('should strip symbols (only)', () => {
const sym = Symbol('id');
const data = { foo: 'bar', [sym]: 'ROOT_QUERY' };
expect(stripSymbols(data)).toEqual({ foo: 'bar' });
});
});

View File

@@ -0,0 +1,61 @@
import { warnOnceInDevelopment } from '../warnOnce';
let lastWarning: string | null;
let keepEnv: string | undefined;
let numCalls = 0;
let oldConsoleWarn: any;
describe('warnOnce', () => {
beforeEach(() => {
keepEnv = process.env.NODE_ENV;
numCalls = 0;
lastWarning = null;
oldConsoleWarn = console.warn;
console.warn = (msg: any) => {
numCalls++;
lastWarning = msg;
};
});
afterEach(() => {
process.env.NODE_ENV = keepEnv;
console.warn = oldConsoleWarn;
});
it('actually warns', () => {
process.env.NODE_ENV = 'development';
warnOnceInDevelopment('hi');
expect(lastWarning).toBe('hi');
expect(numCalls).toEqual(1);
});
it('does not warn twice', () => {
process.env.NODE_ENV = 'development';
warnOnceInDevelopment('ho');
warnOnceInDevelopment('ho');
expect(lastWarning).toEqual('ho');
expect(numCalls).toEqual(1);
});
it('warns two different things once each', () => {
process.env.NODE_ENV = 'development';
warnOnceInDevelopment('slow');
expect(lastWarning).toEqual('slow');
warnOnceInDevelopment('mo');
expect(lastWarning).toEqual('mo');
expect(numCalls).toEqual(2);
});
it('does not warn in production', () => {
process.env.NODE_ENV = 'production';
warnOnceInDevelopment('lo');
warnOnceInDevelopment('lo');
expect(numCalls).toEqual(0);
});
it('warns many times in test', () => {
process.env.NODE_ENV = 'test';
warnOnceInDevelopment('yo');
warnOnceInDevelopment('yo');
expect(lastWarning).toEqual('yo');
expect(numCalls).toEqual(2);
});
});

View File

@@ -0,0 +1,31 @@
/**
* Adds the properties of one or more source objects to a target object. Works exactly like
* `Object.assign`, but as a utility to maintain support for IE 11.
*
* @see https://github.com/apollostack/apollo-client/pull/1009
*/
export function assign<A, B>(a: A, b: B): A & B;
export function assign<A, B, C>(a: A, b: B, c: C): A & B & C;
export function assign<A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D;
export function assign<A, B, C, D, E>(
a: A,
b: B,
c: C,
d: D,
e: E,
): A & B & C & D & E;
export function assign(target: any, ...sources: Array<any>): any;
export function assign(
target: { [key: string]: any },
...sources: Array<{ [key: string]: any }>
): { [key: string]: any } {
sources.forEach(source => {
if (typeof source === 'undefined' || source === null) {
return;
}
Object.keys(source).forEach(key => {
target[key] = source[key];
});
});
return target;
}

View File

@@ -0,0 +1,4 @@
export const canUseWeakMap = typeof WeakMap === 'function' && !(
typeof navigator === 'object' &&
navigator.product === 'ReactNative'
);

View File

@@ -0,0 +1,37 @@
const { toString } = Object.prototype;
/**
* Deeply clones a value to create a new instance.
*/
export function cloneDeep<T>(value: T): T {
return cloneDeepHelper(value, new Map());
}
function cloneDeepHelper<T>(val: T, seen: Map<any, any>): T {
switch (toString.call(val)) {
case "[object Array]": {
if (seen.has(val)) return seen.get(val);
const copy: T & any[] = (val as any).slice(0);
seen.set(val, copy);
copy.forEach(function (child, i) {
copy[i] = cloneDeepHelper(child, seen);
});
return copy;
}
case "[object Object]": {
if (seen.has(val)) return seen.get(val);
// High fidelity polyfills of Object.create and Object.getPrototypeOf are
// possible in all JS environments, so we will assume they exist/work.
const copy = Object.create(Object.getPrototypeOf(val));
seen.set(val, copy);
Object.keys(val).forEach(key => {
copy[key] = cloneDeepHelper((val as any)[key], seen);
});
return copy;
}
default:
return val;
}
}

View File

@@ -0,0 +1,24 @@
export function getEnv(): string | undefined {
if (typeof process !== 'undefined' && process.env.NODE_ENV) {
return process.env.NODE_ENV;
}
// default environment
return 'development';
}
export function isEnv(env: string): boolean {
return getEnv() === env;
}
export function isProduction(): boolean {
return isEnv('production') === true;
}
export function isDevelopment(): boolean {
return isEnv('development') === true;
}
export function isTest(): boolean {
return isEnv('test') === true;
}

View File

@@ -0,0 +1,15 @@
import { ExecutionResult } from 'graphql';
export function tryFunctionOrLogError(f: Function) {
try {
return f();
} catch (e) {
if (console.error) {
console.error(e);
}
}
}
export function graphQLResultHasError(result: ExecutionResult) {
return result.errors && result.errors.length;
}

View File

@@ -0,0 +1,14 @@
export function filterInPlace<T>(
array: T[],
test: (elem: T) => boolean,
context?: any,
): T[] {
let target = 0;
array.forEach(function (elem, i) {
if (test.call(this, elem, i, array)) {
array[target++] = elem;
}
}, context);
array.length = target;
return array;
}

View File

@@ -0,0 +1 @@
export { equal as isEqual } from '@wry/equality';

View File

@@ -0,0 +1,33 @@
import { isDevelopment, isTest } from './environment';
// Taken (mostly) from https://github.com/substack/deep-freeze to avoid
// import hassles with rollup.
function deepFreeze(o: any) {
Object.freeze(o);
Object.getOwnPropertyNames(o).forEach(function(prop) {
if (
o[prop] !== null &&
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
!Object.isFrozen(o[prop])
) {
deepFreeze(o[prop]);
}
});
return o;
}
export function maybeDeepFreeze(obj: any) {
if (isDevelopment() || isTest()) {
// Polyfilled Symbols potentially cause infinite / very deep recursion while deep freezing
// which is known to crash IE11 (https://github.com/apollographql/apollo-client/issues/3043).
const symbolIsPolyfilled =
typeof Symbol === 'function' && typeof Symbol('') === 'string';
if (!symbolIsPolyfilled) {
return deepFreeze(obj);
}
}
return obj;
}

View File

@@ -0,0 +1,115 @@
const { hasOwnProperty } = Object.prototype;
// These mergeDeep and mergeDeepArray utilities merge any number of objects
// together, sharing as much memory as possible with the source objects, while
// remaining careful to avoid modifying any source objects.
// Logically, the return type of mergeDeep should be the intersection of
// all the argument types. The binary call signature is by far the most
// common, but we support 0- through 5-ary as well. After that, the
// resulting type is just the inferred array element type. Note to nerds:
// there is a more clever way of doing this that converts the tuple type
// first to a union type (easy enough: T[number]) and then converts the
// union to an intersection type using distributive conditional type
// inference, but that approach has several fatal flaws (boolean becomes
// true & false, and the inferred type ends up as unknown in many cases),
// in addition to being nearly impossible to explain/understand.
export type TupleToIntersection<T extends any[]> =
T extends [infer A] ? A :
T extends [infer A, infer B] ? A & B :
T extends [infer A, infer B, infer C] ? A & B & C :
T extends [infer A, infer B, infer C, infer D] ? A & B & C & D :
T extends [infer A, infer B, infer C, infer D, infer E] ? A & B & C & D & E :
T extends (infer U)[] ? U : any;
export function mergeDeep<T extends any[]>(
...sources: T
): TupleToIntersection<T> {
return mergeDeepArray(sources);
}
// In almost any situation where you could succeed in getting the
// TypeScript compiler to infer a tuple type for the sources array, you
// could just use mergeDeep instead of mergeDeepArray, so instead of
// trying to convert T[] to an intersection type we just infer the array
// element type, which works perfectly when the sources array has a
// consistent element type.
export function mergeDeepArray<T>(sources: T[]): T {
let target = sources[0] || {} as T;
const count = sources.length;
if (count > 1) {
const pastCopies: any[] = [];
target = shallowCopyForMerge(target, pastCopies);
for (let i = 1; i < count; ++i) {
target = mergeHelper(target, sources[i], pastCopies);
}
}
return target;
}
function isObject(obj: any): obj is Record<string | number, any> {
return obj !== null && typeof obj === 'object';
}
function mergeHelper(
target: any,
source: any,
pastCopies: any[],
) {
if (isObject(source) && isObject(target)) {
// In case the target has been frozen, make an extensible copy so that
// we can merge properties into the copy.
if (Object.isExtensible && !Object.isExtensible(target)) {
target = shallowCopyForMerge(target, pastCopies);
}
Object.keys(source).forEach(sourceKey => {
const sourceValue = source[sourceKey];
if (hasOwnProperty.call(target, sourceKey)) {
const targetValue = target[sourceKey];
if (sourceValue !== targetValue) {
// When there is a key collision, we need to make a shallow copy of
// target[sourceKey] so the merge does not modify any source objects.
// To avoid making unnecessary copies, we use a simple array to track
// past copies, since it's safe to modify copies created earlier in
// the merge. We use an array for pastCopies instead of a Map or Set,
// since the number of copies should be relatively small, and some
// Map/Set polyfills modify their keys.
target[sourceKey] = mergeHelper(
shallowCopyForMerge(targetValue, pastCopies),
sourceValue,
pastCopies,
);
}
} else {
// If there is no collision, the target can safely share memory with
// the source, and the recursion can terminate here.
target[sourceKey] = sourceValue;
}
});
return target;
}
// If source (or target) is not an object, let source replace target.
return source;
}
function shallowCopyForMerge<T>(value: T, pastCopies: any[]): T {
if (
value !== null &&
typeof value === 'object' &&
pastCopies.indexOf(value) < 0
) {
if (Array.isArray(value)) {
value = (value as any).slice(0);
} else {
value = {
__proto__: Object.getPrototypeOf(value),
...value,
};
}
pastCopies.push(value);
}
return value;
}

View File

@@ -0,0 +1,14 @@
/**
* In order to make assertions easier, this function strips `symbol`'s from
* the incoming data.
*
* This can be handy when running tests against `apollo-client` for example,
* since it adds `symbol`'s to the data in the store. Jest's `toEqual`
* function now covers `symbol`'s (https://github.com/facebook/jest/pull/3437),
* which means all test data used in a `toEqual` comparison would also have to
* include `symbol`'s, to pass. By stripping `symbol`'s from the cache data
* we can compare against more simplified test data.
*/
export function stripSymbols<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}

View File

@@ -0,0 +1,24 @@
import { isProduction, isTest } from './environment';
const haveWarned = Object.create({});
/**
* Print a warning only once in development.
* In production no warnings are printed.
* In test all warnings are printed.
*
* @param msg The warning message
* @param type warn or error (will call console.warn or console.error)
*/
export function warnOnceInDevelopment(msg: string, type = 'warn') {
if (!isProduction() && !haveWarned[msg]) {
if (!isTest()) {
haveWarned[msg] = true;
}
if (type === 'error') {
console.error(msg);
} else {
console.warn(msg);
}
}
}