graphql-custom-fields.ts 15 KB

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