register-custom-entity-fields.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import { CustomFieldType } from '@vendure/common/lib/shared-types';
  2. import { assertNever } from '@vendure/common/lib/shared-utils';
  3. import {
  4. Column,
  5. ColumnOptions,
  6. ColumnType,
  7. DataSourceOptions,
  8. Index,
  9. JoinColumn,
  10. JoinTable,
  11. ManyToMany,
  12. ManyToOne,
  13. } from 'typeorm';
  14. import { DateUtils } from 'typeorm/util/DateUtils';
  15. import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-field-types';
  16. import { Logger } from '../config/logger/vendure-logger';
  17. import { VendureConfig } from '../config/vendure-config';
  18. import {
  19. CustomAddressFields,
  20. CustomAdministratorFields,
  21. CustomAssetFields,
  22. CustomChannelFields,
  23. CustomCollectionFields,
  24. CustomCollectionFieldsTranslation,
  25. CustomCustomerFields,
  26. CustomCustomerGroupFields,
  27. CustomFacetFields,
  28. CustomFacetFieldsTranslation,
  29. CustomFacetValueFields,
  30. CustomFacetValueFieldsTranslation,
  31. CustomFulfillmentFields,
  32. CustomGlobalSettingsFields,
  33. CustomOrderFields,
  34. CustomOrderLineFields,
  35. CustomPaymentMethodFields,
  36. CustomPaymentMethodFieldsTranslation,
  37. CustomProductFields,
  38. CustomProductFieldsTranslation,
  39. CustomProductOptionFields,
  40. CustomProductOptionFieldsTranslation,
  41. CustomProductOptionGroupFields,
  42. CustomProductOptionGroupFieldsTranslation,
  43. CustomProductVariantFields,
  44. CustomProductVariantFieldsTranslation,
  45. CustomPromotionFields,
  46. CustomPromotionFieldsTranslation,
  47. CustomRegionFields,
  48. CustomRegionFieldsTranslation,
  49. CustomSellerFields,
  50. CustomShippingMethodFields,
  51. CustomShippingMethodFieldsTranslation,
  52. CustomStockLocationFields,
  53. CustomTaxCategoryFields,
  54. CustomTaxRateFields,
  55. CustomUserFields,
  56. CustomZoneFields,
  57. } from './custom-entity-fields';
  58. /**
  59. * The maximum length of the "length" argument of a MySQL varchar column.
  60. */
  61. const MAX_STRING_LENGTH = 65535;
  62. /**
  63. * Dynamically add columns to the custom field entity based on the CustomFields config.
  64. */
  65. function registerCustomFieldsForEntity(
  66. config: VendureConfig,
  67. entityName: keyof CustomFields,
  68. // eslint-disable-next-line @typescript-eslint/prefer-function-type
  69. ctor: { new (): any },
  70. translation = false,
  71. ) {
  72. const customFields = config.customFields && config.customFields[entityName];
  73. const dbEngine = config.dbConnectionOptions.type;
  74. if (customFields) {
  75. for (const customField of customFields) {
  76. const { name, list, defaultValue, nullable } = customField;
  77. const instance = new ctor();
  78. const registerColumn = () => {
  79. if (customField.type === 'relation') {
  80. if (customField.list) {
  81. ManyToMany(type => customField.entity, customField.inverseSide, {
  82. eager: customField.eager,
  83. })(instance, name);
  84. JoinTable()(instance, name);
  85. } else {
  86. ManyToOne(type => customField.entity, customField.inverseSide, {
  87. eager: customField.eager,
  88. })(instance, name);
  89. JoinColumn()(instance, name);
  90. }
  91. } else {
  92. const options: ColumnOptions = {
  93. type: list ? 'simple-json' : getColumnType(dbEngine, customField.type),
  94. default: getDefault(customField, dbEngine),
  95. name,
  96. nullable: nullable === false ? false : true,
  97. unique: customField.unique ?? false,
  98. };
  99. if ((customField.type === 'string' || customField.type === 'localeString') && !list) {
  100. const length = customField.length || 255;
  101. if (MAX_STRING_LENGTH < length) {
  102. throw new Error(
  103. `ERROR: The "length" property of the custom field "${customField.name}" is ` +
  104. `greater than the maximum allowed value of ${MAX_STRING_LENGTH}`,
  105. );
  106. }
  107. options.length = length;
  108. }
  109. if (
  110. customField.type === 'float' &&
  111. typeof customField.defaultValue === 'number' &&
  112. (dbEngine === 'mariadb' || dbEngine === 'mysql')
  113. ) {
  114. // In the MySQL driver, a default float value will get rounded to the nearest integer.
  115. // unless you specify the precision.
  116. const defaultValueDecimalPlaces = customField.defaultValue.toString().split('.')[1];
  117. if (defaultValueDecimalPlaces) {
  118. options.scale = defaultValueDecimalPlaces.length;
  119. }
  120. }
  121. if (
  122. customField.type === 'datetime' &&
  123. options.precision == null &&
  124. // Setting precision on an sqlite datetime will cause
  125. // spurious migration commands. See https://github.com/typeorm/typeorm/issues/2333
  126. dbEngine !== 'sqljs' &&
  127. dbEngine !== 'sqlite' &&
  128. !list
  129. ) {
  130. options.precision = 6;
  131. }
  132. Column(options)(instance, name);
  133. if ((dbEngine === 'mysql' || dbEngine === 'mariadb') && customField.unique === true) {
  134. // The MySQL driver seems to work differently and will only apply a unique
  135. // constraint if an index is defined on the column. For postgres/sqlite it is
  136. // sufficient to add the `unique: true` property to the column options.
  137. Index({ unique: true })(instance, name);
  138. }
  139. }
  140. };
  141. if (translation) {
  142. if (customField.type === 'localeString' || customField.type === 'localeText') {
  143. registerColumn();
  144. }
  145. } else {
  146. if (customField.type !== 'localeString' && customField.type !== 'localeText') {
  147. registerColumn();
  148. }
  149. }
  150. const relationFieldsCount = customFields.filter(f => f.type === 'relation').length;
  151. const nonLocaleStringFieldsCount = customFields.filter(
  152. f => f.type !== 'localeString' && f.type !== 'relation',
  153. ).length;
  154. if (0 < relationFieldsCount && nonLocaleStringFieldsCount === 0) {
  155. // if (customFields.filter(f => f.type === 'relation').length === customFields.length) {
  156. // If there are _only_ relational customFields defined for an Entity, then TypeORM
  157. // errors when attempting to load that entity ("Cannot set property <fieldName> of undefined").
  158. // Therefore as a work-around we will add a "fake" column to the customFields embedded type
  159. // to prevent this error from occurring.
  160. Column({
  161. type: 'boolean',
  162. nullable: true,
  163. comment:
  164. 'A work-around needed when only relational custom fields are defined on an entity',
  165. })(instance, '__fix_relational_custom_fields__');
  166. }
  167. }
  168. }
  169. }
  170. function formatDefaultDatetime(dbEngine: DataSourceOptions['type'], datetime: any): Date | string {
  171. if (!datetime) {
  172. return datetime;
  173. }
  174. switch (dbEngine) {
  175. case 'sqlite':
  176. case 'sqljs':
  177. return DateUtils.mixedDateToUtcDatetimeString(datetime);
  178. case 'mysql':
  179. case 'postgres':
  180. default:
  181. return DateUtils.mixedDateToUtcDatetimeString(datetime);
  182. // return DateUtils.mixedDateToDate(datetime, true, true);
  183. }
  184. }
  185. function getColumnType(
  186. dbEngine: DataSourceOptions['type'],
  187. type: Exclude<CustomFieldType, 'relation'>,
  188. ): ColumnType {
  189. switch (type) {
  190. case 'string':
  191. case 'localeString':
  192. return 'varchar';
  193. case 'text':
  194. case 'localeText':
  195. switch (dbEngine) {
  196. case 'mysql':
  197. case 'mariadb':
  198. return 'longtext';
  199. default:
  200. return 'text';
  201. }
  202. case 'boolean':
  203. switch (dbEngine) {
  204. case 'mysql':
  205. return 'tinyint';
  206. case 'postgres':
  207. return 'bool';
  208. case 'sqlite':
  209. case 'sqljs':
  210. default:
  211. return 'boolean';
  212. }
  213. case 'int':
  214. return 'int';
  215. case 'float':
  216. return 'double precision';
  217. case 'datetime':
  218. switch (dbEngine) {
  219. case 'postgres':
  220. return 'timestamp';
  221. case 'mysql':
  222. case 'sqlite':
  223. case 'sqljs':
  224. default:
  225. return 'datetime';
  226. }
  227. default:
  228. assertNever(type);
  229. }
  230. return 'varchar';
  231. }
  232. function getDefault(customField: CustomFieldConfig, dbEngine: DataSourceOptions['type']) {
  233. const { name, type, list, defaultValue, nullable } = customField;
  234. if (list && defaultValue) {
  235. if (dbEngine === 'mysql') {
  236. // MySQL does not support defaults on TEXT fields, which is what "simple-json" uses
  237. // internally. See https://stackoverflow.com/q/3466872/772859
  238. Logger.warn(
  239. `MySQL does not support default values on list fields (${name}). No default will be set.`,
  240. );
  241. return undefined;
  242. }
  243. return JSON.stringify(defaultValue);
  244. }
  245. return type === 'datetime' ? formatDefaultDatetime(dbEngine, defaultValue) : defaultValue;
  246. }
  247. /**
  248. * Dynamically registers any custom fields with TypeORM. This function should be run at the bootstrap
  249. * stage of the app lifecycle, before the AppModule is initialized.
  250. */
  251. export function registerCustomEntityFields(config: VendureConfig) {
  252. registerCustomFieldsForEntity(config, 'Address', CustomAddressFields);
  253. registerCustomFieldsForEntity(config, 'Administrator', CustomAdministratorFields);
  254. registerCustomFieldsForEntity(config, 'Asset', CustomAssetFields);
  255. registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFields);
  256. registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFieldsTranslation, true);
  257. registerCustomFieldsForEntity(config, 'Channel', CustomChannelFields);
  258. registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields);
  259. registerCustomFieldsForEntity(config, 'CustomerGroup', CustomCustomerGroupFields);
  260. registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields);
  261. registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true);
  262. registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields);
  263. registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true);
  264. registerCustomFieldsForEntity(config, 'Fulfillment', CustomFulfillmentFields);
  265. registerCustomFieldsForEntity(config, 'Order', CustomOrderFields);
  266. registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields);
  267. registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFields);
  268. registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFieldsTranslation, true);
  269. registerCustomFieldsForEntity(config, 'Product', CustomProductFields);
  270. registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true);
  271. registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields);
  272. registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFieldsTranslation, true);
  273. registerCustomFieldsForEntity(config, 'ProductOptionGroup', CustomProductOptionGroupFields);
  274. registerCustomFieldsForEntity(
  275. config,
  276. 'ProductOptionGroup',
  277. CustomProductOptionGroupFieldsTranslation,
  278. true,
  279. );
  280. registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields);
  281. registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true);
  282. registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFields);
  283. registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFieldsTranslation, true);
  284. registerCustomFieldsForEntity(config, 'TaxCategory', CustomTaxCategoryFields);
  285. registerCustomFieldsForEntity(config, 'TaxRate', CustomTaxRateFields);
  286. registerCustomFieldsForEntity(config, 'User', CustomUserFields);
  287. registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields);
  288. registerCustomFieldsForEntity(config, 'Region', CustomRegionFields);
  289. registerCustomFieldsForEntity(config, 'Region', CustomRegionFieldsTranslation, true);
  290. registerCustomFieldsForEntity(config, 'Seller', CustomSellerFields);
  291. registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFields);
  292. registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFieldsTranslation, true);
  293. registerCustomFieldsForEntity(config, 'StockLocation', CustomStockLocationFields);
  294. registerCustomFieldsForEntity(config, 'Zone', CustomZoneFields);
  295. }