Skip to content

Commit 3ce47ae

Browse files
committed
feat: Add buildSchema() script & isomorphic graphqlQuery() helper
1 parent 2f51f98 commit 3ce47ae

22 files changed

+442
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules/
22
node_modules
33
.env
4+
.env.local
45

56
# -- @generated: @expo/[email protected] --
67

apps/expo/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"typescript": "5.3.3"
2626
},
2727
"scripts": {
28-
"dev": "expo start",
29-
"start": "expo start",
28+
"dev": "expo start --clear",
29+
"start": "expo start --clear",
3030
"android": "expo start --android",
3131
"ios": "expo start --ios",
3232
"web": "expo start --web",

apps/expo/tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"extends": "@app/core/tsconfig",
3-
"include": ["**/*.ts", "**/*.tsx"],
3+
"include": [
4+
"**/*.ts",
5+
"**/*.tsx",
6+
"../../features/app-core/graphql-env.d.ts"
7+
],
48
"exclude": ["node_modules"]
59
}

apps/next/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"include": [
44
"**/*.ts",
55
"**/*.tsx",
6-
".next/types/**/*.ts"
6+
".next/types/**/*.ts",
7+
"../../features/app-core/graphql-env.d.ts"
78
],
89
"exclude": [
910
"node_modules"

features/app-core/appConfig.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Constants from 'expo-constants'
2+
3+
export const expoDebuggerHost = Constants?.expoGoConfig?.debuggerHost || Constants.manifest2?.extra?.expoGo?.debuggerHost // prettier-ignore
4+
export const localURL = expoDebuggerHost?.split?.(':').shift()
5+
6+
export const fallbackBaseURL = localURL ? `http://${localURL}:3000` : ''
7+
8+
export const appConfig = {
9+
baseURL: process.env.NEXT_PUBLIC_BASE_URL || process.env.EXPO_PUBLIC_BASE_URL || process.env.BASE_URL || `${fallbackBaseURL}`, // prettier-ignore
10+
backendURL: process.env.NEXT_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_BACKEND_URL || process.env.BACKEND_URL || `${fallbackBaseURL}`, // prettier-ignore
11+
apiURL: process.env.NEXT_PUBLIC_API_URL || process.env.EXPO_PUBLIC_API_URL || process.env.API_URL || `${fallbackBaseURL}/api`, // prettier-ignore
12+
graphURL: process.env.NEXT_PUBLIC_GRAPH_URL || process.env.EXPO_PUBLIC_GRAPH_URL || process.env.GRAPH_URL || `${fallbackBaseURL}/api/graphql`, // prettier-ignore
13+
} as const

features/app-core/graphql-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type introspection = {
1717
'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'healthCheck': { name: 'healthCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'HealthCheckData'; ofType: null; }; } }; }; };
1818
'HealthCheckArgs': { kind: 'INPUT_OBJECT'; name: 'HealthCheckArgs'; inputFields: [{ name: 'echo'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; };
1919
'String': unknown;
20-
'HealthCheckData': { kind: 'OBJECT'; name: 'HealthCheckData'; fields: { 'echo': { name: 'echo'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'alive': { name: 'alive'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'kicking': { name: 'kicking'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'now': { name: 'now'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'aliveTime': { name: 'aliveTime'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Float'; ofType: null; }; } }; 'aliveSince': { name: 'aliveSince'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'serverTimezone': { name: 'serverTimezone'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'requestHost': { name: 'requestHost'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'requestProtocol': { name: 'requestProtocol'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'requestURL': { name: 'requestURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'baseURL': { name: 'baseURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'apiURL': { name: 'apiURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'port': { name: 'port'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'debugPort': { name: 'debugPort'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'nodeVersion': { name: 'nodeVersion'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'v8Version': { name: 'v8Version'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemArch': { name: 'systemArch'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemPlatform': { name: 'systemPlatform'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemRelease': { name: 'systemRelease'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemFreeMemory': { name: 'systemFreeMemory'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'systemTotalMemory': { name: 'systemTotalMemory'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'systemLoadAverage': { name: 'systemLoadAverage'; type: { kind: 'LIST'; name: never; ofType: { kind: 'SCALAR'; name: 'Float'; ofType: null; }; } }; }; };
20+
'HealthCheckData': { kind: 'OBJECT'; name: 'HealthCheckData'; fields: { 'echo': { name: 'echo'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'alive': { name: 'alive'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'kicking': { name: 'kicking'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'now': { name: 'now'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'aliveTime': { name: 'aliveTime'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Float'; ofType: null; }; } }; 'aliveSince': { name: 'aliveSince'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'serverTimezone': { name: 'serverTimezone'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'requestHost': { name: 'requestHost'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'requestProtocol': { name: 'requestProtocol'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'requestURL': { name: 'requestURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'baseURL': { name: 'baseURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'backendURL': { name: 'backendURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'apiURL': { name: 'apiURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'graphURL': { name: 'graphURL'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'port': { name: 'port'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'debugPort': { name: 'debugPort'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'nodeVersion': { name: 'nodeVersion'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'v8Version': { name: 'v8Version'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemArch': { name: 'systemArch'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemPlatform': { name: 'systemPlatform'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemRelease': { name: 'systemRelease'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'systemFreeMemory': { name: 'systemFreeMemory'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'systemTotalMemory': { name: 'systemTotalMemory'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'systemLoadAverage': { name: 'systemLoadAverage'; type: { kind: 'LIST'; name: never; ofType: { kind: 'SCALAR'; name: 'Float'; ofType: null; }; } }; }; };
2121
'Boolean': unknown;
2222
'Float': unknown;
2323
'Int': unknown;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node --experimental-specifier-resolution=node
2+
import path from 'path'
3+
import fs from 'fs'
4+
import { fileURLToPath } from 'node:url'
5+
import { loadFilesSync } from '@graphql-tools/load-files'
6+
import { mergeTypeDefs } from '@graphql-tools/merge'
7+
import { print } from 'graphql'
8+
9+
/* --- Constants ------------------------------------------------------------------------------- */
10+
11+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
12+
const schemaPath = path.resolve(currentDir, 'schema.graphql')
13+
const typeDefsPath = path.resolve(currentDir, 'typeDefs.ts')
14+
15+
/** --- createSchemaDefinitions() -------------------------------------------------------------- */
16+
/** -i- Combine all custom and other (e.g. generated) graphql schema definitions */
17+
export const createSchemaDefinitions = () => {
18+
const rootDir = path.resolve(currentDir, '../../..')
19+
const schemaPathPattern = `${rootDir}/(features|packages)/**/!(schema).graphql`
20+
const customGraphQLDefinitions = loadFilesSync(schemaPathPattern)
21+
return mergeTypeDefs([
22+
...customGraphQLDefinitions,
23+
/* other typedefs? */
24+
])
25+
}
26+
27+
/* --- Script ---------------------------------------------------------------------------------- */
28+
29+
const buildSchemaDefinitions = async () => {
30+
const schemaDefinitions = createSchemaDefinitions()
31+
const typeDefsString = print(schemaDefinitions)
32+
fs.writeFileSync(schemaPath, typeDefsString)
33+
fs.writeFileSync(typeDefsPath, `export const typeDefs = \`${typeDefsString}\``)
34+
}
35+
36+
buildSchemaDefinitions()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { print } from 'graphql/language/printer'
2+
import type { TadaDocumentNode, ResultOf } from 'gql.tada'
3+
import type { QueryConfig } from './graphqlQuery.types'
4+
import { appConfig } from '../appConfig'
5+
6+
/** --- graphqlQuery --------------------------------------------------------------------------- */
7+
/** -i- Isomorphic graphql request, using the graphql endpoint in browser / native, and the executable schema serverside */
8+
export const graphqlQuery = async <T extends TadaDocumentNode, R = ResultOf<T>>(query: T, config?: QueryConfig<T>) => {
9+
// Config
10+
const { variables, headers, graphqlEndpoint } = config || {}
11+
12+
// Vars
13+
const queryString = print(query)
14+
15+
// -- Native: Execute query with fetch --
16+
17+
try {
18+
const { graphURL } = appConfig
19+
const fetchURL = graphqlEndpoint || graphURL
20+
console.warn({ graphURL, fetchURL, queryString, variables })
21+
const res = await fetch(fetchURL, {
22+
method: 'POST',
23+
headers: {
24+
'Content-Type': 'application/json',
25+
...headers,
26+
},
27+
body: JSON.stringify({ query: queryString, variables }),
28+
})
29+
const { data, errors } = await res.json()
30+
if (errors) throw new Error(errors[0].message)
31+
return data as R
32+
} catch (error) {
33+
throw new Error(error)
34+
}
35+
}
36+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { TadaDocumentNode, VariablesOf } from 'gql.tada'
2+
3+
export type QueryConfig<T extends TadaDocumentNode> = {
4+
variables?: VariablesOf<T>
5+
headers?: Record<string, string>
6+
graphqlEndpoint?: string
7+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { print } from 'graphql/language/printer'
2+
import type { TadaDocumentNode, ResultOf } from 'gql.tada'
3+
import type { QueryConfig } from './graphqlQuery.types'
4+
import { appConfig } from '../appConfig'
5+
6+
/** --- graphqlQuery --------------------------------------------------------------------------- */
7+
/** -i- Isomorphic graphql request, using the graphql endpoint in browser / native, and the executable schema serverside */
8+
export const graphqlQuery = async <T extends TadaDocumentNode, R = ResultOf<T>>(query: T, config?: QueryConfig<T>) => {
9+
// Config
10+
const { variables, headers, graphqlEndpoint } = config || {}
11+
12+
// Flags
13+
const isServer = typeof window === 'undefined'
14+
15+
// Vars
16+
const queryString = print(query)
17+
18+
// -- Server: Execute query with lazy loaded schema --
19+
20+
if (isServer) {
21+
try {
22+
const [
23+
{ graphql },
24+
{ executableSchema },
25+
] = await Promise.all([
26+
import('graphql'),
27+
import('./schema'),
28+
])
29+
const { data } = await graphql({
30+
schema: executableSchema,
31+
source: queryString,
32+
variableValues: variables,
33+
}) as { data: R }
34+
return data
35+
} catch (error) {
36+
throw new Error(error)
37+
}
38+
}
39+
40+
// -- Browser: Execute query with fetch --
41+
42+
try {
43+
const { graphURL } = appConfig
44+
const fetchURL = graphqlEndpoint || graphURL
45+
const res = await fetch(fetchURL, {
46+
method: 'POST',
47+
headers: {
48+
'Content-Type': 'application/json',
49+
...headers,
50+
},
51+
body: JSON.stringify({ query: queryString, variables }),
52+
})
53+
const { data, errors } = await res.json()
54+
if (errors) throw new Error(errors[0].message)
55+
return data as R
56+
} catch (error) {
57+
throw new Error(error)
58+
}
59+
}
60+

0 commit comments

Comments
 (0)