graphql-custom-fields.ts 13 KB

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