adds some actual code. not ready yet

This commit is contained in:
Scott Tolinski 2021-09-30 10:19:53 -06:00
parent dae849234d
commit 5d40d6a60e
7 changed files with 545 additions and 12 deletions

3
.gitignore vendored
View file

@ -0,0 +1,3 @@
node_modules
node_modules/*
.DS_Store

View file

@ -34,9 +34,11 @@ docs coming soon
```
<script context="module" lang="ts">
// The generated function that fetches and caches
import { getSeriesList } from '../whatever'
export async function load() {
// Runs the cache/fetch function populating $gCache before use.
await getSeriesList({
limit: 0
})
@ -45,6 +47,7 @@ docs coming soon
</script>
<script lang="ts">
// Cache becomes populated with data available for SSR
import { gCache } from '@leveluptuts/gQuery'
// $: console.log($gCache.seriesList)
@ -56,10 +59,6 @@ docs coming soon
I guess if you want to do it this way you can.
## gFetch
The graphql fetcher client.
### 1. Initialize
```
@ -68,25 +67,31 @@ export const g = new GFetch({
})
```
Fetch
### 2. Create Fetch Function
This is a function that run a gFetch. A gFetch is a simple graphql fetcher that accepts an array of queries and variables.
The result of this function is your data. If you don't want the caching, you can just use this data directly.
```
const seriesList = ({ variables}) =>
g.fetch<SeriesListQuery>({
queries: [{ query: SeriesListDoc, variables }],
})
g.fetch({
queries: [{ query: SeriesListDoc, variables }],
})
```
Cache and Fetch
### 3. Cache and Fetch (optional)
If you want the caching into `$gCache`, you can pass your fetch func into the cacher and send the data to $gcache instead of using it directly.
Pass gQuery the query name, the fetch function and any variables you might have.
```
export async function getSeriesList(variables) {
async function getSeriesList(variables) {
await gQuery('seriesList', { query: seriesList, variables })
}
```
Use
### 4. Use in Svelte Kit App
```
<script context="module" lang="ts">
@ -114,6 +119,10 @@ Use
It's a Svelte Writable Store. So after a mutation you can quickly and easily manually update the cache.
### Q? Can't you update the cache magically for me?
### Q? Can't you update the cache magically for me after a mutation?
Maybe? If you want to be in charge of writing that bit, the door is open 😼
### Q? Why can't I use this yet?
It's changing too much rn, but will be available asap. Trust me, the sooner I get this done the better.

127
package-lock.json generated Normal file
View file

@ -0,0 +1,127 @@
{
"name": "gquery",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "gquery",
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"camel-case": "^4.1.2",
"graphql": "^15.6.0",
"pascal-case": "^3.1.2",
"svelte": "^3.43.0"
}
},
"node_modules/camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
"dependencies": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
}
},
"node_modules/graphql": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.6.0.tgz",
"integrity": "sha512-WJR872Zlc9hckiEPhXgyUftXH48jp2EjO5tgBBOyNMRJZ9fviL2mJBD6CAysk6N5S0r9BTs09Qk39nnJBkvOXQ==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/svelte": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.43.0.tgz",
"integrity": "sha512-T2pMPHrxXp+SM8pLLUXLQgkdo+JhTls7aqj9cD7z8wT2ccP+OrCAmtQS7h6pvMjitaZhXFNnCK582NxDpy8HSw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
},
"dependencies": {
"camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
"requires": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
}
},
"graphql": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.6.0.tgz",
"integrity": "sha512-WJR872Zlc9hckiEPhXgyUftXH48jp2EjO5tgBBOyNMRJZ9fviL2mJBD6CAysk6N5S0r9BTs09Qk39nnJBkvOXQ=="
},
"lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"requires": {
"tslib": "^2.0.3"
}
},
"no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"requires": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"svelte": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.43.0.tgz",
"integrity": "sha512-T2pMPHrxXp+SM8pLLUXLQgkdo+JhTls7aqj9cD7z8wT2ccP+OrCAmtQS7h6pvMjitaZhXFNnCK582NxDpy8HSw=="
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
}

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"author": "Scott Tolinski",
"bugs": {
"url": "https://github.com/leveluptuts/gQuery/issues"
},
"dependencies": {
"camel-case": "^4.1.2",
"graphql": "^15.6.0",
"pascal-case": "^3.1.2",
"svelte": "^3.43.0"
},
"description": "Not like jQuery. A GraphQL Fetcher & Cache for Svelte Kit",
"exports": {
"./gfetch": "src/gfetch.js"
},
"homepage": "https://github.com/leveluptuts/gQuery#readme",
"keywords": [
"graphql"
],
"license": "ISC",
"main": "src/index.js",
"name": "gquery",
"repository": {
"type": "git",
"url": "git+https://github.com/leveluptuts/gQuery.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"version": "0.0.1"
}

138
src/codegen.ts Normal file
View file

@ -0,0 +1,138 @@
import { CodegenPlugin } from '@graphql-codegen/plugin-helpers'
import { LoadedFragment } from '@graphql-codegen/visitor-plugin-common'
import { concatAST, FragmentDefinitionNode, Kind, OperationDefinitionNode, visit } from 'graphql'
import { pascalCase } from 'pascal-case'
const visitorPluginCommon = require('@graphql-codegen/visitor-plugin-common')
module.exports = {
plugin: (schema, documents, config, info) => {
const allAst = concatAST(documents.map((d) => d.document))
const allFragments: LoadedFragment[] = [
...(allAst.definitions.filter((d) => d.kind === Kind.FRAGMENT_DEFINITION) as FragmentDefinitionNode[]).map(
(fragmentDef) => ({
node: fragmentDef,
name: fragmentDef.name.value,
onType: fragmentDef.typeCondition.name.value,
isExternal: false,
})
),
...(config.externalFragments || []),
]
const visitor = new visitorPluginCommon.ClientSideBaseVisitor(
schema,
allFragments,
{},
{ documentVariableSuffix: 'Doc' },
documents
)
const visitorResult = visit(allAst, { leave: visitor })
const operations = allAst.definitions.filter(
(d) => d.kind === Kind.OPERATION_DEFINITION
) as OperationDefinitionNode[]
const defaultTypes = `
type FetchWrapperArgs<T> = {
fetch: typeof fetch,
variables?: T,
}
type SubscribeWrapperArgs<T> = {
variables?: T,
}
`
const ops = operations
.map((o) => {
if (o) {
let name = o?.name?.value || ''
// const dsl = `export const ${pascalCase(op.name.value)}Doc = gql\`
// ${documents.find((d) => d.rawSDL.includes(`${op.operation} ${op.name.value}`)).rawSDL}\``
const op = `${pascalCase(name)}${pascalCase(o.operation)}`
const opv = `${op}Variables`
let operations = ''
if (o.operation === 'query') {
operations += `
export const ${name} = ({ variables, fetch}: FetchWrapperArgs<${opv}>):
Promise<GFetchReturnWithErrors<${op}>> =>
g.fetch<${op}>({
queries: [{ query: ${pascalCase(name)}Doc, variables }],
fetch
})
`
// If config is set to have subscription query, also write the
if (config.subscriptionQuery) {
operations += `
export const ${name}Subscribe = ({ variables }: SubscribeWrapperArgs<${opv}>):
Readable<GFetchReturnWithErrors<${op}>> =>
g.oFetch<${op}>({
queries: [{ query: ${pascalCase(name)}Doc, variables }]
})
`
}
} else if (o.operation === 'mutation') {
operations += `
export const ${name} = ({ variables }: SubscribeWrapperArgs<${opv}>):
Promise<GFetchReturnWithErrors<${op}>> =>
g.fetch<${op}>({
queries: [{ query: ${pascalCase(name)}Doc, variables }],
fetch,
})
`
}
return operations
}
})
.join('\n')
let imports = [
`import type { Readable } from "svelte/store"`,
`import { g } from '${config.gFetchPath}'`,
`import type { GFetchReturnWithErrors } from '$graphql/gfetchLib'`,
`import gql from "graphql-tag"`,
]
// let schemaInputs = getCachedDocumentNodeFromSchema(schema).definitions.filter((d) => {
// return d.kind === 'InputObjectTypeDefinition'
// })
// let inputs = schemaInputs
// .map((d) => {
// console.log('/* START */')
// // @ts-ignore
// console.log('NAME: ', d.fields[0].name.value)
// // @ts-ignore
// let isReq = d.fields[0]?.type?.kind === 'NonNullType'
// console.log('REQUIRED: ', isReq ? '✅' : '❌')
// // @ts-ignore
// console.log('TYPE: ', isReq ? d.fields[0]?.type?.type?.name?.value : d.fields[0]?.type?.name?.value)
// // @ts-ignore
// // @ts-ignore
// console.log('d.fields[0]', d.fields[0]?.type)
// console.log('/* END */')
// console.log('')
// return `
// const inputName = {
// ${d.fields[0].name.value}: ${isReq ? d.fields[0]?.type?.type?.name?.value : d.fields[0]?.type?.name?.value}
// }
// `
// })
// .join('\n')
// console.log('inputs', inputs)
return {
prepend: imports,
content: [
defaultTypes,
visitor.fragments,
...visitorResult.definitions.filter((t) => typeof t == 'string'),
ops,
].join('\n'),
}
},
} as CodegenPlugin

