Skip to content

Commit 926078d

Browse files
committed
added initial implementation
1 parent 77876ef commit 926078d

File tree

3 files changed

+315
-5
lines changed

3 files changed

+315
-5
lines changed

README.md

+74
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,76 @@
11
# mst-gql
2+
23
Bindings for mobx-state-tree and GraphQL
4+
5+
---
6+
7+
Why
8+
9+
Pro:
10+
11+
- model oriented
12+
- minimal re-renders
13+
- optimistic updates
14+
- local extensions, state
15+
- server reuse
16+
17+
\* Con:
18+
19+
- over fetching risk
20+
21+
\* Features
22+
23+
- optimistic updates
24+
- type reuse between models, TS, graphql (autocompletion)
25+
- No components were harmed
26+
- References
27+
- local storage caching
28+
- React context compatible
29+
- subscription support
30+
31+
## Getting started
32+
33+
### Obtaining graphql-schema
34+
35+
### Scaffolding
36+
37+
### Using the generated store and models
38+
39+
### Initialization transportation
40+
41+
### Connecting to React components with `observer`
42+
43+
### Using React context
44+
45+
### Simplifying queries with reflection
46+
47+
## Api
48+
49+
### MSTGQLStore
50+
51+
`query`
52+
53+
`mutate`
54+
55+
`subscribe`
56+
57+
### MSTGQLObject
58+
59+
### createHttpClient
60+
61+
### coreFields
62+
63+
### primitiveFields
64+
65+
## Tips & tricks
66+
67+
Should scaffolded files be generated
68+
69+
Fold sections in VSCode with this [extension](https://marketplace.visualstudio.com/items?itemName=maptz.regionfolder)
70+
71+
## Roadmap
72+
73+
- [ ] offline actions
74+
- [ ] cache query policy
75+
- [ ] support gql-tag
76+
- [ ] tests

src/mst-gql.ts

+236-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,236 @@
1-
export function hi() {}
1+
import {
2+
types,
3+
getEnv,
4+
getParent,
5+
recordPatches,
6+
getPropertyMembers,
7+
isPrimitiveType,
8+
IAnyModelType
9+
} from "mobx-state-tree"
10+
import {observable, extendObservable, action} from "mobx"
11+
12+
import {GraphQLClient} from "graphql-request"
13+
import {SubscriptionClient} from "subscriptions-transport-ws"
14+
15+
export interface QueryOptions {
16+
raw?: boolean
17+
// TODO: headers
18+
// TODO: cacheStrategy
19+
}
20+
21+
export type CaseHandlers<T, R> = {
22+
fetching(): R
23+
error(error: any): R
24+
data(data: T): R
25+
}
26+
27+
export interface QueryResult<T = unknown> extends Promise<T> {
28+
fetching: boolean
29+
data: T | undefined
30+
error: any
31+
refetch(): Promise<T>
32+
case<R>(handlers: CaseHandlers<T, R>): R
33+
}
34+
35+
export const MSTGQLStore = types.model("MSTGQLStore").actions(self => {
36+
const {
37+
gqlHttpClient,
38+
gqlWsClient
39+
}: {gqlHttpClient: GraphQLClient; gqlWsClient: SubscriptionClient} = getEnv(
40+
self
41+
)
42+
if (!gqlHttpClient && !gqlWsClient)
43+
throw new Error(
44+
"Either gqlHttpClient or gqlWsClient (or both) should provided in the MSTGQLStore environment"
45+
)
46+
47+
function merge(data: unknown) {
48+
if (Array.isArray(data)) return data.map(item => mergeHelper(self, item))
49+
else return mergeHelper(self, data)
50+
}
51+
52+
function makeSingleRequest(query: string, variables: any): Promise<any> {
53+
if (gqlHttpClient) return gqlHttpClient.request(query, variables)
54+
else {
55+
return new Promise((resolve, reject) => {
56+
gqlWsClient
57+
.request({
58+
query,
59+
variables
60+
})
61+
.subscribe({
62+
next(data) {
63+
resolve(data.data)
64+
},
65+
error: reject
66+
})
67+
})
68+
}
69+
}
70+
71+
function query<T>(
72+
query: string,
73+
variables?: any,
74+
options: QueryOptions = {}
75+
): QueryResult<T> {
76+
// TODO: support options.headers
77+
// TODO: support options.cacheStrategy
78+
const req = makeSingleRequest(query, variables)
79+
80+
const handleSuccess = action(data => {
81+
const value = getFirstValue(data)
82+
if (options.raw) {
83+
promise.fetching = false
84+
return Promise.resolve((promise.data = value))
85+
} else {
86+
try {
87+
promise.fetching = false
88+
const normalized = (self as any).merge(value)
89+
return Promise.resolve((promise.data = normalized))
90+
} catch (e) {
91+
return Promise.reject((promise.error = e))
92+
}
93+
}
94+
})
95+
96+
const handleFailure = action(error => {
97+
promise.fetching = false
98+
return Promise.reject((promise.error = error))
99+
})
100+
101+
const promise: QueryResult<T> = req.then(
102+
handleSuccess,
103+
handleFailure
104+
) as any
105+
extendObservable(
106+
promise,
107+
{
108+
fetching: true,
109+
data: undefined,
110+
error: undefined,
111+
refetch() {
112+
// refech returs the old observable states
113+
promise.fetching = false
114+
return makeSingleRequest(query, variables).then(
115+
handleSuccess,
116+
handleFailure
117+
)
118+
},
119+
case<R>(handlers: CaseHandlers<T, R>): R {
120+
return promise.fetching && !promise.data
121+
? handlers.fetching()
122+
: promise.error
123+
? handlers.error(promise.error)
124+
: handlers.data(promise.data!)
125+
}
126+
} as any,
127+
{data: observable.ref}
128+
)
129+
return promise
130+
}
131+
132+
function mutate<T>(
133+
mutation: string,
134+
params?: any,
135+
optimisticUpdate?: () => void
136+
): QueryResult<T> {
137+
if (optimisticUpdate) {
138+
const recorder = recordPatches(self)
139+
optimisticUpdate()
140+
recorder.stop()
141+
const promise = query<T>(mutation, params)
142+
promise.catch(e => {
143+
recorder.undo()
144+
})
145+
return promise
146+
} else {
147+
return query(mutation, params)
148+
}
149+
}
150+
151+
function subscribe(query: string, variables?: any): () => void {
152+
if (!gqlWsClient) throw new Error("No WS client available")
153+
const sub = gqlWsClient
154+
.request({
155+
query,
156+
variables
157+
})
158+
.subscribe({
159+
next(data) {
160+
;(self as any).merge(getFirstValue(data.data))
161+
}
162+
})
163+
return () => sub.unsubscribe()
164+
}
165+
166+
return {merge, mutate, query, subscribe}
167+
})
168+
169+
export const MSTGQLObject = types
170+
.model("MSTGQLObject", {
171+
__typename: types.string,
172+
id: types.identifier
173+
})
174+
.views(self => ({
175+
get store(): typeof MSTGQLStore.Type {
176+
return getParent(self, 2)
177+
}
178+
}))
179+
180+
export type HttpClientOptions = ConstructorParameters<typeof GraphQLClient>[1]
181+
182+
export function createHttpClient(url: string, options: HttpClientOptions = {}) {
183+
return new GraphQLClient(url, options)
184+
}
185+
186+
function typenameToCollectionName(typename: string) {
187+
return typename.toLowerCase() + "s"
188+
}
189+
190+
function mergeHelper(store: any, itemData: any) {
191+
const {__typename, id} = itemData
192+
if (__typename === undefined)
193+
throw new Error(
194+
"__typename field is not available on " + JSON.stringify(itemData)
195+
)
196+
if (id === undefined)
197+
throw new Error("id field is not available on " + JSON.stringify(itemData))
198+
const collection = typenameToCollectionName(__typename)
199+
const current = store[collection].get(id)
200+
if (!current) {
201+
store[collection].set(id, itemData)
202+
return store[collection].get(id)
203+
} else {
204+
// TODO: merge should be recursive for complex values
205+
Object.assign(current, itemData)
206+
return current
207+
}
208+
}
209+
210+
function getFirstValue(data: any) {
211+
const keys = Object.keys(data)
212+
if (keys.length !== 1)
213+
throw new Error(
214+
`Expected exactly one response key, got: ${keys.join(", ")}`
215+
)
216+
return data[keys[0]]
217+
}
218+
219+
export const coreFields = `\n__typename\nid\n`
220+
221+
export function primitiveFields(
222+
mstType: IAnyModelType,
223+
exclude: string[] = []
224+
) {
225+
const excludes = new Set(exclude)
226+
const primitives = new Set(["id", "__typename"])
227+
const reflectionData = getPropertyMembers(mstType)
228+
for (const key in reflectionData.properties)
229+
if (!excludes.has(key)) {
230+
const type = reflectionData.properties[key]
231+
if (isPrimitiveType(type)) primitives.add(key)
232+
}
233+
return Array.from(primitives).join("\n")
234+
}
235+
236+
// TODO: also have a utility for nested objects?

tsconfig.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@
2424

2525
/* Strict Type-Checking Options */
2626
"strict": true /* Enable all strict type-checking options. */,
27-
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
27+
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
2828
// "strictNullChecks": true, /* Enable strict null checks. */
2929
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
3030
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
3131
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
3232
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33-
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
33+
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
3434

3535
/* Additional Checks */
3636
// "noUnusedLocals": true, /* Report errors on unused locals. */
@@ -39,7 +39,7 @@
3939
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
4040

4141
/* Module Resolution Options */
42-
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
42+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
4343
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
4444
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
4545
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
@@ -58,5 +58,6 @@
5858
/* Experimental Options */
5959
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
6060
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
61-
}
61+
},
62+
"files": ["./src/mst-gql.ts"]
6263
}

0 commit comments

Comments
 (0)