configurable-operation.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. // prettier-ignore
  2. import {
  3. ConfigArg,
  4. ConfigArgDefinition,
  5. ConfigurableOperationDefinition,
  6. LanguageCode,
  7. LocalizedString,
  8. Maybe,
  9. StringFieldOption,
  10. } from '@vendure/common/lib/generated-types';
  11. import { ConfigArgType } from '@vendure/common/lib/shared-types';
  12. import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
  13. import { RequestContext } from '../api/common/request-context';
  14. import { DEFAULT_LANGUAGE_CODE } from './constants';
  15. import { InternalServerError } from './error/errors';
  16. import { Injector } from './injector';
  17. import { InjectableStrategy } from './types/injectable-strategy';
  18. /**
  19. * @description
  20. * An array of string values in a given {@link LanguageCode}, used to define human-readable string values.
  21. *
  22. * @example
  23. * ```TypeScript
  24. * const title: LocalizedStringArray = [
  25. * { languageCode: LanguageCode.en, value: 'English Title' },
  26. * { languageCode: LanguageCode.de, value: 'German Title' },
  27. * { languageCode: LanguageCode.zh, value: 'Chinese Title' },
  28. * ]
  29. * ```
  30. *
  31. * @docsCategory common
  32. * @docsPage Configurable Operations
  33. */
  34. export type LocalizedStringArray = Array<Omit<LocalizedString, '__typename'>>;
  35. export interface ConfigArgCommonDef<T extends ConfigArgType> {
  36. type: T;
  37. label?: LocalizedStringArray;
  38. description?: LocalizedStringArray;
  39. }
  40. export type WithArgConfig<T> = {
  41. config?: T;
  42. };
  43. export type StringArgConfig = WithArgConfig<{
  44. options?: Maybe<StringFieldOption[]>;
  45. }>;
  46. export type IntArgConfig = WithArgConfig<{
  47. inputType?: 'default' | 'percentage' | 'money';
  48. }>;
  49. export type ConfigArgDef<T extends ConfigArgType> = T extends 'string'
  50. ? ConfigArgCommonDef<'string'> & StringArgConfig
  51. : T extends 'int'
  52. ? ConfigArgCommonDef<'int'> & IntArgConfig
  53. : ConfigArgCommonDef<T> & WithArgConfig<never>;
  54. /**
  55. * @description
  56. * A object which defines the configurable arguments which may be passed to
  57. * functions in those classes which implement the {@link ConfigurableOperationDef} interface.
  58. *
  59. * @example
  60. * ```TypeScript
  61. * {
  62. * operator: {
  63. * type: 'string',
  64. * config: {
  65. * options: [
  66. * { value: 'startsWith' },
  67. * { value: 'endsWith' },
  68. * { value: 'contains' },
  69. * { value: 'doesNotContain' },
  70. * ],
  71. * },
  72. * },
  73. * term: { type: 'string' },
  74. * }
  75. * ```
  76. *
  77. * @docsCategory common
  78. * @docsPage Configurable Operations
  79. */
  80. export type ConfigArgs<T extends ConfigArgType> = {
  81. [name: string]: ConfigArgDef<T>;
  82. };
  83. // prettier-ignore
  84. /**
  85. * Represents the ConfigArgs once they have been coerced into JavaScript values for use
  86. * in business logic.
  87. */
  88. export type ConfigArgValues<T extends ConfigArgs<any>> = {
  89. [K in keyof T]: T[K] extends ConfigArgDef<'int' | 'float'>
  90. ? number
  91. : T[K] extends ConfigArgDef<'datetime'>
  92. ? Date
  93. : T[K] extends ConfigArgDef<'boolean'>
  94. ? boolean
  95. : T[K] extends ConfigArgDef<'facetValueIds'>
  96. ? string[]
  97. : string
  98. };
  99. /**
  100. * @description
  101. * Common configuration options used when creating a new instance of a
  102. * {@link ConfigurableOperationDef}.
  103. *
  104. * @docsCategory common
  105. * @docsPage Configurable Operations
  106. */
  107. export interface ConfigurableOperationDefOptions<T extends ConfigArgs<ConfigArgType>>
  108. extends InjectableStrategy {
  109. /**
  110. * @description
  111. * A unique code used to identify this operation.
  112. */
  113. code: string;
  114. /**
  115. * @description
  116. * Optional provider-specific arguments which, when specified, are
  117. * editable in the admin-ui. For example, args could be used to store an API key
  118. * for a payment provider service.
  119. *
  120. * @example
  121. * ```ts
  122. * args: {
  123. * apiKey: { type: 'string' },
  124. * }
  125. * ```
  126. *
  127. * See {@link ConfigArgs} for available configuration options.
  128. */
  129. args: T;
  130. /**
  131. * @description
  132. * A human-readable description for the operation method.
  133. */
  134. description: LocalizedStringArray;
  135. }
  136. /**
  137. * @description
  138. * Defines a ConfigurableOperation, which is a method which can be configured
  139. * by the Administrator via the Admin API.
  140. *
  141. * @docsCategory common
  142. * @docsPage Configurable Operations
  143. */
  144. export class ConfigurableOperationDef<T extends ConfigArgs<ConfigArgType>> {
  145. get code(): string {
  146. return this.options.code;
  147. }
  148. get args(): T {
  149. return this.options.args;
  150. }
  151. get description(): LocalizedStringArray {
  152. return this.options.description;
  153. }
  154. constructor(protected options: ConfigurableOperationDefOptions<T>) {}
  155. async init(injector: Injector) {
  156. if (typeof this.options.init === 'function') {
  157. await this.options.init(injector);
  158. }
  159. }
  160. async destroy() {
  161. if (typeof this.options.destroy === 'function') {
  162. await this.options.destroy();
  163. }
  164. }
  165. }
  166. /**
  167. * Convert a ConfigurableOperationDef into a ConfigurableOperation object, typically
  168. * so that it can be sent via the API.
  169. */
  170. export function configurableDefToOperation(
  171. ctx: RequestContext,
  172. def: ConfigurableOperationDef<ConfigArgs<any>>,
  173. ): ConfigurableOperationDefinition {
  174. return {
  175. code: def.code,
  176. description: localizeString(def.description, ctx.languageCode),
  177. args: Object.entries(def.args).map(
  178. ([name, arg]) =>
  179. ({
  180. name,
  181. type: arg.type,
  182. config: localizeConfig(arg, ctx.languageCode),
  183. label: arg.label && localizeString(arg.label, ctx.languageCode),
  184. description: arg.description && localizeString(arg.description, ctx.languageCode),
  185. } as Required<ConfigArgDefinition>),
  186. ),
  187. };
  188. }
  189. function localizeConfig(
  190. arg: StringArgConfig | IntArgConfig | WithArgConfig<undefined>,
  191. languageCode: LanguageCode,
  192. ): any {
  193. const { config } = arg;
  194. if (!config) {
  195. return config;
  196. }
  197. const clone = simpleDeepClone(config);
  198. const options: Maybe<StringFieldOption[]> = (clone as any).options;
  199. if (options) {
  200. for (const option of options) {
  201. if (option.label) {
  202. (option as any).label = localizeString(option.label, languageCode);
  203. }
  204. }
  205. }
  206. return clone;
  207. }
  208. function localizeString(stringArray: LocalizedStringArray, languageCode: LanguageCode): string {
  209. let match = stringArray.find(x => x.languageCode === languageCode);
  210. if (!match) {
  211. match = stringArray.find(x => x.languageCode === DEFAULT_LANGUAGE_CODE);
  212. }
  213. if (!match) {
  214. match = stringArray[0];
  215. }
  216. return match.value;
  217. }
  218. /**
  219. * Coverts an array of ConfigArgs into a hash object:
  220. *
  221. * from:
  222. * [{ name: 'foo', type: 'string', value: 'bar'}]
  223. *
  224. * to:
  225. * { foo: 'bar' }
  226. **/
  227. export function argsArrayToHash<T extends ConfigArgs<any>>(args: ConfigArg[]): ConfigArgValues<T> {
  228. const output: ConfigArgValues<T> = {} as any;
  229. for (const arg of args) {
  230. if (arg && arg.value != null) {
  231. output[arg.name as keyof ConfigArgValues<T>] = coerceValueToType<T>(arg);
  232. }
  233. }
  234. return output;
  235. }
  236. function coerceValueToType<T extends ConfigArgs<any>>(arg: ConfigArg): ConfigArgValues<T>[keyof T] {
  237. switch (arg.type as ConfigArgType) {
  238. case 'string':
  239. return arg.value as any;
  240. case 'int':
  241. return Number.parseInt(arg.value || '', 10) as any;
  242. case 'datetime':
  243. return Date.parse(arg.value || '') as any;
  244. case 'boolean':
  245. return !!(arg.value && (arg.value.toLowerCase() === 'true' || arg.value === '1')) as any;
  246. case 'facetValueIds':
  247. try {
  248. return JSON.parse(arg.value as any);
  249. } catch (err) {
  250. throw new InternalServerError(err.message);
  251. }
  252. default:
  253. return (arg.value as string) as any;
  254. }
  255. }