185
src/gFetch.ts Normal file
View file

@ -0,0 +1,185 @@
import type { DocumentNode } from "graphql/language/ast";
import { readable, writable } from "svelte/store";
import type { Readable } from "svelte/store";
// What's the deal with *gFetch*?
// gFetch is a 0 dependency fetcher for graphql that accepts a custom fetch function
// This is useful if your platform provides you with a customer fetch. ie SvelteKit
// It also uses batched queries to batch several queries into one request.
// Two key exports from this file
// gFetch -> a fetcher that returns async
// ogFetch -> a fetcher that returns a subscription
// * Main Features *
// 1. 0 deps outside of graphql and svelte kit
// 2. Allows for batched queries
// 3. Plays nice with custom fetch functions
// 4. Doesn't use it's own cache, ie, would rely on Svelte's stores
export declare type GFetchQueryDefault = {
errors?: string[];
};
type OptionalPropertyNames<T> = {
[K in keyof T]-?: {} extends { [P in K]: T[K] } ? K : never;
}[keyof T];
type SpreadProperties<L, R, K extends keyof L & keyof R> = {
[P in K]: L[P] | Exclude<R[P], undefined>;
};
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type SpreadTwo<L, R> = Id<
Pick<L, Exclude<keyof L, keyof R>> &
Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>> &
Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>> &
SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;
type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R]
? SpreadTwo<L, Spread<R>>
: unknown;
export declare type GFetchQueryResult<F> = {
[k: string]: F;
};
export declare type GFetchQueries = {
query: DocumentNode;
variables?: Record<string, unknown>;
};
// This function accepts a graphql document and returns a string to be used
// in fetch calls
export function gqlToString(tag: DocumentNode): string {
return tag.loc.source.body;
}
type gFetchProperties = {
queries: GFetchQueries[];
fetch: typeof fetch;
};
export type ApolloClientOptions = {
path?: string;
};
export type ApolloClient = {
path?: string;
};
export type GFetchReturn<T> = {
data: T;
errors?: Error;
};
export type GFetchReturnWithErrors<T> = Spread<[T, GFetchQueryDefault]>;
export class GFetch extends Object {
public path: string;
constructor(options: ApolloClientOptions) {
super();
const { path } = options;
this.path = path;
this.fetch = this.fetch.bind(this);
this.oFetch = this.oFetch.bind(this);
}
// * gFetch
// This is a fetcher that returns a promise that resolves to a graphql response
public async fetch<T>({
queries,
fetch,
}: gFetchProperties): Promise<GFetchReturnWithErrors<T>> {
// Get all the queries and transform the docs into strings
// Fetching gql require them to be strings.
if (!fetch && window) {
fetch = window.fetch;
}
const newQueries = {
...queries[0],
query: gqlToString(queries[0].query),
};
// This is generic fetch, that is polyfilled via svelte kit
// graph ql fetches must be POST
// credentials include for user ssr data
const res = await fetch(this.path, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newQueries),
});
// Gets the data back from the server
const data = await res.json();
return {
...data.data,
} as GFetchReturnWithErrors<T>;
}
// * ogFetch
// This function is a fetcher that returns a svelte readable subscription
// This is to be used for client side fetching of data
public oFetch<F>({
queries,
}: {
queries: GFetchQueries[];
}): Readable<GFetchReturnWithErrors<F>> {
// 1. Build the store and initialize it as empty and error free
const initial = new Map();
// Creates a store that will be used to subscribe to the data
const store = readable(initial, this.makeSubscribe(initial, queries));
return store as unknown as Readable<GFetchReturnWithErrors<F>>;
}
// A dummy function that is used to make subscribe happy.
private unsubscribe() {
// Nothing to do in this case
}
// Part of ogFetch
// Designed this way to work will with Svelte's readable store
private makeSubscribe(data, queries) {
// Create a closure with access to the
// initial data and initialization arguments
return (set) => {
// 3. This won't get executed until the store has
// its first subscriber. Kick off retrieval.
this.fetchDataForSubscription(data, set, queries);
// We're not waiting for the response.
// Return the unsubscribe function which doesn't do
// do anything here (but is part of the stores protocol).
return this.unsubscribe;
};
}
// Part of ogFetch
// Runs gFetch and updates subscription
private async fetchDataForSubscription(data, set, queries: GFetchQueries[]) {
try {
// Dispatch the request for the users
// This code is ONLY run client side, so fetch comes globally from the browser
const response = await this.fetch({ queries, fetch });
set(response);
} catch (error) {
// 6b. if there is a fetch error - deal with it
// and let observers know
data.error = error;
set(data);
}
}
}
export const data = writable();
// ! IDEAS
// Mutations should take care of updating a generated writeable.
// import { tutorial } from '$graphql/state'
// import { updateTutorial } from '$graphql/gfetch.generated'
// updateTutorial()
// $tutorial is auto updated site wide
// Devtools based on svelte toy

39
src/index.ts Normal file
View file

@ -0,0 +1,39 @@
import { writable, get } from "svelte/store";
const newGCache = () => {
const { subscribe, update, set } = writable({});
async function hydrate(newData) {
update((old) => {
return {
...old,
...newData,
};
});
}
return {
subscribe,
set,
update,
hydrate,
};
};
export const gCache = newGCache();
export async function gQuery(typename, { query, variables }) {
const current = get(gCache);
// Extremely Naive cache
// Just checks to see if the data is there, if it is, don't
// Hit the network again
if (!current?.[typename]) {
const newData = await query({
variables,
fetch,
});
await gCache.hydrate(newData);
}
}