Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experiment] Attempt to upgrade stitching to latest #3375

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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
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", startApp(schemaV2, "/"))
app.use("/", startApp(schemaV1, "/"))
} catch (error) {
console.log(error)
}
})()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the double page refresh is required. Below we're returning export default app, which is mounted into the main express app but here we need to wait for getSchemaV2 to return before the routes are actually mounted -- hence the hiccup.


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
}
211 changes: 211 additions & 0 deletions src/lib/stitching2/kaws/v2/stitching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { GraphQLSchema, GraphQLFieldConfigArgumentMap } from "graphql"
import {
pageableFilterArtworksArgsWithInput,
filterArtworksArgs,
} from "schema/v2/filterArtworksConnection"
import gql from "lib/gql"
import { printType } from "lib/stitching/lib/printType"
import { delegateToSchema } from "@graphql-tools/delegate"

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: {
selectionSet: `{ internalID }`,
resolve: ({ internalID: artistID }, args, context, info) => {
return 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.stitchingInfo.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