stellate-plugin.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import { Inject, OnApplicationBootstrap } from '@nestjs/common';
  2. import { ModuleRef } from '@nestjs/core';
  3. import { EventBus, Injector, PluginCommonModule, VendurePlugin } from '@vendure/core';
  4. import { buffer, debounceTime } from 'rxjs/operators';
  5. import { shopApiExtensions } from './api/api-extensions';
  6. import { SearchResponseFieldResolver } from './api/search-response.resolver';
  7. import { STELLATE_PLUGIN_OPTIONS } from './constants';
  8. import { StellateService } from './service/stellate.service';
  9. import { StellatePluginOptions } from './types';
  10. const StellateOptionsProvider = {
  11. provide: STELLATE_PLUGIN_OPTIONS,
  12. useFactory: () => StellatePlugin.options,
  13. };
  14. /**
  15. * @description
  16. * A plugin to integrate the [Stellate](https://stellate.co/) GraphQL caching service with your Vendure server.
  17. * The main purpose of this plugin is to ensure that cached data gets correctly purged in
  18. * response to events inside Vendure. For example, changes to a Product's description should
  19. * purge any associated record for that Product in Stellate's cache.
  20. *
  21. * ## Pre-requisites
  22. *
  23. * You will first need to [set up a free Stellate account](https://stellate.co/signup).
  24. *
  25. * You will also need to generate an **API token** for the Stellate Purging API. For instructions on how to generate the token,
  26. * see the [Stellate Purging API docs](https://docs.stellate.co/docs/purging-api#authentication).
  27. *
  28. * ## Installation
  29. *
  30. * ```
  31. * npm install \@vendure/stellate-plugin
  32. * ```
  33. *
  34. * ## Configuration
  35. *
  36. * The plugin is configured via the `StellatePlugin.init()` method. This method accepts an options object
  37. * which defines the Stellate service name and API token, as well as an array of {@link PurgeRule}s which
  38. * define how the plugin will respond to Vendure events in order to trigger calls to the
  39. * Stellate [Purging API](https://stellate.co/docs/graphql-edge-cache/purging-api).
  40. *
  41. * @example
  42. * ```ts
  43. * import { StellatePlugin, defaultPurgeRules } from '\@vendure/stellate-plugin';
  44. * import { VendureConfig } from '\@vendure/core';
  45. *
  46. * export const config: VendureConfig = {
  47. * // ...
  48. * plugins: [
  49. * StellatePlugin.init({
  50. * // The Stellate service name, i.e. `<serviceName>.stellate.sh`
  51. * serviceName: 'my-service',
  52. * // The API token for the Stellate Purging API. See the "pre-requisites" section above.
  53. * apiToken: process.env.STELLATE_PURGE_API_TOKEN,
  54. * devMode: !isProd || process.env.STELLATE_DEBUG_MODE ? true : false,
  55. * debugLogging: process.env.STELLATE_DEBUG_MODE ? true : false,
  56. * purgeRules: [
  57. * ...defaultPurgeRules,
  58. * // custom purge rules can be added here
  59. * ],
  60. * }),
  61. * ],
  62. * };
  63. * ```
  64. *
  65. * In your Stellate dashboard, you can use the following configuration example as a sensible default for a
  66. * Vendure application:
  67. *
  68. * @example
  69. * ```ts
  70. * import { Config } from "stellate";
  71. *
  72. * const config: Config = {
  73. * config: {
  74. * name: "my-vendure-server",
  75. * originUrl: "https://my-vendure-server.com/shop-api",
  76. * ignoreOriginCacheControl: true,
  77. * passThroughOnly: false,
  78. * scopes: {
  79. * SESSION_BOUND: "header:authorization|cookie:session",
  80. * },
  81. * headers: {
  82. * "access-control-expose-headers": "vendure-auth-token",
  83. * },
  84. * rootTypeNames: {
  85. * query: "Query",
  86. * mutation: "Mutation",
  87. * },
  88. * keyFields: {
  89. * types: {
  90. * SearchResult: ["productId"],
  91. * SearchResponseCacheIdentifier: ["collectionSlug"],
  92. * },
  93. * },
  94. * rules: [
  95. * {
  96. * types: [
  97. * "Product",
  98. * "Collection",
  99. * "ProductVariant",
  100. * "SearchResponse",
  101. * ],
  102. * maxAge: 900,
  103. * swr: 900,
  104. * description: "Cache Products & Collections",
  105. * },
  106. * {
  107. * types: ["Channel"],
  108. * maxAge: 9000,
  109. * swr: 9000,
  110. * description: "Cache active channel",
  111. * },
  112. * {
  113. * types: ["Order", "Customer", "User"],
  114. * maxAge: 0,
  115. * swr: 0,
  116. * description: "Do not cache user data",
  117. * },
  118. * ],
  119. * },
  120. * };
  121. * export default config;
  122. * ```
  123. *
  124. * ## Storefront setup
  125. *
  126. * In your storefront, you should point your GraphQL client to the Stellate GraphQL API endpoint, which is
  127. * `https://<service-name>.stellate.sh`.
  128. *
  129. * Wherever you are using the `search` query (typically in product listing & search pages), you should also add the
  130. * `cacheIdentifier` field to the query. This will ensure that the Stellate cache is correctly purged when
  131. * a Product or Collection is updated.
  132. *
  133. * @example
  134. * ```ts
  135. * import { graphql } from '../generated/gql';
  136. *
  137. * export const searchProductsDocument = graphql(`
  138. * query SearchProducts($input: SearchInput!) {
  139. * search(input: $input) {
  140. * // highlight-start
  141. * cacheIdentifier {
  142. * collectionSlug
  143. * }
  144. * // highlight-end
  145. * items {
  146. * # ...
  147. * }
  148. * }
  149. * }
  150. * }`);
  151. * ```
  152. *
  153. * ## Custom PurgeRules
  154. *
  155. * The configuration above only accounts for caching of some of the built-in Vendure entity types. If you have
  156. * custom entity types, you may well want to add them to the Stellate cache. In this case, you'll also need a way to
  157. * purge those entities from the cache when they are updated. This is where the {@link PurgeRule} comes in.
  158. *
  159. * Let's imagine that you have built a simple CMS plugin for Vendure which exposes an `Article` entity in your Shop API, and
  160. * you have added this to your Stellate configuration:
  161. *
  162. * @example
  163. * ```ts
  164. * import { Config } from "stellate";
  165. *
  166. * const config: Config = {
  167. * config: {
  168. * // ...
  169. * rules: [
  170. * // ...
  171. * {
  172. * types: ["Article"],
  173. * maxAge: 900,
  174. * swr: 900,
  175. * description: "Cache Articles",
  176. * },
  177. * ],
  178. * },
  179. * // ...
  180. * };
  181. * export default config;
  182. * ```
  183. *
  184. * You can then add a custom {@link PurgeRule} to the StellatePlugin configuration:
  185. *
  186. * @example
  187. * ```ts
  188. * import { StellatePlugin, defaultPurgeRules } from "\@vendure/stellate-plugin";
  189. * import { VendureConfig } from "\@vendure/core";
  190. * import { ArticleEvent } from "./plugins/cms/events/article-event";
  191. *
  192. * export const config: VendureConfig = {
  193. * // ...
  194. * plugins: [
  195. * StellatePlugin.init({
  196. * // ...
  197. * purgeRules: [
  198. * ...defaultPurgeRules,
  199. * new PurgeRule({
  200. * eventType: ArticleEvent,
  201. * handler: async ({ events, stellateService }) => {
  202. * const articleIds = events.map((e) => e.article.id);
  203. * stellateService.purge("Article", articleIds);
  204. * },
  205. * }),
  206. * ],
  207. * }),
  208. * ],
  209. * };
  210. * ```
  211. *
  212. * ## DevMode & Debug Logging
  213. *
  214. * In development, you can set `devMode: true`, which will prevent any calls being made to the Stellate Purging API.
  215. *
  216. * If you want to log the calls that _would_ be made to the Stellate Purge API when in devMode, you can set `debugLogging: true`.
  217. * Note that debugLogging generates a lot of debug-level logging, so it is recommended to only enable this when needed.
  218. *
  219. * @example
  220. * ```ts
  221. * import { StellatePlugin, defaultPurgeRules } from '\@vendure/stellate-plugin';
  222. * import { VendureConfig } from '\@vendure/core';
  223. *
  224. * export const config: VendureConfig = {
  225. * // ...
  226. * plugins: [
  227. * StellatePlugin.init({
  228. * // ...
  229. * devMode: !process.env.PRODUCTION,
  230. * debugLogging: process.env.STELLATE_DEBUG_MODE ? true : false,
  231. * purgeRules: [
  232. * ...defaultPurgeRules,
  233. * ],
  234. * }),
  235. * ],
  236. * };
  237. * ```
  238. *
  239. *
  240. * @since 2.1.5
  241. * @docsCategory core plugins/StellatePlugin
  242. */
  243. @VendurePlugin({
  244. imports: [PluginCommonModule],
  245. providers: [StellateOptionsProvider, StellateService],
  246. shopApiExtensions: {
  247. schema: shopApiExtensions,
  248. resolvers: [SearchResponseFieldResolver],
  249. },
  250. compatibility: '^3.0.0',
  251. })
  252. export class StellatePlugin implements OnApplicationBootstrap {
  253. static options: StellatePluginOptions;
  254. static init(options: StellatePluginOptions) {
  255. this.options = options;
  256. return this;
  257. }
  258. constructor(
  259. @Inject(STELLATE_PLUGIN_OPTIONS) private options: StellatePluginOptions,
  260. private eventBus: EventBus,
  261. private stellateService: StellateService,
  262. private moduleRef: ModuleRef,
  263. ) {}
  264. onApplicationBootstrap() {
  265. const injector = new Injector(this.moduleRef);
  266. for (const purgeRule of this.options.purgeRules ?? []) {
  267. const source$ = this.eventBus.ofType(purgeRule.eventType);
  268. source$
  269. .pipe(
  270. buffer(
  271. source$.pipe(
  272. debounceTime(purgeRule.bufferTimeMs ?? this.options.defaultBufferTimeMs ?? 2000),
  273. ),
  274. ),
  275. )
  276. .subscribe(events =>
  277. purgeRule.handle({ events, injector, stellateService: this.stellateService }),
  278. );
  279. }
  280. }
  281. }