Skip to content

Commit

Permalink
wip - schema returned, delegateToSchema undefined
Browse files Browse the repository at this point in the history
  • Loading branch information
damassi committed Aug 30, 2021
1 parent 9acc87c commit 9a68b90
Show file tree
Hide file tree
Showing 11 changed files with 613 additions and 18 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
"@babel/preset-typescript": "7.3.3",
"@babel/register": "7.4.4",
"@graphql-tools/delegate": "6.0.10",
"@graphql-tools/graphql-file-loader": "^7.0.6",
"@graphql-tools/load": "^7.1.9",
"@graphql-tools/schema": "^8.1.2",
"@graphql-tools/stitch": "^8.2.1",
"@graphql-tools/wrap": "^8.0.13",
"@heroku/foreman": "2.0.2",
"@sentry/node": "5.18.1",
"accounting": "0.4.1",
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const {
ENABLE_QUERY_TRACING,
ENABLE_REQUEST_LOGGING,
DISABLE_SCHEMA_STITCHING,
ENABLE_EXPERIMENTAL_STITCHING_MIGRATION,
ENABLE_RESOLVER_BATCHING,
EXCHANGE_API_BASE,
EXCHANGE_APP_ID,
Expand Down Expand Up @@ -162,6 +163,8 @@ export default {
EMBEDLY_KEY,
ENABLE_APOLLO: ENABLE_APOLLO === "true",
ENABLE_ASYNC_STACK_TRACES,
ENABLE_EXPERIMENTAL_STITCHING_MIGRATION:
ENABLE_EXPERIMENTAL_STITCHING_MIGRATION === "true",
ENABLE_METRICS,
ENABLE_QUERY_TRACING,
ENABLE_REQUEST_LOGGING,
Expand Down
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createLoaders } from "./lib/loaders"
import depthLimit from "graphql-depth-limit"
import express from "express"
import { schema as schemaV1 } from "./schema/v1"
import { schema as schemaV2 } from "./schema/v2"
import { getSchema as getSchemaV2 } from "./schema/v2"
import moment from "moment-timezone"
import morgan from "artsy-morgan"
import { fetchPersistedQuery } from "./lib/fetchPersistedQuery"
Expand Down Expand Up @@ -267,8 +267,15 @@ function startApp(appSchema, path: string) {

const app = express()

// This order is important for dd-trace to be able to find the nested routes.
app.use("/v2", startApp(schemaV2, "/"))
app.use("/", startApp(schemaV1, "/"))
;(async () => {
try {
const schemaV2 = await getSchemaV2()
// This order is important for dd-trace to be able to find the nested routes.
app.use("/v2", (req, res, next) => startApp(schemaV2, "/")(req, res, next))
app.use("/", (req, res, next) => startApp(schemaV1, "/")(req, res, next))
} catch (error) {
console.log(error)
}
})()

export default app
12 changes: 12 additions & 0 deletions src/lib/stitching2/kaws/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import urljoin from "url-join"
import config from "config"
import { createRemoteExecutor } from "../lib/createRemoteExecutor"
import { responseLoggerMiddleware } from "../middleware/responseLoggerMiddleware"

const { KAWS_API_BASE } = config

export const createKawsExecutor = () => {
return createRemoteExecutor(urljoin(KAWS_API_BASE, "graphql"), {
middleware: [responseLoggerMiddleware("Kaws")],
})
}
28 changes: 28 additions & 0 deletions src/lib/stitching2/kaws/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createKawsExecutor } from "./link"
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"
import { RenameTypes, RenameRootFields } from "@graphql-tools/wrap"
import { loadSchema } from "@graphql-tools/load"
import { GraphQLSchema } from "graphql"

export const executableKawsSchema = async () => {
const kawsExecutor = createKawsExecutor()
const kawsSchema: GraphQLSchema = await loadSchema("src/data/kaws.graphql", {
loaders: [new GraphQLFileLoader()],
})

const schema = {
schema: kawsSchema,
executor: kawsExecutor,
transforms: [
new RenameTypes((name) => {
return `Marketing${name}`
}),
new RenameRootFields(
(_operation, name) =>
`marketing${name.charAt(0).toUpperCase() + name.slice(1)}`
),
],
}

return schema
}
215 changes: 215 additions & 0 deletions src/lib/stitching2/kaws/v2/stitching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { GraphQLSchema, GraphQLFieldConfigArgumentMap } from "graphql"
import {
pageableFilterArtworksArgsWithInput,
filterArtworksArgs,
} from "schema/v2/filterArtworksConnection"
import gql from "lib/gql"
import { printType } from "lib/stitching/lib/printType"

export const kawsStitchingEnvironmentV2 = (
localSchema: GraphQLSchema,
kawsSchema: GraphQLSchema & { transforms?: any }
) => {
return {
// The SDL used to declare how to stitch an object
extensionSchema: gql`
extend type Artist {
marketingCollections(slugs: [String!], category: String, randomizationSeed: String, size: Int, isFeaturedArtistContent: Boolean, showOnEditorial: Boolean): [MarketingCollection]
}
extend type Fair {
marketingCollections(size: Int): [MarketingCollection]!
}
extend type Viewer {
marketingCollections(slugs: [String!], category: String, randomizationSeed: String, size: Int, isFeaturedArtistContent: Boolean, showOnEditorial: Boolean, artistID: String): [MarketingCollection]
}
extend type MarketingCollection {
internalID: ID!
artworksConnection(${argsToSDL(
pageableFilterArtworksArgsWithInput
).join("\n")}): FilterArtworksConnection
}
type HomePageMarketingCollectionsModule {
results: [MarketingCollection]!
}
extend type HomePage {
marketingCollectionsModule: HomePageMarketingCollectionsModule
}
`,
// Resolvers for the above, this passes in ALL potential parameters
// from KAWS into filter_artworks to allow end users to dynamically
// modify query filters using an admin tool
resolvers: {
Artist: {
marketingCollections: {
fragment: `
... on Artist {
internalID
}
`,
resolve: ({ internalID: artistID }, args, context, info) => {
return info.mergeInfo.delegateToSchema({
schema: kawsSchema,
operation: "query",
fieldName: "marketingCollections",

args: {
artistID,
...args,
},
context,
info,
})
},
},
},
Fair: {
marketingCollections: {
fragment: `
... on Fair {
kawsCollectionSlugs
}
`,
resolve: ({ kawsCollectionSlugs: slugs }, args, context, info) => {
if (slugs.length === 0) return []
return info.mergeInfo.delegateToSchema({
schema: kawsSchema,
operation: "query",
fieldName: "marketingCollections",

args: {
slugs,
...args,
},
context,
info,
})
},
},
},
HomePage: {
marketingCollectionsModule: {
fragment: gql`
... on HomePage {
__typename
}
`,
resolve: () => {
return {}
},
},
},
HomePageMarketingCollectionsModule: {
results: {
fragment: gql`
... on HomePageMarketingCollectionsModule {
__typename
}
`,
resolve: async (_source, _args, context, info) => {
try {
// We hard-code the collections slugs here in MP so that the app
// can display different collections based only on an MP change
// (and not an app deploy).
return await info.mergeInfo.delegateToSchema({
schema: kawsSchema,
operation: "query",
fieldName: "marketingCollections",
args: {
slugs: [
"new-this-week",
"auction-highlights",
"trending-emerging-artists",
],
},
context,
info,
})
} catch (error) {
// The schema guarantees a present array for results, so fall back
// to an empty one if the request to kaws fails. Note that we
// still bubble-up any errors in the GraphQL response.
return []
}
},
},
},
Viewer: {
marketingCollections: {
fragment: gql`
...on Viewer {
__typename
}
`,
resolve: async (_source, args, context, info) => {
return await info.mergeInfo.delegateToSchema({
schema: kawsSchema,
operation: "query",
fieldName: "marketingCollections",

args,
context,
info,
})
},
},
},
MarketingCollection: {
artworksConnection: {
fragment: `
fragment MarketingCollectionQuery on MarketingCollection {
query {
${Object.keys(filterArtworksArgs).join("\n")}
}
}
`,
resolve: (parent, _args, context, info) => {
const query = parent.query
const hasKeyword = Boolean(parent.query.keyword)

const existingLoader =
context.unauthenticatedLoaders.filterArtworksLoader
const newLoader = (loaderParams) => {
return existingLoader.call(null, loaderParams, {
requestThrottleMs: 1000 * 60 * 60,
})
}

// TODO: Should this really modify the context in place?
context.unauthenticatedLoaders.filterArtworksLoader = newLoader

return info.mergeInfo.delegateToSchema({
schema: localSchema,
operation: "query",
fieldName: "artworksConnection",
args: {
...query,
keywordMatchExact: hasKeyword,
..._args,
},
context,
info,
})
},
},
internalID: {
fragment: `
fragment MarketingCollectionIDQuery on MarketingCollection {
id
}
`,
resolve: ({ id }, _args, _context, _info) => id,
},
},
},
}
}

// Very contrived version of what exists in graphql-js but isn’t exported.
// https://github.com/graphql/graphql-js/blob/master/src/utilities/schemaPrinter.js
function argsToSDL(args: GraphQLFieldConfigArgumentMap) {
const result: string[] = []
Object.keys(args).forEach((argName) => {
result.push(`${argName}: ${printType(args[argName].type)}`)
})
return result
}
66 changes: 66 additions & 0 deletions src/lib/stitching2/lib/createRemoteExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import fetch from "node-fetch"
import { print } from "graphql"
import { ExecutionParams, Executor } from "@graphql-tools/delegate"
import { ResolverContext } from "types/graphql"

/**
* The parameter that's passed down to an executor's middleware
*/
interface ExecutorMiddlewareOperationParameter
extends ExecutionParams<unknown, ResolverContext> {
/** The operation's parsed result payload */
result: unknown
/** A stringified representation of the operation */
text: string
}

export type ExecutorMiddleware = (
operation: ExecutorMiddlewareOperationParameter
) => ExecutorMiddlewareOperationParameter

interface ExecutorOptions {
/** Middleware runs at the end of the operation execution */
middleware?: ExecutorMiddleware[]
}

/**
*
* @param graphqlURI URI to the remote graphql service
* @param options Object used to specify middleware or other configuration for the executor
*/
export const createRemoteExecutor = (
graphqlURI: string,
options: ExecutorOptions = {}
) => {
const { middleware = [] } = options

return async ({
document,
variables,
...otherOptions
}): Promise<Executor> => {
const query = print(document)
const fetchResult = await fetch(graphqlURI, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
})
const result = await fetchResult.json()
if (middleware.length) {
return middleware.reduce(
(acc, middleware) =>
middleware({
document,
variables,
text: query,
result: acc,
...otherOptions,
}),
result
).result
}
return result
}
}
Loading

0 comments on commit 9a68b90

Please sign in to comment.