@micro-graphql/core
Advanced tools
| import { DocumentNode } from 'graphql/language/ast'; | ||
| export declare function addRequiredFields(query: DocumentNode): DocumentNode; | ||
| //# sourceMappingURL=helpers.d.ts.map |
| {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAiCpD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAenE"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const kinds_1 = require("graphql/language/kinds"); | ||
| const visitor_1 = require("graphql/language/visitor"); | ||
| const isInlineFragment = (node) => node.kind === kinds_1.Kind.INLINE_FRAGMENT; | ||
| const hasField = (fieldName) => (set) => set.some(({ alias, name }) => (alias || name).value === fieldName); | ||
| const createField = (name) => ({ | ||
| kind: 'Field', | ||
| alias: undefined, | ||
| name: { | ||
| kind: 'Name', | ||
| value: name | ||
| }, | ||
| arguments: [], | ||
| directives: [], | ||
| selectionSet: undefined | ||
| }); | ||
| const hasTypeNameField = hasField('__typename'); | ||
| const hasEdgesField = hasField('edges'); | ||
| const typeNameField = createField('__typename'); | ||
| const connectionFields = ['edges', 'pageInfo']; | ||
| const excludeMetaFields = (node, _, parent) => node.selections | ||
| .some(isInlineFragment) | ||
| || hasEdgesField(node.selections) | ||
| || (!isInlineFragment(parent) && connectionFields.includes(parent.name.value)); | ||
| function addRequiredFields(query) { | ||
| return visitor_1.visit(query, { | ||
| SelectionSet: (node, key, parent) => { | ||
| if (parent | ||
| && (parent.kind === kinds_1.Kind.OPERATION_DEFINITION || excludeMetaFields(node, key, parent))) { | ||
| return undefined; | ||
| } | ||
| if (!hasTypeNameField(node.selections)) { | ||
| node.selections.unshift(typeNameField); | ||
| } | ||
| return node; | ||
| } | ||
| }); | ||
| } | ||
| exports.addRequiredFields = addRequiredFields; | ||
| //# sourceMappingURL=helpers.js.map |
| {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":";;AAEA,kDAA8C;AAC9C,sDAAiD;AAEjD,MAAM,gBAAgB,GAAG,CAAC,IAAS,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,YAAI,CAAC,eAAe,CAAC;AAEpF,MAAM,QAAQ,GAAG,CAAC,SAAc,EAAE,EAAE,CAAC,CAAC,GAAQ,EAAO,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EACjE,KAAK,EAAE,IAAI,EACN,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC;AAEhD,MAAM,WAAW,GAAG,CAAC,IAAY,EAAO,EAAE,CAAC,CAAC;IAC3C,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE;QACL,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,IAAI;KACX;IACD,SAAS,EAAE,EAAE;IACb,UAAU,EAAE,EAAE;IACd,YAAY,EAAE,SAAS;CACvB,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAChD,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;AACxC,MAAM,aAAa,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;AAEhD,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AAE/C,MAAM,iBAAiB,GAAG,CAAC,IAAS,EAAE,CAAM,EAAE,MAAW,EAAO,EAAE,CAAC,IAAI,CAAC,UAAU;KAChF,IAAI,CAAC,gBAAgB,CAAC;OACpB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC;OAC9B,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AAEhF,SAAgB,iBAAiB,CAAC,KAAmB;IACpD,OAAO,eAAK,CAAC,KAAK,EAAE;QACnB,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAW,EAAO,EAAE;YAC7C,IAAI,MAAM;mBACN,CAAC,MAAM,CAAC,IAAI,KAAK,YAAI,CAAC,oBAAoB,IAAI,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,EAAE;gBACxF,OAAO,SAAS,CAAC;aACjB;YAED,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE;gBACtC,IAAI,CAAC,UAAkB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;aAChD;YAED,OAAO,IAAI,CAAC;QACb,CAAC;KACD,CAAC,CAAC;AACJ,CAAC;AAfD,8CAeC"} |
| import { DocumentNode } from 'graphql/language/ast'; | ||
| export declare type SubscribeCallback<TData> = (data: TData) => void; | ||
| export declare type UnsubscribeFunc = () => void; | ||
| export interface IMicroCache { | ||
| readQuery<TData, TVariables>(query: DocumentNode, variables: TVariables): TData | undefined; | ||
| writeQuery<TData, TVariables>(query: DocumentNode, variables: TVariables, data: TData): void; | ||
| subscribe<TData, TVariables>(query: DocumentNode, variables: TVariables, callback: SubscribeCallback<TData>): UnsubscribeFunc; | ||
| } | ||
| export declare function newCache(): IMicroCache; | ||
| //# sourceMappingURL=smart-cache.d.ts.map |
| {"version":3,"file":"smart-cache.d.ts","sourceRoot":"","sources":["../src/smart-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAQpD,oBAAY,iBAAiB,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;AAC7D,oBAAY,eAAe,GAAG,MAAM,IAAI,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC3B,SAAS,CAAC,KAAK,EAAE,UAAU,EAC1B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,UAAU,GACnB,KAAK,GAAG,SAAS,CAAC;IAErB,UAAU,CAAC,KAAK,EAAE,UAAU,EAC3B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,KAAK,GACT,IAAI,CAAC;IAER,SAAS,CAAC,KAAK,EAAE,UAAU,EAC1B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAChC,eAAe,CAAC;CACnB;AASD,wBAAgB,QAAQ,IAAI,WAAW,CAgEtC"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const graphql_norm_1 = require("graphql-norm"); | ||
| function newCache() { | ||
| let cache = {}; | ||
| let subscriptions = []; | ||
| function notifySubscribers(triggers) { | ||
| subscriptions.forEach(s => { | ||
| if (s.triggers.length < 2 | ||
| || s.triggers.slice(1).some(trigger => triggers.some(t => t === trigger))) { | ||
| const { data, fields } = graphql_norm_1.denormalize(s.query, s.variables, cache); | ||
| s.triggers = Object.getOwnPropertyNames(fields); | ||
| if (data) { | ||
| s.callback(data); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| return { | ||
| readQuery(query, variables) { | ||
| const { data } = graphql_norm_1.denormalize(query, variables, cache); | ||
| return data; | ||
| }, | ||
| writeQuery(query, variables, data) { | ||
| const normalizedData = graphql_norm_1.normalize(query, variables, data); | ||
| cache = graphql_norm_1.merge(cache, normalizedData); | ||
| notifySubscribers(Object.getOwnPropertyNames(normalizedData)); | ||
| }, | ||
| subscribe(query, variables, callback) { | ||
| const { data, fields } = graphql_norm_1.denormalize(query, variables, cache); | ||
| if (typeof data !== 'undefined') { | ||
| callback(data); | ||
| } | ||
| subscriptions.push({ | ||
| callback: callback, | ||
| triggers: Object.getOwnPropertyNames(fields), | ||
| query, | ||
| variables | ||
| }); | ||
| return () => { | ||
| subscriptions = subscriptions.filter(s => s.callback !== callback); | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| exports.newCache = newCache; | ||
| //# sourceMappingURL=smart-cache.js.map |
| {"version":3,"file":"smart-cache.js","sourceRoot":"","sources":["../src/smart-cache.ts"],"names":[],"mappings":";;AACA,+CAKsB;AA+BtB,SAAgB,QAAQ;IACvB,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,aAAa,GAAoB,EAAE,CAAC;IAExC,SAAS,iBAAiB,CAAC,QAAkB;QAC5C,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;YACzB,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;mBACrB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EACxE;gBACD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,0BAAW,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,SAAsB,EAAE,KAAK,CAAC,CAAC;gBAE/E,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;gBAEhD,IAAI,IAAI,EAAE;oBACT,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;iBACjB;aACD;QACF,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,OAAO;QACN,SAAS,CACR,KAAmB,EACnB,SAAqB;YAErB,MAAM,EAAE,IAAI,EAAE,GAAG,0BAAW,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YAEtD,OAAO,IAAyB,CAAC;QAClC,CAAC;QAED,UAAU,CACT,KAAmB,EACnB,SAAqB,EACrB,IAAW;YAEX,MAAM,cAAc,GAAG,wBAAS,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;YACzD,KAAK,GAAG,oBAAK,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAErC,iBAAiB,CAAC,MAAM,CAAC,mBAAmB,CAAC,cAAc,CAAC,CAAC,CAAC;QAC/D,CAAC;QAED,SAAS,CACR,KAAmB,EACnB,SAAqB,EACrB,QAAkC;YAElC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,0BAAW,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YAE9D,IAAI,OAAO,IAAI,KAAK,WAAW,EAAE;gBAChC,QAAQ,CAAC,IAAa,CAAC,CAAC;aACxB;YAED,aAAa,CAAC,IAAI,CAAC;gBAClB,QAAQ,EAAE,QAAsC;gBAChD,QAAQ,EAAE,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;gBAC5C,KAAK;gBACL,SAAS;aACT,CAAC,CAAC;YAEH,OAAO,GAAS,EAAE;gBACjB,aAAa,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;YACpE,CAAC,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC;AAhED,4BAgEC"} |
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import { DocumentNode } from 'graphql/language/ast'; | ||
| import { Kind } from 'graphql/language/kinds'; | ||
| import { visit } from 'graphql/language/visitor'; | ||
| const isInlineFragment = (node: any): boolean => node.kind === Kind.INLINE_FRAGMENT; | ||
| const hasField = (fieldName: any) => (set: any): any => set.some(({ | ||
| alias, name | ||
| }: any) => (alias || name).value === fieldName); | ||
| const createField = (name: string): any => ({ | ||
| kind: 'Field', | ||
| alias: undefined, | ||
| name: { | ||
| kind: 'Name', | ||
| value: name | ||
| }, | ||
| arguments: [], | ||
| directives: [], | ||
| selectionSet: undefined | ||
| }); | ||
| const hasTypeNameField = hasField('__typename'); | ||
| const hasEdgesField = hasField('edges'); | ||
| const typeNameField = createField('__typename'); | ||
| const connectionFields = ['edges', 'pageInfo']; | ||
| const excludeMetaFields = (node: any, _: any, parent: any): any => node.selections | ||
| .some(isInlineFragment) | ||
| || hasEdgesField(node.selections) | ||
| || (!isInlineFragment(parent) && connectionFields.includes(parent.name.value)); | ||
| export function addRequiredFields(query: DocumentNode): DocumentNode { | ||
| return visit(query, { | ||
| SelectionSet: (node, key, parent: any): any => { | ||
| if (parent | ||
| && (parent.kind === Kind.OPERATION_DEFINITION || excludeMetaFields(node, key, parent))) { | ||
| return undefined; | ||
| } | ||
| if (!hasTypeNameField(node.selections)) { | ||
| (node.selections as any).unshift(typeNameField); | ||
| } | ||
| return node; | ||
| } | ||
| }); | ||
| } |
| // eslint-disable-next-line import/no-extraneous-dependencies | ||
| import gql from 'graphql-tag'; | ||
| export const query = gql` | ||
| query MockFilmQuery($filmID: ID) { | ||
| film(filmID: $filmID) { | ||
| id | ||
| title | ||
| episodeID | ||
| } | ||
| } | ||
| `; | ||
| export const variables = { filmID: 1 }; | ||
| export const response = ` | ||
| { | ||
| "data": { | ||
| "film": { | ||
| "__typename": "Film", | ||
| "id": "ZmlsbXM6MQ==", | ||
| "title": "A New Hope", | ||
| "episodeID": 4 | ||
| } | ||
| } | ||
| } | ||
| `; |
+8
-1
| { | ||
| "name": "@micro-graphql/core", | ||
| "version": "0.1.2", | ||
| "version": "0.2.0-alpha.0", | ||
| "license": "MIT", | ||
@@ -11,3 +11,10 @@ "main": "lib/index.js", | ||
| }, | ||
| "dependencies": { | ||
| "graphql": "*", | ||
| "graphql-norm": "^1.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/graphql": "^14.5.0", | ||
| "graphql": "^14.6.0", | ||
| "graphql-tag": "^2.10.3", | ||
| "typescript": "^3.7.5" | ||
@@ -14,0 +21,0 @@ }, |
+102
-42
@@ -1,62 +0,122 @@ | ||
| import { objectHash } from './hash'; | ||
| import { DocumentNode } from 'graphql/language/ast'; | ||
| import { | ||
| normalize, | ||
| denormalize, | ||
| merge, | ||
| Variables | ||
| } from 'graphql-norm'; | ||
| export interface IMicroGraphQLCacheResult<TValue> { | ||
| data?: TValue; | ||
| success: boolean; | ||
| } | ||
| import { addRequiredFields } from './helpers'; | ||
| export type SubscribeCallback<TData> = (data: TData) => void; | ||
| export type UnsubscribeFunc = () => void; | ||
| export interface IMicroGraphQLCache { | ||
| tryGet<TValue>( | ||
| query: string, | ||
| variables: { [key: string]: unknown } | undefined | ||
| ): IMicroGraphQLCacheResult<TValue>; | ||
| trySet<TValue>( | ||
| query: string, | ||
| variables: { [key: string]: unknown } | undefined, | ||
| data?: TValue | ||
| ): boolean; | ||
| readQuery<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables?: TVariables | ||
| ): TData | undefined; | ||
| writeQuery<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables: TVariables | undefined, | ||
| data: TData | ||
| ): void; | ||
| subscribe<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables: TVariables | undefined, | ||
| callback: SubscribeCallback<TData> | ||
| ): UnsubscribeFunc; | ||
| prepareQuery(query: DocumentNode): DocumentNode; | ||
| stringify(): string; | ||
| restore(data: string): void; | ||
| prepareQuery(query: string): string; | ||
| } | ||
| type SubscriptionArr = Array<{ | ||
| callback: SubscribeCallback<unknown>; | ||
| triggers: string[]; | ||
| query: DocumentNode; | ||
| variables: unknown; | ||
| }>; | ||
| export function createCache(): IMicroGraphQLCache { | ||
| let cache: { [key: string]: unknown } = {}; | ||
| let cache = {}; | ||
| let subscriptions: SubscriptionArr = []; | ||
| function notifySubscribers(triggers: string[]): void { | ||
| subscriptions.forEach(s => { | ||
| if (s.triggers.length < 2 | ||
| || s.triggers.slice(1).some(trigger => triggers.some(t => t === trigger)) | ||
| ) { | ||
| const { data, fields } = denormalize(s.query, s.variables as Variables, cache); | ||
| s.triggers = Object.getOwnPropertyNames(fields); | ||
| if (data) { | ||
| s.callback(data); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| return { | ||
| tryGet: <TValue>( | ||
| query: string, | ||
| variables: { [key: string]: unknown } | undefined | ||
| ): IMicroGraphQLCacheResult<TValue> => { | ||
| const key = objectHash({ query, variables }); | ||
| stringify(): string { | ||
| return JSON.stringify(cache); | ||
| }, | ||
| const success = key in cache; | ||
| const data = success ? cache[key] : undefined; | ||
| restore(data: string): void { | ||
| cache = JSON.parse(data) || {}; | ||
| }, | ||
| return { | ||
| data: data as TValue, | ||
| success | ||
| }; | ||
| prepareQuery(query: DocumentNode): DocumentNode { | ||
| return addRequiredFields(query); | ||
| }, | ||
| trySet: <TValue>( | ||
| query: string, | ||
| variables: { [key: string]: unknown } | undefined, | ||
| data?: TValue | ||
| ): boolean => { | ||
| const key = objectHash({ query, variables }); | ||
| cache[key] = data; | ||
| readQuery<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables?: TVariables | ||
| ): TData | undefined { | ||
| const { data } = denormalize(query, variables, cache); | ||
| return false; | ||
| return data as TData | undefined; | ||
| }, | ||
| stringify(): string { | ||
| return JSON.stringify(cache); | ||
| writeQuery<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables: TVariables | undefined, | ||
| data: TData | ||
| ): void { | ||
| const normalizedData = normalize(query, variables, data); | ||
| cache = merge(cache, normalizedData); | ||
| notifySubscribers(Object.getOwnPropertyNames(normalizedData)); | ||
| }, | ||
| restore(data: string): void { | ||
| cache = JSON.parse(data); | ||
| }, | ||
| prepareQuery(query: string): string { | ||
| return query; | ||
| subscribe<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables: TVariables | undefined, | ||
| callback: SubscribeCallback<TData> | ||
| ): UnsubscribeFunc { | ||
| const { data, fields } = denormalize(query, variables, cache); | ||
| if (typeof data !== 'undefined') { | ||
| callback(data as TData); | ||
| } | ||
| subscriptions.push({ | ||
| callback: callback as SubscribeCallback<unknown>, | ||
| triggers: Object.getOwnPropertyNames(fields), | ||
| query, | ||
| variables | ||
| }); | ||
| return (): void => { | ||
| subscriptions = subscriptions.filter(s => s.callback !== callback); | ||
| }; | ||
| } | ||
| }; | ||
| } |
+79
-130
@@ -1,3 +0,4 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| /* eslint-disable import/extensions */ | ||
| import { DocumentNode } from 'graphql/language/ast'; | ||
| import { print } from 'graphql/language/printer'; | ||
| import { GraphQLFormattedError } from 'graphql/error/formatError'; | ||
@@ -7,166 +8,114 @@ import { IMicroGraphQLCache } from './cache'; | ||
| export interface IMicroGraphQLConfig { | ||
| cache: IMicroGraphQLCache; | ||
| url: string; | ||
| ssr?: boolean; | ||
| fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>; | ||
| } | ||
| export interface IMicroGraphQLError { | ||
| message: string; | ||
| path?: ReadonlyArray<string | number>; | ||
| extensions?: { [key: string]: unknown }; | ||
| } | ||
| export interface IMicroGraphQLResult<TData> { | ||
| loading: boolean; | ||
| data?: TData; | ||
| errors?: IMicroGraphQLError[]; | ||
| errors?: GraphQLFormattedError[]; | ||
| } | ||
| export interface IMicroGraphQLQueryOptions<TQueryVariables extends { [key: string]: unknown }> { | ||
| export interface IMicroGraphQLQueryOptions { | ||
| skipCache?: boolean; | ||
| variables?: TQueryVariables; | ||
| } | ||
| export interface IMicroGraphQLSubscriptionOptions< | ||
| TQueryVariables extends { [key: string]: unknown } | ||
| > | ||
| extends IMicroGraphQLQueryOptions<TQueryVariables> { | ||
| query: string; | ||
| export interface IMicroGraphQLClient { | ||
| cache: IMicroGraphQLCache; | ||
| resolveQueries(): Promise<void>; | ||
| query<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables?: TVariables, | ||
| options?: IMicroGraphQLQueryOptions | ||
| ): Promise<IMicroGraphQLResult<TData>>; | ||
| mutate<TData, TVariables>( | ||
| mutation: DocumentNode, | ||
| variables?: TVariables | ||
| ): Promise<IMicroGraphQLResult<TData>>; | ||
| } | ||
| export interface IMicroGraphQLClient { | ||
| export interface IMicroGraphQLClientConfig { | ||
| cache: IMicroGraphQLCache; | ||
| fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>; | ||
| ssr?: boolean; | ||
| // eslint-disable-next-line max-len | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| query<TData extends { [key: string]: any }, TQueryVariables extends { [key: string]: any }>( | ||
| query: string, | ||
| options?: IMicroGraphQLQueryOptions<TQueryVariables> | ||
| ): Promise<IMicroGraphQLResult<TData>>; | ||
| // eslint-disable-next-line max-len | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| subscribe: <TData extends { [key: string]: any }, TQueryVariables extends { [key: string]: any }>( | ||
| options: IMicroGraphQLSubscriptionOptions<TQueryVariables>, | ||
| subscription: (data: IMicroGraphQLResult<TData>) => void | ||
| ) => () => void; | ||
| resolveQueries(): Promise<void>; | ||
| url: string; | ||
| } | ||
| export class MicroGraphQLKeyError extends Error {} | ||
| export const queryKeyError = 'error creating a unique key for the query'; | ||
| export function createClient({ | ||
| url, | ||
| cache, | ||
| fetch, | ||
| ssr, | ||
| fetch | ||
| }: IMicroGraphQLConfig): IMicroGraphQLClient { | ||
| const subscriptions = new Map<string, Array<(data: IMicroGraphQLResult<any> & {}) => void>>(); | ||
| url | ||
| }: IMicroGraphQLClientConfig): IMicroGraphQLClient { | ||
| const requests: { [key: string]: Promise<unknown> } = {}; | ||
| const queries: { [key: string]: Promise<IMicroGraphQLResult<unknown>> } = {}; | ||
| async function doRequest<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables?: TVariables | ||
| ): Promise<IMicroGraphQLResult<TData>> { | ||
| const resultPromise = (async (): Promise<IMicroGraphQLResult<TData>> => { | ||
| const response = await fetch(url, { | ||
| method: 'post', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify({ | ||
| query: print(query), | ||
| variables | ||
| }) | ||
| }); | ||
| return { | ||
| cache, | ||
| ssr, | ||
| resolveQueries: async (): Promise<void> => { | ||
| await Promise.all(Object.getOwnPropertyNames(queries).map(key => queries[key])); | ||
| }, | ||
| // eslint-disable-next-line max-len | ||
| subscribe: <TData extends { [key: string]: any }, TQueryVariables extends { [key: string]: any }>( | ||
| options: IMicroGraphQLSubscriptionOptions<TQueryVariables>, | ||
| subscription: (data: IMicroGraphQLResult<TData>) => void | ||
| ): (() => void) => { | ||
| const query = cache.prepareQuery(options.query); | ||
| const json = (await response.json()) as IMicroGraphQLResult<TData>; | ||
| const cached = cache.tryGet<TData>(query, options.variables); | ||
| if (cached.success) { | ||
| subscription({ | ||
| loading: false, | ||
| data: cached.data | ||
| }); | ||
| if (json.data) { | ||
| cache.writeQuery<TData, TVariables>(query, variables, json.data); | ||
| } | ||
| const key = objectHash({ query, variables: options.variables }); | ||
| return json; | ||
| })(); | ||
| const subs = subscriptions.get(key) || []; | ||
| subs.push(subscription); | ||
| subscriptions.set(key, subs); | ||
| if (ssr) { | ||
| requests[objectHash({ query, variables })] = resultPromise; | ||
| } | ||
| return (): void => { | ||
| const toRemoveFrom = subscriptions.get(key)!; | ||
| subscriptions.set( | ||
| key, | ||
| toRemoveFrom.filter(s => s !== subscription) | ||
| ); | ||
| }; | ||
| }, | ||
| // eslint-disable-next-line max-len | ||
| query: async <TData extends { [key: string]: any }, TQueryVariables extends { [key: string]: any }>( | ||
| inputQuery: string, | ||
| options?: IMicroGraphQLQueryOptions<TQueryVariables> | ||
| ): Promise<IMicroGraphQLResult<TData>> => { | ||
| const { skipCache }: IMicroGraphQLQueryOptions<TQueryVariables> = { | ||
| skipCache: false, | ||
| ...options | ||
| }; | ||
| const result = await resultPromise; | ||
| const query = cache.prepareQuery(inputQuery); | ||
| if (!skipCache) { | ||
| const cachedResult = !skipCache && cache.tryGet<TData>(query, options && options.variables); | ||
| return result; | ||
| } | ||
| if (cachedResult.success) { | ||
| return { | ||
| loading: false, | ||
| data: cachedResult.data | ||
| }; | ||
| } | ||
| } | ||
| return { | ||
| cache, | ||
| const key = objectHash({ query, variables: options && options.variables }); | ||
| async resolveQueries(): Promise<void> { | ||
| await Promise.all(Object.getOwnPropertyNames(requests).map(key => requests[key])); | ||
| }, | ||
| const subs = subscriptions.get(key); | ||
| if (subs) { | ||
| subs.forEach(sub => sub({ | ||
| loading: true | ||
| })); | ||
| async query<TData, TVariables>( | ||
| query: DocumentNode, | ||
| variables?: TVariables, | ||
| options: IMicroGraphQLQueryOptions = { | ||
| skipCache: false | ||
| } | ||
| ): Promise<IMicroGraphQLResult<TData>> { | ||
| const { skipCache } = options; | ||
| const resultPromise = (async (): Promise<IMicroGraphQLResult<TData>> => { | ||
| const response = await fetch(url, { | ||
| method: 'post', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify({ | ||
| query, | ||
| variables: options && options.variables | ||
| }) | ||
| }); | ||
| const preparedQuery = cache.prepareQuery(query); | ||
| const json = (await response.json()) as IMicroGraphQLResult<TData>; | ||
| if (!skipCache) { | ||
| const data = cache.readQuery<TData, TVariables>(preparedQuery, variables); | ||
| if (json.data) { | ||
| cache.trySet<TData>(query, options && options.variables, json.data); | ||
| if (typeof data !== 'undefined') { | ||
| return { | ||
| data | ||
| }; | ||
| } | ||
| } | ||
| return { | ||
| errors: undefined, | ||
| ...json, | ||
| loading: false | ||
| }; | ||
| })(); | ||
| const result = await doRequest<TData, TVariables>(query, variables); | ||
| if (ssr) { | ||
| queries[key] = resultPromise; | ||
| } | ||
| return result; | ||
| }, | ||
| const result = await resultPromise; | ||
| async mutate<TData, TVariables>( | ||
| mutation: DocumentNode, | ||
| variables: TVariables | ||
| ): Promise<IMicroGraphQLResult<TData>> { | ||
| const preparedMutation = cache.prepareQuery(mutation); | ||
| if (subs) { | ||
| subs.forEach(sub => sub(result)); | ||
| } | ||
| const result = await doRequest<TData, TVariables>(preparedMutation, variables); | ||
@@ -173,0 +122,0 @@ return result; |
+0
-1
| export * from './client'; | ||
| export * from './cache'; | ||
| export * from './gql'; |
+243
-37
@@ -1,12 +0,9 @@ | ||
| /* eslint-disable import/extensions */ | ||
| import { | ||
| createCache, | ||
| IMicroGraphQLCache, | ||
| gql, | ||
| frag | ||
| } from '../src'; | ||
| // eslint-disable-next-line import/no-extraneous-dependencies | ||
| import gql from 'graphql-tag'; | ||
| const fragment = frag` | ||
| import { createCache, IMicroGraphQLCache } from '../src/cache'; | ||
| const fragment = gql` | ||
| fragment TestFrag on Film { | ||
| title | ||
| releaseDate | ||
| } | ||
@@ -19,10 +16,78 @@ `; | ||
| id | ||
| ${fragment} | ||
| ...TestFrag | ||
| ... on Film { | ||
| title | ||
| } | ||
| } | ||
| } | ||
| ${fragment} | ||
| `; | ||
| const subQuery = gql` | ||
| query TestSubQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| id | ||
| title | ||
| } | ||
| } | ||
| `; | ||
| interface IVariables { | ||
| id: string; | ||
| } | ||
| const variables = { id: 'abc' }; | ||
| describe('cache', () => { | ||
| interface IExpected { | ||
| film: { | ||
| __typename: 'Film'; | ||
| id: string; | ||
| title: string; | ||
| releaseDate: string; | ||
| }; | ||
| } | ||
| const expected: IExpected = { | ||
| film: { | ||
| __typename: 'Film', | ||
| id: 'abc', | ||
| title: 'abc', | ||
| releaseDate: '2020-01-30' | ||
| } | ||
| }; | ||
| const changed: IExpected = { | ||
| film: { | ||
| __typename: 'Film', | ||
| id: 'abc', | ||
| title: 'def', | ||
| releaseDate: '2020-02-30' | ||
| } | ||
| }; | ||
| interface ISubExpected { | ||
| film: { | ||
| __typename: 'Film'; | ||
| id: string; | ||
| title: string; | ||
| }; | ||
| } | ||
| const subExpected: ISubExpected = { | ||
| film: { | ||
| __typename: 'Film', | ||
| id: 'abc', | ||
| title: 'abc' | ||
| } | ||
| }; | ||
| const subChanged: ISubExpected = { | ||
| film: { | ||
| __typename: 'Film', | ||
| id: 'abc', | ||
| title: 'def' | ||
| } | ||
| }; | ||
| describe('smart-cache', () => { | ||
| let cache: IMicroGraphQLCache; | ||
@@ -34,38 +99,179 @@ | ||
| it('returns success as false and undefined data for unset key', () => { | ||
| const cached = cache.tryGet(query, variables); | ||
| expect(cached).toBeTruthy(); | ||
| expect(cached.success).toBe(false); | ||
| expect(cached.data).toBeUndefined(); | ||
| }); | ||
| it('skips initial callback if nothing in cache', () => { | ||
| let called = 0; | ||
| it('can set key and retrieve value', () => { | ||
| const data = { v: 10 }; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| cache.prepareQuery(query), | ||
| variables, | ||
| () => { | ||
| called += 1; | ||
| } | ||
| ); | ||
| cache.trySet(query, variables, data); | ||
| const cached = cache.tryGet(query, variables); | ||
| expect(cached).toBeTruthy(); | ||
| expect(cached.success).toBe(true); | ||
| expect(cached.data).toBe(data); | ||
| unsubscribe(); | ||
| expect(called).toBe(0); | ||
| }); | ||
| it('can write and read query', () => { | ||
| cache.writeQuery(cache.prepareQuery(query), variables, expected); | ||
| const data = cache.readQuery<IExpected, IVariables>(query, variables); | ||
| expect(data).toEqual(expected); | ||
| }); | ||
| it('can stringify and restore', () => { | ||
| const data = { v: 10 }; | ||
| cache.writeQuery(cache.prepareQuery(query), variables, expected); | ||
| const data = cache.readQuery<IExpected, IVariables>(query, variables); | ||
| expect(data).toEqual(expected); | ||
| cache.trySet(query, variables, data); | ||
| const stringified = cache.stringify(); | ||
| const secondCache = createCache(); | ||
| secondCache.restore(cache.stringify()); | ||
| const data2 = cache.readQuery<IExpected, IVariables>(cache.prepareQuery(query), variables); | ||
| expect(data2).toEqual(expected); | ||
| }); | ||
| const newCache = createCache(); | ||
| newCache.restore(stringified); | ||
| it('can restore default', () => { | ||
| const secondCache = createCache(); | ||
| secondCache.restore('""'); | ||
| const data2 = cache.readQuery<IExpected, IVariables>(cache.prepareQuery(query), variables); | ||
| expect(data2).toBeUndefined(); | ||
| }); | ||
| const cached = cache.tryGet(query, variables); | ||
| expect(cached).toBeTruthy(); | ||
| expect(cached.success).toBe(true); | ||
| expect(cached.data).toBe(data); | ||
| it('can subscribe and get initial value', () => { | ||
| cache.writeQuery<IExpected, IVariables>(cache.prepareQuery(query), variables, expected); | ||
| const restoredCached = cache.tryGet(query, variables); | ||
| expect(restoredCached).toBeTruthy(); | ||
| expect(restoredCached.success).toBe(true); | ||
| expect(restoredCached.data).toEqual(data); | ||
| let called = 0; | ||
| let result: IExpected | undefined; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| query, | ||
| variables, | ||
| (data: IExpected) => { | ||
| called += 1; | ||
| result = data; | ||
| } | ||
| ); | ||
| unsubscribe(); | ||
| expect(called).toBe(1); | ||
| expect(result).toEqual(expected); | ||
| }); | ||
| it('skips callback if query not fulfilled', () => { | ||
| const unrelatedQuery = gql` | ||
| query TestNoFulfilledQuery { | ||
| rofl { | ||
| name | ||
| } | ||
| } | ||
| `; | ||
| let called = 0; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| cache.prepareQuery(unrelatedQuery), | ||
| variables, | ||
| () => { | ||
| called += 1; | ||
| } | ||
| ); | ||
| cache.writeQuery(cache.prepareQuery(query), variables, expected); | ||
| expect(called).toBe(0); | ||
| unsubscribe(); | ||
| }); | ||
| it('skips callback if query not fulfilled and existing data', () => { | ||
| const unrelatedQuery = gql` | ||
| query TestNoFulfilledQuery { | ||
| rofl { | ||
| name | ||
| } | ||
| } | ||
| `; | ||
| cache.writeQuery(cache.prepareQuery(unrelatedQuery), {}, { | ||
| rofl: { | ||
| __typename: 'Rofl', | ||
| name: 'rofl' | ||
| } | ||
| }); | ||
| let called = 0; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| cache.prepareQuery(unrelatedQuery), | ||
| variables, | ||
| () => { | ||
| called += 1; | ||
| } | ||
| ); | ||
| expect(called).toBe(1); | ||
| cache.writeQuery(cache.prepareQuery(query), variables, expected); | ||
| expect(called).toBe(1); | ||
| unsubscribe(); | ||
| }); | ||
| it('can subscribe and get updated value', () => { | ||
| cache.writeQuery(query, variables, expected); | ||
| let called = 0; | ||
| let result: IExpected | undefined; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| cache.prepareQuery(query), | ||
| variables, | ||
| (data: IExpected) => { | ||
| called += 1; | ||
| result = data; | ||
| } | ||
| ); | ||
| expect(called).toBe(1); | ||
| expect(result).toEqual(expected); | ||
| cache.writeQuery(cache.prepareQuery(query), variables, changed); | ||
| unsubscribe(); | ||
| expect(called).toBe(2); | ||
| expect(result).toEqual(changed); | ||
| }); | ||
| it('can subscribe and get updated value from sub query', () => { | ||
| cache.writeQuery(cache.prepareQuery(query), variables, expected); | ||
| let called = 0; | ||
| let result: IExpected | undefined; | ||
| const unsubscribe = cache.subscribe<IExpected, IVariables>( | ||
| cache.prepareQuery(query), | ||
| variables, | ||
| (data: IExpected) => { | ||
| called += 1; | ||
| result = data; | ||
| } | ||
| ); | ||
| expect(called).toBe(1); | ||
| expect(result).toEqual(expected); | ||
| cache.writeQuery(cache.prepareQuery(subQuery), variables, subChanged); | ||
| unsubscribe(); | ||
| expect(called).toBe(2); | ||
| expect(result).toEqual({ | ||
| film: { | ||
| ...expected.film, | ||
| ...subChanged.film | ||
| } | ||
| }); | ||
| }); | ||
| }); |
+96
-212
@@ -1,2 +0,2 @@ | ||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| // eslint-disable-next-line import/no-extraneous-dependencies | ||
| import 'jest-fetch-mock'; | ||
@@ -7,259 +7,143 @@ | ||
| createClient, | ||
| IMicroGraphQLResult, | ||
| IMicroGraphQLCacheResult, | ||
| IMicroGraphQLConfig | ||
| IMicroGraphQLClient, | ||
| IMicroGraphQLResult | ||
| } from '../src'; | ||
| import { query, variables, response } from './mock-film'; | ||
| describe('client', () => { | ||
| const query = ` | ||
| query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| } | ||
| } | ||
| `; | ||
| const variables = { id: 1 }; | ||
| interface IQueryResult { | ||
| film: { | ||
| title: string; | ||
| }; | ||
| } | ||
| const validateResult = (result?: IMicroGraphQLResult<IQueryResult>): void => { | ||
| expect(result).toBeTruthy(); | ||
| expect(result!.data).toBeTruthy(); | ||
| expect(result!.data!.film).toBeTruthy(); | ||
| expect(result!.data!.film.title).toBe('A New Hope'); | ||
| }; | ||
| let options: IMicroGraphQLConfig; | ||
| beforeEach(() => { | ||
| it('skips cache if no data', async () => { | ||
| global.fetch.resetMocks(); | ||
| global.fetch.mockResponse(` | ||
| {"data":{"film":{"title":"A New Hope"}}} | ||
| `); | ||
| global.fetch.mockResponse('{}'); | ||
| options = { | ||
| const client = createClient({ | ||
| cache: createCache(), | ||
| fetch: global.fetch, | ||
| url: 'https://swapi-graphql.netlify.com/.netlify/functions/index', | ||
| cache: createCache() | ||
| }; | ||
| }); | ||
| url: 'https://swapi-graphql.netlify.com/.netlify/functions/index' | ||
| }); | ||
| it('can return cached result with prepare query', async () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const cached: any = {}; | ||
| await client.query(query, variables); | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: { | ||
| tryGet: <TValue>(): IMicroGraphQLCacheResult<TValue> => ({ | ||
| success: true, | ||
| data: cached | ||
| }), | ||
| trySet: (): boolean => false, | ||
| restore: jest.fn(), | ||
| stringify: jest.fn(), | ||
| prepareQuery: (q: string): string => q | ||
| } | ||
| }); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| }); | ||
| expect(result).toBeTruthy(); | ||
| expect(result.data).toBe(cached); | ||
| const data = client.cache.readQuery(query, variables); | ||
| expect(data).toBeUndefined(); | ||
| }); | ||
| it('can skip cached result with prepare query', async () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const cached: any = {}; | ||
| describe('query', () => { | ||
| const expected = JSON.parse(response); | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: { | ||
| tryGet: <TValue>(): IMicroGraphQLCacheResult<TValue> => ({ | ||
| success: true, | ||
| data: cached | ||
| }), | ||
| trySet: (): boolean => false, | ||
| restore: jest.fn(), | ||
| stringify: jest.fn(), | ||
| prepareQuery: (q: string): string => q | ||
| } | ||
| }); | ||
| let client: IMicroGraphQLClient; | ||
| let promise: Promise<IMicroGraphQLResult<unknown>>; | ||
| beforeEach(() => { | ||
| global.fetch.resetMocks(); | ||
| global.fetch.mockResponse(response); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables, | ||
| skipCache: true | ||
| }); | ||
| expect(result).toBeTruthy(); | ||
| }); | ||
| client = createClient({ | ||
| cache: createCache(), | ||
| fetch: global.fetch, | ||
| url: 'https://swapi-graphql.netlify.com/.netlify/functions/index' | ||
| }); | ||
| it('can return cached result', async () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const cached: any = {}; | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: { | ||
| tryGet: <TValue>(): IMicroGraphQLCacheResult<TValue> => ({ | ||
| success: true, | ||
| data: cached | ||
| }), | ||
| trySet: (): boolean => false, | ||
| restore: jest.fn(), | ||
| stringify: jest.fn(), | ||
| prepareQuery: (q: string): string => q | ||
| } | ||
| promise = client.query(query, variables); | ||
| }); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| it('can query', async () => { | ||
| const result = await promise; | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| }); | ||
| expect(result).toBeTruthy(); | ||
| expect(result.data).toBe(cached); | ||
| }); | ||
| it('can get cached result when subscribing', async () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const cached: any = {}; | ||
| it('can use cache', async () => { | ||
| const result = await promise; | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| let prepared = false; | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: { | ||
| tryGet: <TValue>(): IMicroGraphQLCacheResult<TValue> => ({ | ||
| success: true, | ||
| data: cached | ||
| }), | ||
| trySet: (): boolean => false, | ||
| restore: jest.fn(), | ||
| stringify: jest.fn(), | ||
| prepareQuery: (q: string): string => { | ||
| prepared = true; | ||
| return q; | ||
| } | ||
| } | ||
| const secondResult = await client.query(query, variables); | ||
| expect(secondResult).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| }); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let result: any; | ||
| client.subscribe<IQueryResult, {}>({ | ||
| query, | ||
| variables | ||
| }, (res) => { | ||
| result = res; | ||
| }); | ||
| expect(result).toBeTruthy(); | ||
| expect(result.data).toBe(cached); | ||
| expect(prepared).toBe(true); | ||
| }); | ||
| it('can skip cache', async () => { | ||
| const result = await promise; | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| it('skips cache if no data', async () => { | ||
| global.fetch.resetMocks(); | ||
| global.fetch.mockResponse(` | ||
| {} | ||
| `); | ||
| const client = createClient(options); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| const secondResult = await client.query(query, variables, { skipCache: true }); | ||
| expect(secondResult).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(2); | ||
| }); | ||
| expect(result.data).toBeUndefined(); | ||
| expect(client.cache!.tryGet(query, variables).success).toBe(false); | ||
| }); | ||
| it('can make query', async () => { | ||
| const client = createClient(options); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| it('stores in cache', async () => { | ||
| await promise; | ||
| const data = client.cache.readQuery(query, variables); | ||
| expect(data).toEqual(expected.data); | ||
| }); | ||
| validateResult(result); | ||
| }); | ||
| it('can make ssr query', async () => { | ||
| const client = createClient({ ...options, ssr: true }); | ||
| describe('query ssr', () => { | ||
| const expected = JSON.parse(response); | ||
| const result = client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| }); | ||
| let client: IMicroGraphQLClient; | ||
| beforeEach(() => { | ||
| global.fetch.resetMocks(); | ||
| global.fetch.mockResponse(response); | ||
| await client.resolveQueries(); | ||
| client = createClient({ | ||
| cache: createCache(), | ||
| fetch: global.fetch, | ||
| url: 'https://swapi-graphql.netlify.com/.netlify/functions/index', | ||
| ssr: true | ||
| }); | ||
| validateResult(await result); | ||
| }); | ||
| it('can set provided cache', async () => { | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: createCache() | ||
| client.query(query, variables); | ||
| }); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| }); | ||
| validateResult(result); | ||
| it('can resolve queries', async () => { | ||
| await client.resolveQueries(); | ||
| const secondResult = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| const result = await client.query(query, variables); | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| }); | ||
| expect(secondResult.data).toBe(result.data); | ||
| }); | ||
| it('can skip cache', async () => { | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: createCache() | ||
| }); | ||
| describe('mutate', () => { | ||
| const expected = JSON.parse(response); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| }); | ||
| validateResult(result); | ||
| let client: IMicroGraphQLClient; | ||
| let promise: Promise<IMicroGraphQLResult<unknown>>; | ||
| beforeEach(() => { | ||
| global.fetch.resetMocks(); | ||
| global.fetch.mockResponse(response); | ||
| const secondResult = await client.query<IQueryResult, {}>(query, { | ||
| skipCache: true, | ||
| variables | ||
| client = createClient({ | ||
| cache: createCache(), | ||
| fetch: global.fetch, | ||
| url: 'https://swapi-graphql.netlify.com/.netlify/functions/index' | ||
| }); | ||
| promise = client.mutate(query, variables); | ||
| }); | ||
| expect(secondResult.data).not.toBe(result.data); | ||
| expect(secondResult.data).toEqual(result.data); | ||
| }); | ||
| it('can receive subscription value', async () => { | ||
| const client = createClient({ | ||
| ...options, | ||
| cache: createCache() | ||
| it('can mutate', async () => { | ||
| const result = await promise; | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| }); | ||
| let subscriptionCount = 0; | ||
| let dataFromSubscription: IMicroGraphQLResult<IQueryResult>; | ||
| const unsubscribe = client.subscribe<IQueryResult, {}>( | ||
| { query, variables }, | ||
| data => { | ||
| dataFromSubscription = data; | ||
| subscriptionCount += 1; | ||
| } | ||
| ); | ||
| it('can mutate multiple times', async () => { | ||
| const result = await promise; | ||
| expect(result).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(1); | ||
| const result = await client.query<IQueryResult, {}>(query, { | ||
| variables | ||
| const secondResult = await client.mutate(query, variables); | ||
| expect(secondResult).toEqual(expected); | ||
| expect(global.fetch.mock.calls.length).toBe(2); | ||
| }); | ||
| validateResult(result); | ||
| validateResult(dataFromSubscription!); | ||
| unsubscribe(); | ||
| const secondResult = await client.query<IQueryResult, {}>(query, { | ||
| skipCache: true, | ||
| variables | ||
| it('stores in cache', async () => { | ||
| await promise; | ||
| const data = client.cache.readQuery(query, variables); | ||
| expect(data).toEqual(expected.data); | ||
| }); | ||
| expect(secondResult.data).not.toBe(result.data); | ||
| expect(secondResult.data).toEqual(result.data); | ||
| expect(subscriptionCount).toBe(2); | ||
| }); | ||
| }); |
-32
| export function gql(strings: TemplateStringsArray, ...fragments: IMicroGraphQLFragment[]): string { | ||
| const frags = fragments.map(fragment => fragment.definition).join('\n').trim(); | ||
| return strings.map((str, i) => { | ||
| const cat = fragments[i] ? `...${fragments[i].name}` : ''; | ||
| return str + cat; | ||
| }).join('').trim() + (frags ? `\n${frags}` : ''); | ||
| } | ||
| export interface IMicroGraphQLFragment { | ||
| definition: string; | ||
| name: string; | ||
| } | ||
| export const noFragmentNameError = 'no name found for the provided fragment'; | ||
| export function frag( | ||
| strings: TemplateStringsArray | ||
| ): IMicroGraphQLFragment { | ||
| const definition = gql(strings); | ||
| const name = definition.match(/fragment\s([A-z][\w\d]*)\son/); | ||
| if (!name || !name[1]) { | ||
| throw new Error(noFragmentNameError); | ||
| } | ||
| return { | ||
| definition, | ||
| name: name[1] | ||
| }; | ||
| } |
-107
| import { frag, gql, noFragmentNameError } from '../src'; | ||
| describe('gql', () => { | ||
| it('should support no fragments', () => { | ||
| const tag = gql` | ||
| query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| } | ||
| } | ||
| `; | ||
| expect(tag).toBe(`query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| } | ||
| }`); | ||
| }); | ||
| it('throws if no fragment name', () => { | ||
| expect(() => frag` | ||
| fragment on Film { | ||
| id | ||
| } | ||
| `).toThrow(noFragmentNameError); | ||
| }); | ||
| it('can parse fragment', () => { | ||
| const fragTag = frag` | ||
| fragment FilmInfo on Film { | ||
| id | ||
| } | ||
| `; | ||
| expect(fragTag.name).toBe('FilmInfo'); | ||
| expect(fragTag.definition).toBe(`fragment FilmInfo on Film { | ||
| id | ||
| }`); | ||
| }); | ||
| it('should support single fragment', () => { | ||
| const fragTag = frag` | ||
| fragment FilmInfo on Film { | ||
| id | ||
| } | ||
| `; | ||
| const tag = gql` | ||
| query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| ${fragTag} | ||
| } | ||
| } | ||
| `; | ||
| expect(tag).toBe(`query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| ...FilmInfo | ||
| } | ||
| } | ||
| fragment FilmInfo on Film { | ||
| id | ||
| }`); | ||
| }); | ||
| it('should support multiple fragment', () => { | ||
| const fragTag1 = frag` | ||
| fragment FilmInfo on Film { | ||
| id | ||
| } | ||
| `; | ||
| const fragTag2 = frag` | ||
| fragment FilmDetails on Film { | ||
| id | ||
| episodeID | ||
| } | ||
| `; | ||
| const tag = gql` | ||
| query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| ${fragTag1} | ||
| ${fragTag2} | ||
| } | ||
| } | ||
| `; | ||
| expect(tag).toBe(`query TestQuery($id: ID) { | ||
| film(filmID: $id) { | ||
| title | ||
| ...FilmInfo | ||
| ...FilmDetails | ||
| } | ||
| } | ||
| fragment FilmInfo on Film { | ||
| id | ||
| } | ||
| fragment FilmDetails on Film { | ||
| id | ||
| episodeID | ||
| }`); | ||
| }); | ||
| }); |
Sorry, the diff of this file is not supported yet
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
193282
4.78%40
25%1028
10.42%2
Infinity%4
300%3
50%34
142.86%+ Added
+ Added
+ Added
+ Added
+ Added