Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
1 change: 1 addition & 0 deletions apps/expo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"include": [
"**/*.ts",
"**/*.tsx",
"../../features/app-core/graphql-env.d.ts",
"../../features/**/*.tsx",
"../../features/**/*.ts",
"../../packages/**/*.tsx",
Expand Down
12 changes: 12 additions & 0 deletions apps/next/app/(main)/api/graphql/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { NextRequest } from 'next/server'
import { createGraphQLServerHandler } from '@app/core/graphql/graphqlServer'

/* --- Handler --------------------------------------------------------------------------------- */

const handler = createGraphQLServerHandler()

/* --- /api/graphql ---------------------------------------------------------------------------- */

export const GET = (req: NextRequest) => handler(req)

export const POST = (req: NextRequest) => handler(req)
1 change: 1 addition & 0 deletions apps/next/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../features/app-core/graphql-env.d.ts",
"../../features/**/*.tsx",
"../../features/**/*.ts",
"../../packages/**/*.tsx",
Expand Down
33 changes: 33 additions & 0 deletions features/app-core/graphql-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable */
/* prettier-ignore */

/** An IntrospectionQuery representation of your schema.
*
* @remarks
* This is an introspection of your schema saved as a file by GraphQLSP.
* It will automatically be used by `gql.tada` to infer the types of your GraphQL documents.
* If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to
* instead save to a .ts instead of a .d.ts file.
*/
export type introspection = {
query: 'Query';
mutation: never;
subscription: never;
types: {
'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'healthCheck': { name: 'healthCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'HealthCheckData'; ofType: null; }; } }; }; };
'HealthCheckArgs': { kind: 'INPUT_OBJECT'; name: 'HealthCheckArgs'; inputFields: [{ name: 'echo'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; };
'String': unknown;
'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; }; } }; }; };
'Boolean': unknown;
'Float': unknown;
'Int': unknown;
};
};

import * as gqlTada from 'gql.tada';

declare module 'gql.tada' {
interface setupSchema {
introspection: introspection
}
}
36 changes: 36 additions & 0 deletions features/app-core/graphql/buildSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node --experimental-specifier-resolution=node
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'node:url'
import { loadFilesSync } from '@graphql-tools/load-files'
import { mergeTypeDefs } from '@graphql-tools/merge'
import { print } from 'graphql'

/* --- Constants ------------------------------------------------------------------------------- */

const currentDir = path.dirname(fileURLToPath(import.meta.url))
const schemaPath = path.resolve(currentDir, 'schema.graphql')
const typeDefsPath = path.resolve(currentDir, 'typeDefs.ts')

/** --- createSchemaDefinitions() -------------------------------------------------------------- */
/** -i- Combine all custom and other (e.g. generated) graphql schema definitions */
export const createSchemaDefinitions = () => {
const rootDir = path.resolve(currentDir, '../../..')
const schemaPathPattern = `${rootDir}/(features|packages)/**/!(schema).graphql`
const customGraphQLDefinitions = loadFilesSync(schemaPathPattern)
return mergeTypeDefs([
...customGraphQLDefinitions,
/* other typedefs? */
])
}

/* --- Script ---------------------------------------------------------------------------------- */

const buildSchemaDefinitions = async () => {
const schemaDefinitions = createSchemaDefinitions()
const typeDefsString = print(schemaDefinitions)
fs.writeFileSync(schemaPath, typeDefsString)
fs.writeFileSync(typeDefsPath, `export const typeDefs = \`${typeDefsString}\``)
}

buildSchemaDefinitions()
35 changes: 35 additions & 0 deletions features/app-core/graphql/graphqlQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { print } from 'graphql/language/printer'
import type { TadaDocumentNode, ResultOf } from 'gql.tada'
import type { QueryConfig } from './graphqlQuery.types'
import { appConfig } from '../appConfig'

/** --- graphqlQuery --------------------------------------------------------------------------- */
/** -i- Isomorphic graphql request, uses the graphql endpoint in browser & mobile, but the executable schema serverside */
export const graphqlQuery = async <T extends TadaDocumentNode, R = ResultOf<T>>(query: T, config?: QueryConfig<T>) => {
// Config
const { variables, headers, graphqlEndpoint } = config || {}

// Vars
const queryString = print(query)

// -- Native: Execute query with fetch --

try {
const { graphURL } = appConfig
const fetchURL = graphqlEndpoint || graphURL
const res = await fetch(fetchURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({ query: queryString, variables }),
})
const { data, errors } = await res.json()
if (errors) throw new Error(errors[0].message)
return data as R
} catch (error) {
throw new Error(error)
}
}

7 changes: 7 additions & 0 deletions features/app-core/graphql/graphqlQuery.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { TadaDocumentNode, VariablesOf } from 'gql.tada'

export type QueryConfig<T extends TadaDocumentNode> = {
variables?: VariablesOf<T>
headers?: Record<string, string>
graphqlEndpoint?: string
}
60 changes: 60 additions & 0 deletions features/app-core/graphql/graphqlQuery.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { print } from 'graphql/language/printer'
import type { TadaDocumentNode, ResultOf } from 'gql.tada'
import type { QueryConfig } from './graphqlQuery.types'
import { appConfig } from '../appConfig'

