graphql-custom-fields.ts 21 KB

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