graphql-custom-fields.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import { assertNever, getGraphQlInputName } from '@vendure/common/lib/shared-utils';
  2. import {
  3. buildSchema,
  4. extendSchema,
  5. GraphQLInputObjectType,
  6. GraphQLList,
  7. GraphQLSchema,
  8. parse,
  9. } from 'graphql';
  10. import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
  11. /**
  12. * Given a CustomFields config object, generates an SDL string extending the built-in
  13. * types with a customFields property for all entities, translations and inputs for which
  14. * custom fields are defined.
  15. */
  16. export function addGraphQLCustomFields(
  17. typeDefsOrSchema: string | GraphQLSchema,
  18. customFieldConfig: CustomFields,
  19. publicOnly: boolean,
  20. ): GraphQLSchema {
  21. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  22. let customFieldTypeDefs = '';
  23. if (!schema.getType('JSON')) {
  24. customFieldTypeDefs += `
  25. scalar JSON
  26. `;
  27. }
  28. if (!schema.getType('DateTime')) {
  29. customFieldTypeDefs += `
  30. scalar DateTime
  31. `;
  32. }
  33. for (const entityName of Object.keys(customFieldConfig)) {
  34. const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(
  35. config => {
  36. return !config.internal && (publicOnly === true ? config.public !== false : true);
  37. },
  38. );
  39. for (const fieldDef of customEntityFields) {
  40. if (fieldDef.type === 'relation') {
  41. if (!schema.getType(fieldDef.graphQLType || fieldDef.entity.name)) {
  42. throw new Error(
  43. `The GraphQL type "${fieldDef.graphQLType}" specified by the ${entityName}.${fieldDef.name} custom field does not exist`,
  44. );
  45. }
  46. }
  47. }
  48. const localeStringFields = customEntityFields.filter(field => field.type === 'localeString');
  49. const nonLocaleStringFields = customEntityFields.filter(field => field.type !== 'localeString');
  50. const writeableLocaleStringFields = localeStringFields.filter(field => !field.readonly);
  51. const writeableNonLocaleStringFields = nonLocaleStringFields.filter(field => !field.readonly);
  52. const filterableFields = customEntityFields.filter(field => field.type !== 'relation');
  53. if (schema.getType(entityName)) {
  54. if (customEntityFields.length) {
  55. customFieldTypeDefs += `
  56. type ${entityName}CustomFields {
  57. ${mapToFields(customEntityFields, getGraphQlType)}
  58. }
  59. extend type ${entityName} {
  60. customFields: ${entityName}CustomFields
  61. }
  62. `;
  63. } else {
  64. customFieldTypeDefs += `
  65. extend type ${entityName} {
  66. customFields: JSON
  67. }
  68. `;
  69. }
  70. }
  71. if (localeStringFields.length && schema.getType(`${entityName}Translation`)) {
  72. customFieldTypeDefs += `
  73. type ${entityName}TranslationCustomFields {
  74. ${mapToFields(localeStringFields, getGraphQlType)}
  75. }
  76. extend type ${entityName}Translation {
  77. customFields: ${entityName}TranslationCustomFields
  78. }
  79. `;
  80. }
  81. if (schema.getType(`Create${entityName}Input`)) {
  82. if (writeableNonLocaleStringFields.length) {
  83. customFieldTypeDefs += `
  84. input Create${entityName}CustomFieldsInput {
  85. ${mapToFields(
  86. writeableNonLocaleStringFields,
  87. getGraphQlInputType,
  88. getGraphQlInputName,
  89. )}
  90. }
  91. extend input Create${entityName}Input {
  92. customFields: Create${entityName}CustomFieldsInput
  93. }
  94. `;
  95. } else {
  96. customFieldTypeDefs += `
  97. extend input Create${entityName}Input {
  98. customFields: JSON
  99. }
  100. `;
  101. }
  102. }
  103. if (schema.getType(`Update${entityName}Input`)) {
  104. if (writeableNonLocaleStringFields.length) {
  105. customFieldTypeDefs += `
  106. input Update${entityName}CustomFieldsInput {
  107. ${mapToFields(
  108. writeableNonLocaleStringFields,
  109. getGraphQlInputType,
  110. getGraphQlInputName,
  111. )}
  112. }
  113. extend input Update${entityName}Input {
  114. customFields: Update${entityName}CustomFieldsInput
  115. }
  116. `;
  117. } else {
  118. customFieldTypeDefs += `
  119. extend input Update${entityName}Input {
  120. customFields: JSON
  121. }
  122. `;
  123. }
  124. }
  125. if (customEntityFields.length && schema.getType(`${entityName}SortParameter`)) {
  126. customFieldTypeDefs += `
  127. extend input ${entityName}SortParameter {
  128. ${mapToFields(customEntityFields, () => 'SortOrder')}
  129. }
  130. `;
  131. }
  132. if (filterableFields.length && schema.getType(`${entityName}FilterParameter`)) {
  133. customFieldTypeDefs += `
  134. extend input ${entityName}FilterParameter {
  135. ${mapToFields(filterableFields, getFilterOperator)}
  136. }
  137. `;
  138. }
  139. if (writeableLocaleStringFields) {
  140. const translationInputs = [
  141. `${entityName}TranslationInput`,
  142. `Create${entityName}TranslationInput`,
  143. `Update${entityName}TranslationInput`,
  144. ];
  145. for (const inputName of translationInputs) {
  146. if (schema.getType(inputName)) {
  147. if (writeableLocaleStringFields.length) {
  148. customFieldTypeDefs += `
  149. input ${inputName}CustomFields {
  150. ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
  151. }
  152. extend input ${inputName} {
  153. customFields: ${inputName}CustomFields
  154. }
  155. `;
  156. } else {
  157. customFieldTypeDefs += `
  158. extend input ${inputName} {
  159. customFields: JSON
  160. }
  161. `;
  162. }
  163. }
  164. }
  165. }
  166. }
  167. const publicAddressFields = customFieldConfig.Address?.filter(
  168. config => !config.internal && (publicOnly === true ? config.public !== false : true),
  169. );
  170. if (customFieldConfig.Address?.length && publicAddressFields?.length) {
  171. // For custom fields on the Address entity, we also extend the OrderAddress
  172. // type (which is used to store address snapshots on Orders)
  173. if (schema.getType('OrderAddress')) {
  174. customFieldTypeDefs += `
  175. extend type OrderAddress {
  176. customFields: AddressCustomFields
  177. }
  178. `;
  179. }
  180. if (schema.getType('UpdateOrderAddressInput')) {
  181. customFieldTypeDefs += `
  182. extend input UpdateOrderAddressInput {
  183. customFields: UpdateAddressCustomFieldsInput
  184. }
  185. `;
  186. }
  187. } else {
  188. if (schema.getType('OrderAddress')) {
  189. customFieldTypeDefs += `
  190. extend type OrderAddress {
  191. customFields: JSON
  192. }
  193. `;
  194. }
  195. }
  196. return extendSchema(schema, parse(customFieldTypeDefs));
  197. }
  198. export function addServerConfigCustomFields(
  199. typeDefsOrSchema: string | GraphQLSchema,
  200. customFieldConfig: CustomFields,
  201. ) {
  202. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  203. const customFieldTypeDefs = `
  204. type CustomFields {
  205. ${Object.keys(customFieldConfig).reduce(
  206. (output, name) => output + name + `: [CustomFieldConfig!]!\n`,
  207. '',
  208. )}
  209. }
  210. extend type ServerConfig {
  211. customFieldConfig: CustomFields!
  212. }
  213. `;
  214. return extendSchema(schema, parse(customFieldTypeDefs));
  215. }
  216. export function addActiveAdministratorCustomFields(
  217. typeDefsOrSchema: string | GraphQLSchema,
  218. administratorCustomFields: CustomFieldConfig[],
  219. ) {
  220. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  221. const extension = `
  222. extend input UpdateActiveAdministratorInput {
  223. customFields: ${
  224. 0 < administratorCustomFields?.length ? 'UpdateAdministratorCustomFieldsInput' : 'JSON'
  225. }
  226. }
  227. `;
  228. return extendSchema(schema, parse(extension));
  229. }
  230. /**
  231. * If CustomFields are defined on the Customer entity, then an extra `customFields` field is added to
  232. * the `RegisterCustomerInput` so that public writable custom fields can be set when a new customer
  233. * is registered.
  234. */
  235. export function addRegisterCustomerCustomFieldsInput(
  236. typeDefsOrSchema: string | GraphQLSchema,
  237. customerCustomFields: CustomFieldConfig[],
  238. ): GraphQLSchema {
  239. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  240. if (!customerCustomFields || customerCustomFields.length === 0) {
  241. return schema;
  242. }
  243. const publicWritableCustomFields = customerCustomFields.filter(fieldDef => {
  244. return fieldDef.public !== false && !fieldDef.readonly && !fieldDef.internal;
  245. });
  246. if (publicWritableCustomFields.length < 1) {
  247. return schema;
  248. }
  249. const customFieldTypeDefs = `
  250. input RegisterCustomerCustomFieldsInput {
  251. ${mapToFields(publicWritableCustomFields, getGraphQlInputType, getGraphQlInputName)}
  252. }
  253. extend input RegisterCustomerInput {
  254. customFields: RegisterCustomerCustomFieldsInput
  255. }
  256. `;
  257. return extendSchema(schema, parse(customFieldTypeDefs));
  258. }
  259. /**
  260. * If CustomFields are defined on the Order entity, we add a `customFields` field to the ModifyOrderInput
  261. * type.
  262. */
  263. export function addModifyOrderCustomFields(
  264. typeDefsOrSchema: string | GraphQLSchema,
  265. orderCustomFields: CustomFieldConfig[],
  266. ): GraphQLSchema {
  267. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  268. if (!orderCustomFields || orderCustomFields.length === 0) {
  269. return schema;
  270. }
  271. if (schema.getType('ModifyOrderInput') && schema.getType('UpdateOrderCustomFieldsInput')) {
  272. const customFieldTypeDefs = `
  273. extend input ModifyOrderInput {
  274. customFields: UpdateOrderCustomFieldsInput
  275. }
  276. `;
  277. return extendSchema(schema, parse(customFieldTypeDefs));
  278. }
  279. return schema;
  280. }
  281. /**
  282. * If CustomFields are defined on the OrderLine entity, then an extra `customFields` argument
  283. * must be added to the `addItemToOrder` and `adjustOrderLine` mutations, as well as the related
  284. * fields in the `ModifyOrderInput` type.
  285. */
  286. export function addOrderLineCustomFieldsInput(
  287. typeDefsOrSchema: string | GraphQLSchema,
  288. orderLineCustomFields: CustomFieldConfig[],
  289. ): GraphQLSchema {
  290. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  291. const publicCustomFields = orderLineCustomFields.filter(f => f.public !== false);
  292. if (!publicCustomFields || publicCustomFields.length === 0) {
  293. return schema;
  294. }
  295. const schemaConfig = schema.toConfig();
  296. const mutationType = schemaConfig.mutation;
  297. if (!mutationType) {
  298. return schema;
  299. }
  300. const input = new GraphQLInputObjectType({
  301. name: 'OrderLineCustomFieldsInput',
  302. fields: publicCustomFields.reduce((fields, field) => {
  303. const name = getGraphQlInputName(field);
  304. // tslint:disable-next-line:no-non-null-assertion
  305. const primitiveType = schema.getType(getGraphQlInputType(field))!;
  306. const type = field.list === true ? new GraphQLList(primitiveType) : primitiveType;
  307. return { ...fields, [name]: { type } };
  308. }, {}),
  309. });
  310. schemaConfig.types.push(input);
  311. const addItemToOrderMutation = mutationType.getFields().addItemToOrder;
  312. const adjustOrderLineMutation = mutationType.getFields().adjustOrderLine;
  313. if (addItemToOrderMutation) {
  314. addItemToOrderMutation.args.push({
  315. name: 'customFields',
  316. type: input,
  317. description: null,
  318. defaultValue: null,
  319. extensions: null,
  320. astNode: null,
  321. deprecationReason: null,
  322. });
  323. }
  324. if (adjustOrderLineMutation) {
  325. adjustOrderLineMutation.args.push({
  326. name: 'customFields',
  327. type: input,
  328. description: null,
  329. defaultValue: null,
  330. extensions: null,
  331. astNode: null,
  332. deprecationReason: null,
  333. });
  334. }
  335. let extendedSchema = new GraphQLSchema(schemaConfig);
  336. if (schema.getType('AddItemInput')) {
  337. const customFieldTypeDefs = `
  338. extend input AddItemInput {
  339. customFields: OrderLineCustomFieldsInput
  340. }
  341. `;
  342. extendedSchema = extendSchema(extendedSchema, parse(customFieldTypeDefs));
  343. }
  344. if (schema.getType('AdjustOrderLineInput')) {
  345. const customFieldTypeDefs = `
  346. extend input AdjustOrderLineInput {
  347. customFields: OrderLineCustomFieldsInput
  348. }
  349. `;
  350. extendedSchema = extendSchema(extendedSchema, parse(customFieldTypeDefs));
  351. }
  352. return extendedSchema;
  353. }
  354. export function addShippingMethodQuoteCustomFields(
  355. typeDefsOrSchema: string | GraphQLSchema,
  356. shippingMethodCustomFields: CustomFieldConfig[],
  357. ) {
  358. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  359. let customFieldTypeDefs = ``;
  360. const publicCustomFields = shippingMethodCustomFields.filter(f => f.public !== false);
  361. if (0 < publicCustomFields.length) {
  362. customFieldTypeDefs = `
  363. extend type ShippingMethodQuote {
  364. customFields: ShippingMethodCustomFields
  365. }
  366. `;
  367. } else {
  368. customFieldTypeDefs = `
  369. extend type ShippingMethodQuote {
  370. customFields: JSON
  371. }
  372. `;
  373. }
  374. return extendSchema(schema, parse(customFieldTypeDefs));
  375. }
  376. export function addPaymentMethodQuoteCustomFields(
  377. typeDefsOrSchema: string | GraphQLSchema,
  378. paymentMethodCustomFields: CustomFieldConfig[],
  379. ) {
  380. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  381. let customFieldTypeDefs = ``;
  382. const publicCustomFields = paymentMethodCustomFields.filter(f => f.public !== false);
  383. if (0 < publicCustomFields.length) {
  384. customFieldTypeDefs = `
  385. extend type PaymentMethodQuote {
  386. customFields: PaymentMethodCustomFields
  387. }
  388. `;
  389. } else {
  390. customFieldTypeDefs = `
  391. extend type PaymentMethodQuote {
  392. customFields: JSON
  393. }
  394. `;
  395. }
  396. return extendSchema(schema, parse(customFieldTypeDefs));
  397. }
  398. /**
  399. * Maps an array of CustomFieldConfig objects into a string of SDL fields.
  400. */
  401. function mapToFields(
  402. fieldDefs: CustomFieldConfig[],
  403. typeFn: (def: CustomFieldConfig) => string | undefined,
  404. nameFn?: (def: Pick<CustomFieldConfig, 'name' | 'type' | 'list'>) => string,
  405. ): string {
  406. const res = fieldDefs
  407. .map(field => {
  408. const primitiveType = typeFn(field);
  409. if (!primitiveType) {
  410. return;
  411. }
  412. const finalType = field.list ? `[${primitiveType}!]` : primitiveType;
  413. const name = nameFn ? nameFn(field) : field.name;
  414. return `${name}: ${finalType}`;
  415. })
  416. .filter(x => x != null);
  417. return res.join('\n');
  418. }
  419. function getFilterOperator(config: CustomFieldConfig): string | undefined {
  420. switch (config.type) {
  421. case 'datetime':
  422. return 'DateOperators';
  423. case 'string':
  424. case 'localeString':
  425. case 'text':
  426. return 'StringOperators';
  427. case 'boolean':
  428. return 'BooleanOperators';
  429. case 'int':
  430. case 'float':
  431. return 'NumberOperators';
  432. case 'relation':
  433. return undefined;
  434. default:
  435. assertNever(config);
  436. }
  437. return 'String';
  438. }
  439. function getGraphQlInputType(config: CustomFieldConfig): string {
  440. return config.type === 'relation' ? `ID` : getGraphQlType(config);
  441. }
  442. function getGraphQlType(config: CustomFieldConfig): string {
  443. switch (config.type) {
  444. case 'string':
  445. case 'localeString':
  446. case 'text':
  447. return 'String';
  448. case 'datetime':
  449. return 'DateTime';
  450. case 'boolean':
  451. return 'Boolean';
  452. case 'int':
  453. return 'Int';
  454. case 'float':
  455. return 'Float';
  456. case 'relation':
  457. return config.graphQLType || config.entity.name;
  458. default:
  459. assertNever(config);
  460. }
  461. return 'String';
  462. }