graphql-custom-fields.ts 19 KB

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