/** --- graphqlQuery --------------------------------------------------------------------------- */
/** -i- Isomorphic graphql request, uses the graphql endpoint in browser & mobile, but the executable schema serverside */
export const graphqlQuery = async <T extends TadaDocumentNode, R = ResultOf<T>>(query: T, config?: QueryConfig<T>) => {
// Config
const { variables, headers, graphqlEndpoint } = config || {}

// Flags
const isServer = typeof window === 'undefined'

// Vars
const queryString = print(query)

// -- Server: Execute query with lazy loaded schema --

if (isServer) {
try {
const [
{ graphql },
{ executableSchema },
] = await Promise.all([
import('graphql'),
import('./schema'),
])
const { data } = await graphql({
schema: executableSchema,
source: queryString,
variableValues: variables,
}) as { data: R }
return data
} catch (error) {
throw new Error(error)
}
}

// -- Browser: Execute query with fetch --

try {
const { graphURL } = appConfig
const fetchURL = graphqlEndpoint || graphURL
const res = await fetch(fetchURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({ query: queryString, variables }),
})
const { data, errors } = await res.json()
if (errors) throw new Error(errors[0].message)
return data as R
} catch (error) {
throw new Error(error)
}
}

23 changes: 23 additions & 0 deletions features/app-core/graphql/graphqlServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApolloServer } from '@apollo/server'
import type { NextRequest } from 'next/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { createRequestContext } from '../middleware/createRequestContext'
import { schemaBundle } from './schema'

/* --- Apollo Server --------------------------------------------------------------------------- */

export const graphqlServer = new ApolloServer({
typeDefs: schemaBundle.typeDefs,
resolvers: schemaBundle.resolvers,
introspection: true,
})

/** --- createGraphQLServerHandler() ----------------------------------------------------------- */
/** -i- Create the apollo graphql server handler for Next.js API router and provides context to resolvers */
export const createGraphQLServerHandler = () => {
return startServerAndCreateNextHandler(graphqlServer, {
context: async (req: NextRequest) => {
return await createRequestContext({ req })
},
})
}
39 changes: 39 additions & 0 deletions features/app-core/graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
type Query {
healthCheck(args: HealthCheckArgs): HealthCheckData!
}

input HealthCheckArgs {
echo: String
}

type HealthCheckData {
echo: String
status: String!
alive: Boolean!
kicking: Boolean!
now: String!
aliveTime: Float!
aliveSince: String!
serverTimezone: String!
requestHost: String
requestProtocol: String
requestURL: String
baseURL: String
backendURL: String
apiURL: String
graphURL: String
port: Int
debugPort: Int
nodeVersion: String
v8Version: String
systemArch: String
systemPlatform: String
systemRelease: String
systemFreeMemory: Float
systemTotalMemory: Float
systemLoadAverage: [Float]
}

schema {
query: Query
}
42 changes: 42 additions & 0 deletions features/app-core/graphql/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mergeResolvers } from '@graphql-tools/merge'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { gql } from 'graphql-tag'
import type { RequestContext } from '../middleware/createRequestContext'
import { typeDefs } from './typeDefs'
import { healthCheck } from '../resolvers/healthCheck'

/** --- createResolver() ----------------------------------------------------------------------- */
/** -i- Helper to wrap a resolver function and map context and args to it */
export const createResolver = <T>(resolver: (input: { args: T, context: RequestContext }) => Promise<unknown>) => {
return async (parent: unknown, { args }: { args: T }, context: RequestContext, info: unknown) => {
return resolver({ args, context: { ...context, parent, info } })
}
}

/* --- Custom Resolvers ------------------------------------------------------------------------ */

const customResolvers = {
Query: {
healthCheck: createResolver(healthCheck),
},
}

/** --- createRootResolver() ------------------------------------------------------------------- */
/** -i- Combine all custom and other (e.g. injected) resolvers */
export const createRootResolver = () => mergeResolvers([
customResolvers,
/* other resolvers? */
])

/* --- Schema ---------------------------------------------------------------------------------- */

export const schemaBundle = {
typeDefs: gql`${typeDefs}`,
resolvers: createRootResolver(),
}

export const executableSchema = makeExecutableSchema(schemaBundle)

/* --- Exports --------------------------------------------------------------------------------- */

export default executableSchema
39 changes: 39 additions & 0 deletions features/app-core/graphql/typeDefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const typeDefs = `type Query {
healthCheck(args: HealthCheckArgs): HealthCheckData!
}

input HealthCheckArgs {
echo: String
}

type HealthCheckData {
echo: String
status: String!
alive: Boolean!
kicking: Boolean!
now: String!
aliveTime: Float!
aliveSince: String!
serverTimezone: String!
requestHost: String
requestProtocol: String
requestURL: String
baseURL: String
backendURL: String
apiURL: String
graphURL: String
port: Int
debugPort: Int
nodeVersion: String
v8Version: String
systemArch: String
systemPlatform: String
systemRelease: String
systemFreeMemory: Float
systemTotalMemory: Float
systemLoadAverage: [Float]
}

schema {
query: Query
}`
15 changes: 13 additions & 2 deletions features/app-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
"name": "@app/core",
"version": "1.0.0",
"private": true,
"dependencies": {},
"type": "module",
"dependencies": {
"@apollo/server": "^4.10.2",
"@as-integrations/next": "^3.0.0",
"@graphql-tools/load-files": "^7.0.0",
"gql.tada": "^1.4.3",
"graphql-tag": "^2.12.6"
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.9.1",
"ts-node": "^10.9.2",
"typescript": "5.3.3"
},
"scripts": {}
"scripts": {
"build:schema": "node -r ts-node/register --loader ts-node/esm --experimental-specifier-resolution=node ./graphql/buildSchema.ts"
}
}
Loading