graphql-custom-fields.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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. if (customFieldConfig.Address?.length) {
  168. // For custom fields on the Address entity, we also extend the OrderAddress
  169. // type (which is used to store address snapshots on Orders)
  170. if (schema.getType('OrderAddress')) {
  171. customFieldTypeDefs += `
  172. extend type OrderAddress {
  173. customFields: AddressCustomFields
  174. }
  175. `;
  176. }
  177. if (schema.getType('UpdateOrderAddressInput')) {
  178. customFieldTypeDefs += `
  179. extend input UpdateOrderAddressInput {
  180. customFields: UpdateAddressCustomFieldsInput
  181. }
  182. `;
  183. }
  184. } else {
  185. if (schema.getType('OrderAddress')) {
  186. customFieldTypeDefs += `
  187. extend type OrderAddress {
  188. customFields: JSON
  189. }
  190. `;
  191. }
  192. }
  193. return extendSchema(schema, parse(customFieldTypeDefs));
  194. }
  195. export function addServerConfigCustomFields(
  196. typeDefsOrSchema: string | GraphQLSchema,
  197. customFieldConfig: CustomFields,
  198. ) {
  199. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  200. const customFieldTypeDefs = `
  201. type CustomFields {
  202. ${Object.keys(customFieldConfig).reduce(
  203. (output, name) => output + name + `: [CustomFieldConfig!]!\n`,
  204. '',
  205. )}
  206. }
  207. extend type ServerConfig {
  208. customFieldConfig: CustomFields!
  209. }
  210. `;
  211. return extendSchema(schema, parse(customFieldTypeDefs));
  212. }
  213. export function addActiveAdministratorCustomFields(
  214. typeDefsOrSchema: string | GraphQLSchema,
  215. administratorCustomFields: CustomFieldConfig[],
  216. ) {
  217. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  218. const extension = `
  219. extend input UpdateActiveAdministratorInput {
  220. customFields: ${
  221. 0 < administratorCustomFields?.length ? 'UpdateAdministratorCustomFieldsInput' : 'JSON'
  222. }
  223. }
  224. `;
  225. return extendSchema(schema, parse(extension));
  226. }
  227. /**
  228. * If CustomFields are defined on the Customer entity, then an extra `customFields` field is added to
  229. * the `RegisterCustomerInput` so that public writable custom fields can be set when a new customer
  230. * is registered.
  231. */
  232. export function addRegisterCustomerCustomFieldsInput(
  233. typeDefsOrSchema: string | GraphQLSchema,
  234. customerCustomFields: CustomFieldConfig[],
  235. ): GraphQLSchema {
  236. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  237. if (!customerCustomFields || customerCustomFields.length === 0) {
  238. return schema;
  239. }
  240. const publicWritableCustomFields = customerCustomFields.filter(fieldDef => {
  241. return fieldDef.public !== false && !fieldDef.readonly && !fieldDef.internal;
  242. });
  243. if (publicWritableCustomFields.length < 1) {
  244. return schema;
  245. }
  246. const customFieldTypeDefs = `
  247. input RegisterCustomerCustomFieldsInput {
  248. ${mapToFields(publicWritableCustomFields, getGraphQlInputType, getGraphQlInputName)}
  249. }
  250. extend input RegisterCustomerInput {
  251. customFields: RegisterCustomerCustomFieldsInput
  252. }
  253. `;
  254. return extendSchema(schema, parse(customFieldTypeDefs));
  255. }
  256. /**
  257. * If CustomFields are defined on the Order entity, we add a `customFields` field to the ModifyOrderInput
  258. * type.
  259. */
  260. export function addModifyOrderCustomFields(
  261. typeDefsOrSchema: string | GraphQLSchema,
  262. orderCustomFields: CustomFieldConfig[],
  263. ): GraphQLSchema {
  264. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  265. if (!orderCustomFields || orderCustomFields.length === 0) {
  266. return schema;
  267. }
  268. if (schema.getType('ModifyOrderInput') && schema.getType('UpdateOrderCustomFieldsInput')) {
  269. const customFieldTypeDefs = `
  270. extend input ModifyOrderInput {
  271. customFields: UpdateOrderCustomFieldsInput
  272. }
  273. `;
  274. return extendSchema(schema, parse(customFieldTypeDefs));
  275. }
  276. return schema;
  277. }
  278. /**
  279. * If CustomFields are defined on the OrderLine entity, then an extra `customFields` argument
  280. * must be added to the `addItemToOrder` and `adjustOrderLine` mutations, as well as the related
  281. * fields in the `ModifyOrderInput` type.
  282. */
  283. export function addOrderLineCustomFieldsInput(
  284. typeDefsOrSchema: string | GraphQLSchema,
  285. orderLineCustomFields: CustomFieldConfig[],
  286. ): GraphQLSchema {
  287. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  288. const publicCustomFields = orderLineCustomFields.filter(f => f.public !== false);
  289. if (!publicCustomFields || publicCustomFields.length === 0) {
  290. return schema;
  291. }
  292. const schemaConfig = schema.toConfig();
  293. const mutationType = schemaConfig.mutation;
  294. if (!mutationType) {
  295. return schema;
  296. }
  297. const input = new GraphQLInputObjectType({
  298. name: 'OrderLineCustomFieldsInput',
  299. fields: publicCustomFields.reduce((fields, field) => {
  300. const name = getGraphQlInputName(field);
  301. // tslint:disable-next-line:no-non-null-assertion
  302. const primitiveType = schema.getType(getGraphQlInputType(field))!;
  303. const type = field.list === true ? new GraphQLList(primitiveType) : primitiveType;
  304. return { ...fields, [name]: { type } };
  305. }, {}),
  306. });
  307. schemaConfig.types.push(input);
  308. const addItemToOrderMutation = mutationType.getFields().addItemToOrder;
  309. const adjustOrderLineMutation = mutationType.getFields().adjustOrderLine;
  310. if (addItemToOrderMutation) {
  311. addItemToOrderMutation.args.push({
  312. name: 'customFields',
  313. type: input,
  314. description: null,
  315. defaultValue: null,
  316. extensions: null,
  317. astNode: null,
  318. deprecationReason: null,
  319. });
  320. }
  321. if (adjustOrderLineMutation) {
  322. adjustOrderLineMutation.args.push({
  323. name: 'customFields',
  324. type: input,
  325. description: null,
  326. defaultValue: null,
  327. extensions: null,
  328. astNode: null,
  329. deprecationReason: null,
  330. });
  331. }
  332. let extendedSchema = new GraphQLSchema(schemaConfig);
  333. if (schema.getType('AddItemInput')) {
  334. const customFieldTypeDefs = `
  335. extend input AddItemInput {
  336. customFields: OrderLineCustomFieldsInput
  337. }
  338. `;
  339. extendedSchema = extendSchema(extendedSchema, parse(customFieldTypeDefs));
  340. }
  341. if (schema.getType('AdjustOrderLineInput')) {
  342. const customFieldTypeDefs = `
  343. extend input AdjustOrderLineInput {
  344. customFields: OrderLineCustomFieldsInput
  345. }
  346. `;
  347. extendedSchema = extendSchema(extendedSchema, parse(customFieldTypeDefs));
  348. }
  349. return extendedSchema;
  350. }
  351. export function addShippingMethodQuoteCustomFields(
  352. typeDefsOrSchema: string | GraphQLSchema,
  353. shippingMethodCustomFields: CustomFieldConfig[],
  354. ) {
  355. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  356. let customFieldTypeDefs = ``;
  357. const publicCustomFields = shippingMethodCustomFields.filter(f => f.public !== false);
  358. if (0 < publicCustomFields.length) {
  359. customFieldTypeDefs = `
  360. extend type ShippingMethodQuote {
  361. customFields: ShippingMethodCustomFields
  362. }
  363. `;
  364. } else {
  365. customFieldTypeDefs = `
  366. extend type ShippingMethodQuote {
  367. customFields: JSON
  368. }
  369. `;
  370. }
  371. return extendSchema(schema, parse(customFieldTypeDefs));
  372. }
  373. export function addPaymentMethodQuoteCustomFields(
  374. typeDefsOrSchema: string | GraphQLSchema,
  375. paymentMethodCustomFields: CustomFieldConfig[],
  376. ) {
  377. const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
  378. let customFieldTypeDefs = ``;
  379. const publicCustomFields = paymentMethodCustomFields.filter(f => f.public !== false);
  380. if (0 < publicCustomFields.length) {
  381. customFieldTypeDefs = `
  382. extend type PaymentMethodQuote {
  383. customFields: ShippingMethodCustomFields
  384. }
  385. `;
  386. } else {
  387. customFieldTypeDefs = `
  388. extend type PaymentMethodQuote {
  389. customFields: JSON
  390. }
  391. `;
  392. }
  393. return extendSchema(schema, parse(customFieldTypeDefs));
  394. }
  395. /**
  396. * Maps an array of CustomFieldConfig objects into a string of SDL fields.
  397. */
  398. function mapToFields(
  399. fieldDefs: CustomFieldConfig[],
  400. typeFn: (def: CustomFieldConfig) => string | undefined,
  401. nameFn?: (def: Pick<CustomFieldConfig, 'name' | 'type' | 'list'>) => string,
  402. ): string {
  403. const res = fieldDefs
  404. .map(field => {
  405. const primitiveType = typeFn(field);
  406. if (!primitiveType) {
  407. return;
  408. }
  409. const finalType = field.list ? `[${primitiveType}!]` : primitiveType;
  410. const name = nameFn ? nameFn(field) : field.name;
  411. return `${name}: ${finalType}`;
  412. })
  413. .filter(x => x != null);
  414. return res.join('\n');
  415. }
  416. function getFilterOperator(config: CustomFieldConfig): string | undefined {
  417. switch (config.type) {
  418. case 'datetime':
  419. return 'DateOperators';
  420. case 'string':
  421. case 'localeString':
  422. case 'text':
  423. return 'StringOperators';
  424. case 'boolean':
  425. return 'BooleanOperators';
  426. case 'int':
  427. case 'float':
  428. return 'NumberOperators';
  429. case 'relation':
  430. return undefined;
  431. default:
  432. assertNever(config);
  433. }
  434. return 'String';
  435. }
  436. function getGraphQlInputType(config: CustomFieldConfig): string {
  437. return config.type === 'relation' ? `ID` : getGraphQlType(config);
  438. }
  439. function getGraphQlType(config: CustomFieldConfig): string {
  440. switch (config.type) {
  441. case 'string':
  442. case 'localeString':
  443. case 'text':
  444. return 'String';
  445. case 'datetime':
  446. return 'DateTime';
  447. case 'boolean':
  448. return 'Boolean';
  449. case 'int':
  450. return 'Int';
  451. case 'float':
  452. return 'Float';
  453. case 'relation':
  454. return config.graphQLType || config.entity.name;
  455. default:
  456. assertNever(config);
  457. }
  458. return 'String';
  459. }