@strapi/content-manager 5.60.0 → 5.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/dist/admin/constants/customOrder.js +14 -0
  2. package/dist/admin/constants/customOrder.js.map +1 -0
  3. package/dist/admin/constants/customOrder.mjs +10 -0
  4. package/dist/admin/constants/customOrder.mjs.map +1 -0
  5. package/dist/admin/hooks/useDocument.js +22 -11
  6. package/dist/admin/hooks/useDocument.js.map +1 -1
  7. package/dist/admin/hooks/useDocument.mjs +22 -11
  8. package/dist/admin/hooks/useDocument.mjs.map +1 -1
  9. package/dist/admin/hooks/useDocumentLayout.js.map +1 -1
  10. package/dist/admin/hooks/useDocumentLayout.mjs.map +1 -1
  11. package/dist/admin/hooks/useHasBeenNearViewport.js +70 -0
  12. package/dist/admin/hooks/useHasBeenNearViewport.js.map +1 -0
  13. package/dist/admin/hooks/useHasBeenNearViewport.mjs +46 -0
  14. package/dist/admin/hooks/useHasBeenNearViewport.mjs.map +1 -0
  15. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.js +102 -32
  16. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.js.map +1 -1
  17. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.mjs +104 -35
  18. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.mjs.map +1 -1
  19. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js +20 -17
  20. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js.map +1 -1
  21. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs +20 -17
  22. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs.map +1 -1
  23. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js +15 -1
  24. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js.map +1 -1
  25. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs +17 -3
  26. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs.map +1 -1
  27. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.js +21 -0
  28. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.js.map +1 -1
  29. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.mjs +22 -1
  30. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.mjs.map +1 -1
  31. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js +4 -7
  32. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js.map +1 -1
  33. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs +4 -7
  34. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs.map +1 -1
  35. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js +46 -10
  36. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js.map +1 -1
  37. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs +47 -11
  38. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs.map +1 -1
  39. package/dist/admin/pages/EditView/components/FormLayout.js +201 -51
  40. package/dist/admin/pages/EditView/components/FormLayout.js.map +1 -1
  41. package/dist/admin/pages/EditView/components/FormLayout.mjs +202 -53
  42. package/dist/admin/pages/EditView/components/FormLayout.mjs.map +1 -1
  43. package/dist/admin/pages/ListConfiguration/components/Settings.js +31 -2
  44. package/dist/admin/pages/ListConfiguration/components/Settings.js.map +1 -1
  45. package/dist/admin/pages/ListConfiguration/components/Settings.mjs +31 -2
  46. package/dist/admin/pages/ListConfiguration/components/Settings.mjs.map +1 -1
  47. package/dist/admin/pages/ListView/ListViewPage.js +196 -132
  48. package/dist/admin/pages/ListView/ListViewPage.js.map +1 -1
  49. package/dist/admin/pages/ListView/ListViewPage.mjs +196 -132
  50. package/dist/admin/pages/ListView/ListViewPage.mjs.map +1 -1
  51. package/dist/admin/pages/ListView/components/CustomOrder.js +571 -0
  52. package/dist/admin/pages/ListView/components/CustomOrder.js.map +1 -0
  53. package/dist/admin/pages/ListView/components/CustomOrder.mjs +546 -0
  54. package/dist/admin/pages/ListView/components/CustomOrder.mjs.map +1 -0
  55. package/dist/admin/pages/ListView/components/TableActions.js +4 -1
  56. package/dist/admin/pages/ListView/components/TableActions.js.map +1 -1
  57. package/dist/admin/pages/ListView/components/TableActions.mjs +4 -1
  58. package/dist/admin/pages/ListView/components/TableActions.mjs.map +1 -1
  59. package/dist/admin/services/documents.js +22 -1
  60. package/dist/admin/services/documents.js.map +1 -1
  61. package/dist/admin/services/documents.mjs +21 -2
  62. package/dist/admin/services/documents.mjs.map +1 -1
  63. package/dist/admin/src/constants/customOrder.d.ts +8 -0
  64. package/dist/admin/src/exports.d.ts +1 -1
  65. package/dist/admin/src/hooks/useDocument.d.ts +4 -0
  66. package/dist/admin/src/hooks/useDocumentLayout.d.ts +6 -2
  67. package/dist/admin/src/hooks/useHasBeenNearViewport.d.ts +13 -0
  68. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.d.ts +13 -1
  69. package/dist/admin/src/pages/EditView/components/FormLayout.d.ts +11 -1
  70. package/dist/admin/src/pages/ListView/components/CustomOrder.d.ts +72 -0
  71. package/dist/admin/src/services/documents.d.ts +10 -2
  72. package/dist/admin/translations/en.json.js +9 -0
  73. package/dist/admin/translations/en.json.js.map +1 -1
  74. package/dist/admin/translations/en.json.mjs +9 -0
  75. package/dist/admin/translations/en.json.mjs.map +1 -1
  76. package/dist/server/audit-logs.js +36 -0
  77. package/dist/server/audit-logs.js.map +1 -0
  78. package/dist/server/audit-logs.mjs +33 -0
  79. package/dist/server/audit-logs.mjs.map +1 -0
  80. package/dist/server/bootstrap.js +8 -0
  81. package/dist/server/bootstrap.js.map +1 -1
  82. package/dist/server/bootstrap.mjs +8 -0
  83. package/dist/server/bootstrap.mjs.map +1 -1
  84. package/dist/server/controllers/collection-types.js +8 -0
  85. package/dist/server/controllers/collection-types.js.map +1 -1
  86. package/dist/server/controllers/collection-types.mjs +8 -0
  87. package/dist/server/controllers/collection-types.mjs.map +1 -1
  88. package/dist/server/controllers/content-types.js +15 -0
  89. package/dist/server/controllers/content-types.js.map +1 -1
  90. package/dist/server/controllers/content-types.mjs +16 -1
  91. package/dist/server/controllers/content-types.mjs.map +1 -1
  92. package/dist/server/controllers/index.js +3 -1
  93. package/dist/server/controllers/index.js.map +1 -1
  94. package/dist/server/controllers/index.mjs +2 -0
  95. package/dist/server/controllers/index.mjs.map +1 -1
  96. package/dist/server/controllers/relations.js +5 -1
  97. package/dist/server/controllers/relations.js.map +1 -1
  98. package/dist/server/controllers/relations.mjs +5 -1
  99. package/dist/server/controllers/relations.mjs.map +1 -1
  100. package/dist/server/controllers/single-types.js +10 -1
  101. package/dist/server/controllers/single-types.js.map +1 -1
  102. package/dist/server/controllers/single-types.mjs +10 -1
  103. package/dist/server/controllers/single-types.mjs.map +1 -1
  104. package/dist/server/controllers/validation/model-configuration.js +2 -1
  105. package/dist/server/controllers/validation/model-configuration.js.map +1 -1
  106. package/dist/server/controllers/validation/model-configuration.mjs +2 -1
  107. package/dist/server/controllers/validation/model-configuration.mjs.map +1 -1
  108. package/dist/server/custom-order/constants.js +21 -0
  109. package/dist/server/custom-order/constants.js.map +1 -0
  110. package/dist/server/custom-order/constants.mjs +17 -0
  111. package/dist/server/custom-order/constants.mjs.map +1 -0
  112. package/dist/server/custom-order/controllers/custom-order.js +77 -0
  113. package/dist/server/custom-order/controllers/custom-order.js.map +1 -0
  114. package/dist/server/custom-order/controllers/custom-order.mjs +75 -0
  115. package/dist/server/custom-order/controllers/custom-order.mjs.map +1 -0
  116. package/dist/server/custom-order/controllers/index.js +10 -0
  117. package/dist/server/custom-order/controllers/index.js.map +1 -0
  118. package/dist/server/custom-order/controllers/index.mjs +8 -0
  119. package/dist/server/custom-order/controllers/index.mjs.map +1 -0
  120. package/dist/server/custom-order/index.js +71 -0
  121. package/dist/server/custom-order/index.js.map +1 -0
  122. package/dist/server/custom-order/index.mjs +69 -0
  123. package/dist/server/custom-order/index.mjs.map +1 -0
  124. package/dist/server/custom-order/routes/custom-order.js +38 -0
  125. package/dist/server/custom-order/routes/custom-order.js.map +1 -0
  126. package/dist/server/custom-order/routes/custom-order.mjs +36 -0
  127. package/dist/server/custom-order/routes/custom-order.mjs.map +1 -0
  128. package/dist/server/custom-order/routes/index.js +13 -0
  129. package/dist/server/custom-order/routes/index.js.map +1 -0
  130. package/dist/server/custom-order/routes/index.mjs +11 -0
  131. package/dist/server/custom-order/routes/index.mjs.map +1 -0
  132. package/dist/server/custom-order/services/custom-order.js +286 -0
  133. package/dist/server/custom-order/services/custom-order.js.map +1 -0
  134. package/dist/server/custom-order/services/custom-order.mjs +284 -0
  135. package/dist/server/custom-order/services/custom-order.mjs.map +1 -0
  136. package/dist/server/custom-order/services/document-middleware.js +83 -0
  137. package/dist/server/custom-order/services/document-middleware.js.map +1 -0
  138. package/dist/server/custom-order/services/document-middleware.mjs +81 -0
  139. package/dist/server/custom-order/services/document-middleware.mjs.map +1 -0
  140. package/dist/server/custom-order/services/index.js +10 -0
  141. package/dist/server/custom-order/services/index.js.map +1 -0
  142. package/dist/server/custom-order/services/index.mjs +8 -0
  143. package/dist/server/custom-order/services/index.mjs.map +1 -0
  144. package/dist/server/custom-order/services/utils.js +152 -0
  145. package/dist/server/custom-order/services/utils.js.map +1 -0
  146. package/dist/server/custom-order/services/utils.mjs +148 -0
  147. package/dist/server/custom-order/services/utils.mjs.map +1 -0
  148. package/dist/server/custom-order/utils.js +13 -0
  149. package/dist/server/custom-order/utils.js.map +1 -0
  150. package/dist/server/custom-order/utils.mjs +10 -0
  151. package/dist/server/custom-order/utils.mjs.map +1 -0
  152. package/dist/server/history/constants.js +2 -1
  153. package/dist/server/history/constants.js.map +1 -1
  154. package/dist/server/history/constants.mjs +2 -1
  155. package/dist/server/history/constants.mjs.map +1 -1
  156. package/dist/server/history/controllers/history-version.js +6 -2
  157. package/dist/server/history/controllers/history-version.js.map +1 -1
  158. package/dist/server/history/controllers/history-version.mjs +6 -2
  159. package/dist/server/history/controllers/history-version.mjs.map +1 -1
  160. package/dist/server/history/services/history.js +23 -6
  161. package/dist/server/history/services/history.js.map +1 -1
  162. package/dist/server/history/services/history.mjs +24 -7
  163. package/dist/server/history/services/history.mjs.map +1 -1
  164. package/dist/server/history/services/lifecycles.js +2 -16
  165. package/dist/server/history/services/lifecycles.js.map +1 -1
  166. package/dist/server/history/services/lifecycles.mjs +2 -16
  167. package/dist/server/history/services/lifecycles.mjs.map +1 -1
  168. package/dist/server/register.js +4 -0
  169. package/dist/server/register.js.map +1 -1
  170. package/dist/server/register.mjs +4 -0
  171. package/dist/server/register.mjs.map +1 -1
  172. package/dist/server/routes/index.js +3 -1
  173. package/dist/server/routes/index.js.map +1 -1
  174. package/dist/server/routes/index.mjs +2 -0
  175. package/dist/server/routes/index.mjs.map +1 -1
  176. package/dist/server/services/content-types.js +2 -2
  177. package/dist/server/services/content-types.js.map +1 -1
  178. package/dist/server/services/content-types.mjs +2 -2
  179. package/dist/server/services/content-types.mjs.map +1 -1
  180. package/dist/server/services/document-manager.js +13 -0
  181. package/dist/server/services/document-manager.js.map +1 -1
  182. package/dist/server/services/document-manager.mjs +14 -1
  183. package/dist/server/services/document-manager.mjs.map +1 -1
  184. package/dist/server/services/index.js +3 -1
  185. package/dist/server/services/index.js.map +1 -1
  186. package/dist/server/services/index.mjs +2 -0
  187. package/dist/server/services/index.mjs.map +1 -1
  188. package/dist/server/services/utils/configuration/settings.js +2 -1
  189. package/dist/server/services/utils/configuration/settings.js.map +1 -1
  190. package/dist/server/services/utils/configuration/settings.mjs +2 -1
  191. package/dist/server/services/utils/configuration/settings.mjs.map +1 -1
  192. package/dist/server/src/audit-logs.d.ts +40 -0
  193. package/dist/server/src/audit-logs.d.ts.map +1 -0
  194. package/dist/server/src/bootstrap.d.ts.map +1 -1
  195. package/dist/server/src/controllers/collection-types.d.ts +1 -1
  196. package/dist/server/src/controllers/collection-types.d.ts.map +1 -1
  197. package/dist/server/src/controllers/content-types.d.ts.map +1 -1
  198. package/dist/server/src/controllers/index.d.ts +21 -3
  199. package/dist/server/src/controllers/index.d.ts.map +1 -1
  200. package/dist/server/src/controllers/relations.d.ts +19 -1
  201. package/dist/server/src/controllers/relations.d.ts.map +1 -1
  202. package/dist/server/src/controllers/single-types.d.ts +1 -1
  203. package/dist/server/src/controllers/single-types.d.ts.map +1 -1
  204. package/dist/server/src/custom-order/constants.d.ts +18 -0
  205. package/dist/server/src/custom-order/constants.d.ts.map +1 -0
  206. package/dist/server/src/custom-order/controllers/custom-order.d.ts +8 -0
  207. package/dist/server/src/custom-order/controllers/custom-order.d.ts.map +1 -0
  208. package/dist/server/src/custom-order/controllers/index.d.ts +3 -0
  209. package/dist/server/src/custom-order/controllers/index.d.ts.map +1 -0
  210. package/dist/server/src/custom-order/index.d.ts +8 -0
  211. package/dist/server/src/custom-order/index.d.ts.map +1 -0
  212. package/dist/server/src/custom-order/routes/custom-order.d.ts +4 -0
  213. package/dist/server/src/custom-order/routes/custom-order.d.ts.map +1 -0
  214. package/dist/server/src/custom-order/routes/index.d.ts +10 -0
  215. package/dist/server/src/custom-order/routes/index.d.ts.map +1 -0
  216. package/dist/server/src/custom-order/services/custom-order.d.ts +43 -0
  217. package/dist/server/src/custom-order/services/custom-order.d.ts.map +1 -0
  218. package/dist/server/src/custom-order/services/document-middleware.d.ts +16 -0
  219. package/dist/server/src/custom-order/services/document-middleware.d.ts.map +1 -0
  220. package/dist/server/src/custom-order/services/index.d.ts +24 -0
  221. package/dist/server/src/custom-order/services/index.d.ts.map +1 -0
  222. package/dist/server/src/custom-order/services/utils.d.ts +34 -0
  223. package/dist/server/src/custom-order/services/utils.d.ts.map +1 -0
  224. package/dist/server/src/custom-order/utils.d.ts +6 -0
  225. package/dist/server/src/custom-order/utils.d.ts.map +1 -0
  226. package/dist/server/src/history/constants.d.ts.map +1 -1
  227. package/dist/server/src/history/controllers/history-version.d.ts.map +1 -1
  228. package/dist/server/src/history/services/history.d.ts.map +1 -1
  229. package/dist/server/src/history/services/lifecycles.d.ts.map +1 -1
  230. package/dist/server/src/index.d.ts +22 -6
  231. package/dist/server/src/index.d.ts.map +1 -1
  232. package/dist/server/src/register.d.ts.map +1 -1
  233. package/dist/server/src/routes/index.d.ts.map +1 -1
  234. package/dist/server/src/services/content-types.d.ts +1 -3
  235. package/dist/server/src/services/content-types.d.ts.map +1 -1
  236. package/dist/server/src/services/document-manager.d.ts.map +1 -1
  237. package/dist/server/src/services/index.d.ts +1 -3
  238. package/dist/server/src/services/index.d.ts.map +1 -1
  239. package/dist/server/src/services/utils/configuration/settings.d.ts.map +1 -1
  240. package/dist/shared/contracts/content-types.d.ts +5 -0
  241. package/dist/shared/contracts/content-types.d.ts.map +1 -1
  242. package/dist/shared/contracts/custom-order.d.ts +33 -0
  243. package/dist/shared/contracts/custom-order.d.ts.map +1 -0
  244. package/dist/shared/contracts/custom-order.js +3 -0
  245. package/dist/shared/contracts/custom-order.js.map +1 -0
  246. package/dist/shared/contracts/custom-order.mjs +2 -0
  247. package/dist/shared/contracts/custom-order.mjs.map +1 -0
  248. package/dist/shared/contracts/history-versions.d.ts +0 -1
  249. package/dist/shared/contracts/history-versions.d.ts.map +1 -1
  250. package/dist/shared/contracts/index.d.ts +1 -0
  251. package/dist/shared/contracts/index.d.ts.map +1 -1
  252. package/dist/shared/contracts/index.js +2 -0
  253. package/dist/shared/contracts/index.js.map +1 -1
  254. package/dist/shared/contracts/index.mjs +2 -0
  255. package/dist/shared/contracts/index.mjs.map +1 -1
  256. package/package.json +8 -8
@@ -0,0 +1,284 @@
1
+ import { errors } from '@strapi/utils';
2
+ import storeUtils from '../../services/utils/store.mjs';
3
+ import { POSITION_ATTRIBUTE, ENABLED_CACHE_TTL_MS } from '../constants.mjs';
4
+ import { createDocumentMiddleware } from './document-middleware.mjs';
5
+ import { computeMove } from './utils.mjs';
6
+
7
+ /**
8
+ * Core store key of the list view settings of a content type, see `services/utils/store.ts`.
9
+ */ const CONFIGURATION_KEY_PREFIX = 'plugin_content_manager_configuration_content_types::';
10
+ const createCustomOrderService = ({ strapi })=>{
11
+ const state = {
12
+ enabled: new Set(),
13
+ loadedAt: 0,
14
+ refreshId: 0,
15
+ isRefreshing: false,
16
+ isInitialized: false
17
+ };
18
+ /**
19
+ * Only the content types that received the position attribute at register can be ordered.
20
+ */ const isOrderable = (uid)=>{
21
+ const model = strapi.getModel(uid);
22
+ return model?.attributes?.[POSITION_ATTRIBUTE] !== undefined;
23
+ };
24
+ /**
25
+ * Reads which content types have custom order turned on in their list view settings.
26
+ * The setting lives in the database and can be changed at runtime, possibly from
27
+ * another instance, so it can't be read once and trusted forever.
28
+ */ const refresh = async ()=>{
29
+ state.refreshId += 1;
30
+ const refreshId = state.refreshId;
31
+ const configurations = await storeUtils.getAllConfigurations();
32
+ // A more recent refresh has started in the meantime, its result is the one to keep
33
+ if (refreshId !== state.refreshId) {
34
+ return;
35
+ }
36
+ state.enabled = new Set(configurations.flatMap(({ uid, settings })=>uid && settings?.customOrder === true && isOrderable(uid) ? [
37
+ uid
38
+ ] : []));
39
+ state.loadedAt = Date.now();
40
+ };
41
+ const refreshInBackgroundIfStale = ()=>{
42
+ if (state.isRefreshing || Date.now() - state.loadedAt < ENABLED_CACHE_TTL_MS) {
43
+ return;
44
+ }
45
+ state.isRefreshing = true;
46
+ refresh().then(()=>healMissingPositions()).catch((error)=>{
47
+ strapi.log.warn('[custom-order] Could not refresh the list of ordered content types', {
48
+ error
49
+ });
50
+ }).finally(()=>{
51
+ state.isRefreshing = false;
52
+ });
53
+ };
54
+ const getTable = (uid)=>{
55
+ const { tableName, attributes } = strapi.db.metadata.get(uid);
56
+ const columnOf = (attributeName)=>{
57
+ const attribute = attributes[attributeName];
58
+ return 'columnName' in attribute && attribute.columnName ? attribute.columnName : attributeName;
59
+ };
60
+ return {
61
+ tableName,
62
+ positionColumn: columnOf(POSITION_ATTRIBUTE),
63
+ documentIdColumn: columnOf('documentId')
64
+ };
65
+ };
66
+ const toPosition = (value)=>value === null || value === undefined ? undefined : Number(value);
67
+ /**
68
+ * Takes the lock on the order of a content type until the current transaction ends.
69
+ *
70
+ * The position of a new document and a move both read positions before writing them. Two
71
+ * of them at the same time would give two documents the same position, and nothing could
72
+ * tell those two apart afterwards. The lock is the row holding the list view settings of
73
+ * the content type, which exists whenever custom order is on. `FOR UPDATE` holds it until
74
+ * the transaction ends on PostgreSQL and MySQL. SQLite ignores it, its single connection
75
+ * serializes writers anyway.
76
+ */ const lockOrder = async (uid)=>{
77
+ await strapi.db.queryBuilder('strapi::core-store').select([
78
+ 'id'
79
+ ]).where({
80
+ key: `${CONFIGURATION_KEY_PREFIX}${uid}`
81
+ }).forUpdate().first().execute();
82
+ };
83
+ /**
84
+ * Runs `fn` in a transaction, as the only writer of the order of a content type.
85
+ */ const withOrderLock = async (uid, fn)=>{
86
+ const result = await strapi.db.transaction(async ()=>{
87
+ await lockOrder(uid);
88
+ return fn();
89
+ });
90
+ return result;
91
+ };
92
+ const hasRowWithoutPosition = async (uid)=>{
93
+ const row = await strapi.db.queryBuilder(uid).select([
94
+ 'id'
95
+ ]).where({
96
+ [POSITION_ATTRIBUTE]: {
97
+ $null: true
98
+ }
99
+ }).first().execute();
100
+ return row !== undefined && row !== null;
101
+ };
102
+ /**
103
+ * Position of a document, or undefined if it has none (or does not exist).
104
+ */ const getPosition = async (uid, documentId)=>{
105
+ const row = await strapi.db.queryBuilder(uid).select([
106
+ POSITION_ATTRIBUTE
107
+ ]).where({
108
+ documentId,
109
+ [POSITION_ATTRIBUTE]: {
110
+ $notNull: true
111
+ }
112
+ }).first().execute();
113
+ return toPosition(row?.[POSITION_ATTRIBUTE]);
114
+ };
115
+ /**
116
+ * Position that puts a document before every other one.
117
+ */ const getTopPosition = async (uid)=>{
118
+ const row = await strapi.db.queryBuilder(uid).min(POSITION_ATTRIBUTE).first().execute();
119
+ const min = toPosition(row?.min);
120
+ return min === undefined ? 0 : min - 1;
121
+ };
122
+ /**
123
+ * Gives a position to every row that has none.
124
+ *
125
+ * - Rows of a document that already has a position (e.g. a locale created outside of the
126
+ * Document Service) take that position.
127
+ * - If nothing is ordered yet, documents are ordered by creation, which is the order the
128
+ * API returned so far.
129
+ * - Otherwise the remaining documents were created while custom order was off: like any
130
+ * new entry they go on top, the most recent first.
131
+ */ const assignMissingPositions = async (uid)=>{
132
+ // Checked before taking the lock: this runs in the background on every refresh
133
+ if (!await hasRowWithoutPosition(uid)) {
134
+ return;
135
+ }
136
+ const { tableName, positionColumn, documentIdColumn } = getTable(uid);
137
+ const knex = strapi.db.connection;
138
+ await strapi.db.transaction(async ({ trx })=>{
139
+ await lockOrder(uid);
140
+ const table = ()=>strapi.db.getConnection(tableName).transacting(trx);
141
+ // Another writer may have given the positions in the meantime
142
+ const rowWithoutPosition = await table().whereNull(positionColumn).first('id');
143
+ if (!rowWithoutPosition) {
144
+ return;
145
+ }
146
+ // The updated table can't be read directly in a sub query of its own update on MySQL,
147
+ // the aggregated derived tables below are what make these statements portable.
148
+ const documentIdOfUpdatedRow = knex.ref(`${tableName}.${documentIdColumn}`);
149
+ const topRow = await table().min({
150
+ min: positionColumn
151
+ }).first();
152
+ const top = toPosition(topRow?.min);
153
+ if (top !== undefined) {
154
+ const knownPositions = strapi.db.getConnection(tableName).select({
155
+ d: documentIdColumn
156
+ }).max({
157
+ p: positionColumn
158
+ }).whereNotNull(positionColumn).groupBy(documentIdColumn).as('x');
159
+ const positionOfDocument = knex.queryBuilder().select('x.p').from(knownPositions).where('x.d', documentIdOfUpdatedRow);
160
+ await table().whereNull(positionColumn).update({
161
+ [positionColumn]: knex.raw('(?)', [
162
+ positionOfDocument
163
+ ])
164
+ });
165
+ }
166
+ const firstRowIds = strapi.db.getConnection(tableName).select({
167
+ d: documentIdColumn
168
+ }).min({
169
+ m: 'id'
170
+ }).groupBy(documentIdColumn).as('x');
171
+ const firstRowIdOfDocument = knex.queryBuilder().select('x.m').from(firstRowIds).where('x.d', documentIdOfUpdatedRow);
172
+ await table().whereNull(positionColumn).update({
173
+ [positionColumn]: top === undefined ? knex.raw('(?)', [
174
+ firstRowIdOfDocument
175
+ ]) : knex.raw('? - (?)', [
176
+ top,
177
+ firstRowIdOfDocument
178
+ ])
179
+ });
180
+ });
181
+ };
182
+ /**
183
+ * Rows written outside of the Document Service (an import, a raw query) have no position.
184
+ * Until they get one they sit at one end of the list, which end depends on the database,
185
+ * so they get one at the next refresh rather than at the next move.
186
+ */ const healMissingPositions = async ()=>{
187
+ for (const uid of state.enabled){
188
+ await assignMissingPositions(uid);
189
+ }
190
+ };
191
+ const service = {
192
+ isOrderable,
193
+ refresh,
194
+ withOrderLock,
195
+ /**
196
+ * Whether the entries of a content type currently follow a custom order.
197
+ */ isEnabled (uid) {
198
+ refreshInBackgroundIfStale();
199
+ return state.enabled.has(uid);
200
+ },
201
+ async bootstrap () {
202
+ // Prevent initializing the service twice
203
+ if (state.isInitialized) {
204
+ return;
205
+ }
206
+ state.isInitialized = true;
207
+ await refresh();
208
+ await healMissingPositions();
209
+ strapi.documents.use(createDocumentMiddleware(service));
210
+ },
211
+ /**
212
+ * To call after the list view settings of a content type have been saved.
213
+ */ async applySettings (uid, settings) {
214
+ if (settings?.customOrder === true && isOrderable(uid)) {
215
+ await assignMissingPositions(uid);
216
+ }
217
+ await refresh();
218
+ },
219
+ getTopPosition,
220
+ assignMissingPositions,
221
+ /**
222
+ * Makes sure every row of a document shares the document's position. Needed when a row
223
+ * is created for an existing document, typically a new locale.
224
+ */ async syncDocumentPosition (uid, documentId) {
225
+ await withOrderLock(uid, async ()=>{
226
+ const position = await getPosition(uid, documentId) ?? await getTopPosition(uid);
227
+ await strapi.db.queryBuilder(uid).update({
228
+ [POSITION_ATTRIBUTE]: position
229
+ }).where({
230
+ documentId,
231
+ [POSITION_ATTRIBUTE]: {
232
+ $null: true
233
+ }
234
+ }).execute();
235
+ });
236
+ },
237
+ /**
238
+ * Places a document right before or after another one.
239
+ *
240
+ * Writes go through the query builder on purpose: the position is not content, so
241
+ * moving a document must not run lifecycles nor touch `updatedAt`, which would flag
242
+ * every shifted entry as modified.
243
+ */ async move ({ uid, documentId, anchorId, placement }) {
244
+ const { positionColumn } = getTable(uid);
245
+ await withOrderLock(uid, async ()=>{
246
+ await assignMissingPositions(uid);
247
+ const from = await getPosition(uid, documentId);
248
+ const anchor = await getPosition(uid, anchorId);
249
+ if (from === undefined || anchor === undefined) {
250
+ throw new errors.NotFoundError('Document not found');
251
+ }
252
+ const plan = computeMove({
253
+ from,
254
+ anchor,
255
+ placement
256
+ });
257
+ if (!plan) {
258
+ return;
259
+ }
260
+ if (plan.shift) {
261
+ const { from: start, to: end, by } = plan.shift;
262
+ const shiftedRows = strapi.db.queryBuilder(uid).where({
263
+ [POSITION_ATTRIBUTE]: end === null ? {
264
+ $gte: start
265
+ } : {
266
+ $gte: start,
267
+ $lte: end
268
+ }
269
+ });
270
+ await (by === 1 ? shiftedRows.increment(positionColumn) : shiftedRows.decrement(positionColumn)).execute();
271
+ }
272
+ await strapi.db.queryBuilder(uid).update({
273
+ [POSITION_ATTRIBUTE]: plan.position
274
+ }).where({
275
+ documentId
276
+ }).execute();
277
+ });
278
+ }
279
+ };
280
+ return service;
281
+ };
282
+
283
+ export { createCustomOrderService };
284
+ //# sourceMappingURL=custom-order.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-order.mjs","sources":["../../../../server/src/custom-order/services/custom-order.ts"],"sourcesContent":["import type { Core, UID } from '@strapi/types';\nimport { errors } from '@strapi/utils';\n\nimport storeUtils from '../../services/utils/store';\nimport { ENABLED_CACHE_TTL_MS, POSITION_ATTRIBUTE } from '../constants';\nimport { createDocumentMiddleware } from './document-middleware';\nimport { computeMove, type Placement } from './utils';\n\ninterface StoredConfiguration {\n uid?: string;\n settings?: { customOrder?: boolean };\n}\n\ntype PositionRow = { [POSITION_ATTRIBUTE]: number | string | null };\n\n/**\n * Core store key of the list view settings of a content type, see `services/utils/store.ts`.\n */\nconst CONFIGURATION_KEY_PREFIX = 'plugin_content_manager_configuration_content_types::';\n\nconst createCustomOrderService = ({ strapi }: { strapi: Core.Strapi }) => {\n const state: {\n enabled: Set<string>;\n loadedAt: number;\n refreshId: number;\n isRefreshing: boolean;\n isInitialized: boolean;\n } = {\n enabled: new Set(),\n loadedAt: 0,\n refreshId: 0,\n isRefreshing: false,\n isInitialized: false,\n };\n\n /**\n * Only the content types that received the position attribute at register can be ordered.\n */\n const isOrderable = (uid: string) => {\n const model = strapi.getModel(uid as UID.ContentType);\n\n return model?.attributes?.[POSITION_ATTRIBUTE] !== undefined;\n };\n\n /**\n * Reads which content types have custom order turned on in their list view settings.\n * The setting lives in the database and can be changed at runtime, possibly from\n * another instance, so it can't be read once and trusted forever.\n */\n const refresh = async () => {\n state.refreshId += 1;\n const refreshId = state.refreshId;\n\n const configurations: StoredConfiguration[] = await storeUtils.getAllConfigurations();\n\n // A more recent refresh has started in the meantime, its result is the one to keep\n if (refreshId !== state.refreshId) {\n return;\n }\n\n state.enabled = new Set(\n configurations.flatMap(({ uid, settings }) =>\n uid && settings?.customOrder === true && isOrderable(uid) ? [uid] : []\n )\n );\n state.loadedAt = Date.now();\n };\n\n const refreshInBackgroundIfStale = () => {\n if (state.isRefreshing || Date.now() - state.loadedAt < ENABLED_CACHE_TTL_MS) {\n return;\n }\n\n state.isRefreshing = true;\n\n refresh()\n .then(() => healMissingPositions())\n .catch((error: unknown) => {\n strapi.log.warn('[custom-order] Could not refresh the list of ordered content types', {\n error,\n });\n })\n .finally(() => {\n state.isRefreshing = false;\n });\n };\n\n const getTable = (uid: UID.ContentType) => {\n const { tableName, attributes } = strapi.db.metadata.get(uid);\n\n const columnOf = (attributeName: string) => {\n const attribute = attributes[attributeName];\n\n return 'columnName' in attribute && attribute.columnName\n ? attribute.columnName\n : attributeName;\n };\n\n return {\n tableName,\n positionColumn: columnOf(POSITION_ATTRIBUTE),\n documentIdColumn: columnOf('documentId'),\n };\n };\n\n const toPosition = (value: number | string | null | undefined) =>\n value === null || value === undefined ? undefined : Number(value);\n\n /**\n * Takes the lock on the order of a content type until the current transaction ends.\n *\n * The position of a new document and a move both read positions before writing them. Two\n * of them at the same time would give two documents the same position, and nothing could\n * tell those two apart afterwards. The lock is the row holding the list view settings of\n * the content type, which exists whenever custom order is on. `FOR UPDATE` holds it until\n * the transaction ends on PostgreSQL and MySQL. SQLite ignores it, its single connection\n * serializes writers anyway.\n */\n const lockOrder = async (uid: UID.ContentType) => {\n await strapi.db\n .queryBuilder('strapi::core-store')\n .select(['id'])\n .where({ key: `${CONFIGURATION_KEY_PREFIX}${uid}` })\n .forUpdate()\n .first()\n .execute();\n };\n\n /**\n * Runs `fn` in a transaction, as the only writer of the order of a content type.\n */\n const withOrderLock = async <T>(uid: UID.ContentType, fn: () => Promise<T>): Promise<T> => {\n const result: T = await strapi.db.transaction(async () => {\n await lockOrder(uid);\n\n return fn();\n });\n\n return result;\n };\n\n const hasRowWithoutPosition = async (uid: UID.ContentType) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .select(['id'])\n .where({ [POSITION_ATTRIBUTE]: { $null: true } })\n .first()\n .execute<{ id: number } | undefined>();\n\n return row !== undefined && row !== null;\n };\n\n /**\n * Position of a document, or undefined if it has none (or does not exist).\n */\n const getPosition = async (uid: UID.ContentType, documentId: string) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .select([POSITION_ATTRIBUTE])\n .where({ documentId, [POSITION_ATTRIBUTE]: { $notNull: true } })\n .first()\n .execute<PositionRow | undefined>();\n\n return toPosition(row?.[POSITION_ATTRIBUTE]);\n };\n\n /**\n * Position that puts a document before every other one.\n */\n const getTopPosition = async (uid: UID.ContentType) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .min(POSITION_ATTRIBUTE)\n .first()\n .execute<{ min: number | string | null } | undefined>();\n\n const min = toPosition(row?.min);\n\n return min === undefined ? 0 : min - 1;\n };\n\n /**\n * Gives a position to every row that has none.\n *\n * - Rows of a document that already has a position (e.g. a locale created outside of the\n * Document Service) take that position.\n * - If nothing is ordered yet, documents are ordered by creation, which is the order the\n * API returned so far.\n * - Otherwise the remaining documents were created while custom order was off: like any\n * new entry they go on top, the most recent first.\n */\n const assignMissingPositions = async (uid: UID.ContentType) => {\n // Checked before taking the lock: this runs in the background on every refresh\n if (!(await hasRowWithoutPosition(uid))) {\n return;\n }\n\n const { tableName, positionColumn, documentIdColumn } = getTable(uid);\n const knex = strapi.db.connection;\n\n await strapi.db.transaction(async ({ trx }) => {\n await lockOrder(uid);\n\n const table = () => strapi.db.getConnection(tableName).transacting(trx);\n\n // Another writer may have given the positions in the meantime\n const rowWithoutPosition = await table().whereNull(positionColumn).first('id');\n\n if (!rowWithoutPosition) {\n return;\n }\n\n // The updated table can't be read directly in a sub query of its own update on MySQL,\n // the aggregated derived tables below are what make these statements portable.\n const documentIdOfUpdatedRow = knex.ref(`${tableName}.${documentIdColumn}`);\n\n const topRow: { min: number | string | null } | undefined = await table()\n .min({ min: positionColumn })\n .first();\n const top = toPosition(topRow?.min);\n\n if (top !== undefined) {\n const knownPositions = strapi.db\n .getConnection(tableName)\n .select({ d: documentIdColumn })\n .max({ p: positionColumn })\n .whereNotNull(positionColumn)\n .groupBy(documentIdColumn)\n .as('x');\n\n const positionOfDocument = knex\n .queryBuilder()\n .select('x.p')\n .from(knownPositions)\n .where('x.d', documentIdOfUpdatedRow);\n\n await table()\n .whereNull(positionColumn)\n .update({ [positionColumn]: knex.raw('(?)', [positionOfDocument]) });\n }\n\n const firstRowIds = strapi.db\n .getConnection(tableName)\n .select({ d: documentIdColumn })\n .min({ m: 'id' })\n .groupBy(documentIdColumn)\n .as('x');\n\n const firstRowIdOfDocument = knex\n .queryBuilder()\n .select('x.m')\n .from(firstRowIds)\n .where('x.d', documentIdOfUpdatedRow);\n\n await table()\n .whereNull(positionColumn)\n .update({\n [positionColumn]:\n top === undefined\n ? knex.raw('(?)', [firstRowIdOfDocument])\n : knex.raw('? - (?)', [top, firstRowIdOfDocument]),\n });\n });\n };\n\n /**\n * Rows written outside of the Document Service (an import, a raw query) have no position.\n * Until they get one they sit at one end of the list, which end depends on the database,\n * so they get one at the next refresh rather than at the next move.\n */\n const healMissingPositions = async () => {\n for (const uid of state.enabled) {\n await assignMissingPositions(uid as UID.ContentType);\n }\n };\n\n const service = {\n isOrderable,\n\n refresh,\n\n withOrderLock,\n\n /**\n * Whether the entries of a content type currently follow a custom order.\n */\n isEnabled(uid: string) {\n refreshInBackgroundIfStale();\n\n return state.enabled.has(uid);\n },\n\n async bootstrap() {\n // Prevent initializing the service twice\n if (state.isInitialized) {\n return;\n }\n\n state.isInitialized = true;\n\n await refresh();\n await healMissingPositions();\n\n strapi.documents.use(createDocumentMiddleware(service));\n },\n\n /**\n * To call after the list view settings of a content type have been saved.\n */\n async applySettings(uid: UID.ContentType, settings?: { customOrder?: boolean } | null) {\n if (settings?.customOrder === true && isOrderable(uid)) {\n await assignMissingPositions(uid);\n }\n\n await refresh();\n },\n\n getTopPosition,\n\n assignMissingPositions,\n\n /**\n * Makes sure every row of a document shares the document's position. Needed when a row\n * is created for an existing document, typically a new locale.\n */\n async syncDocumentPosition(uid: UID.ContentType, documentId: string) {\n await withOrderLock(uid, async () => {\n const position = (await getPosition(uid, documentId)) ?? (await getTopPosition(uid));\n\n await strapi.db\n .queryBuilder(uid)\n .update({ [POSITION_ATTRIBUTE]: position })\n .where({ documentId, [POSITION_ATTRIBUTE]: { $null: true } })\n .execute();\n });\n },\n\n /**\n * Places a document right before or after another one.\n *\n * Writes go through the query builder on purpose: the position is not content, so\n * moving a document must not run lifecycles nor touch `updatedAt`, which would flag\n * every shifted entry as modified.\n */\n async move({\n uid,\n documentId,\n anchorId,\n placement,\n }: {\n uid: UID.ContentType;\n documentId: string;\n anchorId: string;\n placement: Placement;\n }) {\n const { positionColumn } = getTable(uid);\n\n await withOrderLock(uid, async () => {\n await assignMissingPositions(uid);\n\n const from = await getPosition(uid, documentId);\n const anchor = await getPosition(uid, anchorId);\n\n if (from === undefined || anchor === undefined) {\n throw new errors.NotFoundError('Document not found');\n }\n\n const plan = computeMove({ from, anchor, placement });\n\n if (!plan) {\n return;\n }\n\n if (plan.shift) {\n const { from: start, to: end, by } = plan.shift;\n\n const shiftedRows = strapi.db.queryBuilder(uid).where({\n [POSITION_ATTRIBUTE]: end === null ? { $gte: start } : { $gte: start, $lte: end },\n });\n\n await (\n by === 1 ? shiftedRows.increment(positionColumn) : shiftedRows.decrement(positionColumn)\n ).execute();\n }\n\n await strapi.db\n .queryBuilder(uid)\n .update({ [POSITION_ATTRIBUTE]: plan.position })\n .where({ documentId })\n .execute();\n });\n },\n };\n\n return service;\n};\n\nexport type CustomOrderService = ReturnType<typeof createCustomOrderService>;\n\nexport { createCustomOrderService };\n"],"names":["CONFIGURATION_KEY_PREFIX","createCustomOrderService","strapi","state","enabled","Set","loadedAt","refreshId","isRefreshing","isInitialized","isOrderable","uid","model","getModel","attributes","POSITION_ATTRIBUTE","undefined","refresh","configurations","storeUtils","getAllConfigurations","flatMap","settings","customOrder","Date","now","refreshInBackgroundIfStale","ENABLED_CACHE_TTL_MS","then","healMissingPositions","catch","error","log","warn","finally","getTable","tableName","db","metadata","get","columnOf","attributeName","attribute","columnName","positionColumn","documentIdColumn","toPosition","value","Number","lockOrder","queryBuilder","select","where","key","forUpdate","first","execute","withOrderLock","fn","result","transaction","hasRowWithoutPosition","row","$null","getPosition","documentId","$notNull","getTopPosition","min","assignMissingPositions","knex","connection","trx","table","getConnection","transacting","rowWithoutPosition","whereNull","documentIdOfUpdatedRow","ref","topRow","top","knownPositions","d","max","p","whereNotNull","groupBy","as","positionOfDocument","from","update","raw","firstRowIds","m","firstRowIdOfDocument","service","isEnabled","has","bootstrap","documents","use","createDocumentMiddleware","applySettings","syncDocumentPosition","position","move","anchorId","placement","anchor","errors","NotFoundError","plan","computeMove","shift","start","to","end","by","shiftedRows","$gte","$lte","increment","decrement"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ var _ = require('lodash');
4
+ var constants = require('../constants.js');
5
+ var utils = require('./utils.js');
6
+
7
+ const setParams = (context, params)=>{
8
+ Object.assign(context, {
9
+ params
10
+ });
11
+ };
12
+ const DEFAULT_SORT = {
13
+ [constants.POSITION_ATTRIBUTE]: 'asc'
14
+ };
15
+ /**
16
+ * Keeps the Document Service in line with the custom order of a content type:
17
+ * documents come back in that order unless a sort is requested, new documents go on top,
18
+ * and the position can only be changed by moving a document.
19
+ */ const createDocumentMiddleware = (customOrder)=>async (context, next)=>{
20
+ if (!customOrder.isOrderable(context.uid)) {
21
+ return next();
22
+ }
23
+ let params = context.params ?? {};
24
+ // The position is not content: whatever a client puts in the data is dropped, whether
25
+ // custom order is on for the content type or not. A value written while it is off would
26
+ // otherwise be taken for a real position once it is turned on.
27
+ if (params.data && constants.POSITION_ATTRIBUTE in params.data) {
28
+ params = {
29
+ ...params,
30
+ data: _.omit(params.data, constants.POSITION_ATTRIBUTE)
31
+ };
32
+ setParams(context, params);
33
+ }
34
+ if (!customOrder.isEnabled(context.uid)) {
35
+ return next();
36
+ }
37
+ switch(context.action){
38
+ case 'findMany':
39
+ case 'findFirst':
40
+ {
41
+ const sort = utils.withTieBreaker(utils.isEmptySort(params.sort) ? DEFAULT_SORT : params.sort);
42
+ if (sort !== params.sort) {
43
+ setParams(context, {
44
+ ...params,
45
+ sort
46
+ });
47
+ }
48
+ return next();
49
+ }
50
+ case 'create':
51
+ case 'clone':
52
+ {
53
+ // The top position is read then written with the document: under the lock, so that
54
+ // two new documents can't get the same one, and in one transaction with the insert.
55
+ return customOrder.withOrderLock(context.uid, async ()=>{
56
+ const position = await customOrder.getTopPosition(context.uid);
57
+ setParams(context, {
58
+ ...params,
59
+ data: {
60
+ ...params.data,
61
+ [constants.POSITION_ATTRIBUTE]: position
62
+ }
63
+ });
64
+ return next();
65
+ });
66
+ }
67
+ case 'update':
68
+ {
69
+ const result = await next();
70
+ const entry = result;
71
+ // Updating a locale that does not exist yet creates a row without position
72
+ if (entry && params.documentId && (entry[constants.POSITION_ATTRIBUTE] ?? null) === null) {
73
+ await customOrder.syncDocumentPosition(context.uid, params.documentId);
74
+ }
75
+ return result;
76
+ }
77
+ default:
78
+ return next();
79
+ }
80
+ };
81
+
82
+ exports.createDocumentMiddleware = createDocumentMiddleware;
83
+ //# sourceMappingURL=document-middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-middleware.js","sources":["../../../../server/src/custom-order/services/document-middleware.ts"],"sourcesContent":["import type { Modules, UID } from '@strapi/types';\nimport { omit } from 'lodash';\n\nimport { POSITION_ATTRIBUTE } from '../constants';\nimport { isEmptySort, withTieBreaker } from './utils';\n\ntype Context = Modules.Documents.Middleware.Context;\n\ninterface Params {\n sort?: unknown;\n data?: Record<string, unknown>;\n documentId?: string;\n}\n\ninterface CustomOrder {\n isOrderable(uid: string): boolean;\n isEnabled(uid: string): boolean;\n getTopPosition(uid: UID.ContentType): Promise<number>;\n syncDocumentPosition(uid: UID.ContentType, documentId: string): Promise<void>;\n withOrderLock<T>(uid: UID.ContentType, fn: () => Promise<T>): Promise<T>;\n}\n\nconst setParams = (context: Context, params: Params) => {\n Object.assign(context, { params });\n};\n\nconst DEFAULT_SORT = { [POSITION_ATTRIBUTE]: 'asc' };\n\n/**\n * Keeps the Document Service in line with the custom order of a content type:\n * documents come back in that order unless a sort is requested, new documents go on top,\n * and the position can only be changed by moving a document.\n */\nconst createDocumentMiddleware =\n (customOrder: CustomOrder): Modules.Documents.Middleware.Middleware =>\n async (context, next) => {\n if (!customOrder.isOrderable(context.uid)) {\n return next();\n }\n\n let params = (context.params ?? {}) as Params;\n\n // The position is not content: whatever a client puts in the data is dropped, whether\n // custom order is on for the content type or not. A value written while it is off would\n // otherwise be taken for a real position once it is turned on.\n if (params.data && POSITION_ATTRIBUTE in params.data) {\n params = { ...params, data: omit(params.data, POSITION_ATTRIBUTE) };\n setParams(context, params);\n }\n\n if (!customOrder.isEnabled(context.uid)) {\n return next();\n }\n\n switch (context.action) {\n case 'findMany':\n case 'findFirst': {\n const sort = withTieBreaker(isEmptySort(params.sort) ? DEFAULT_SORT : params.sort);\n\n if (sort !== params.sort) {\n setParams(context, { ...params, sort });\n }\n\n return next();\n }\n\n case 'create':\n case 'clone': {\n // The top position is read then written with the document: under the lock, so that\n // two new documents can't get the same one, and in one transaction with the insert.\n return customOrder.withOrderLock(context.uid, async () => {\n const position = await customOrder.getTopPosition(context.uid);\n\n setParams(context, {\n ...params,\n data: { ...params.data, [POSITION_ATTRIBUTE]: position },\n });\n\n return next();\n });\n }\n\n case 'update': {\n const result = await next();\n const entry = result as Record<string, unknown> | null | undefined;\n\n // Updating a locale that does not exist yet creates a row without position\n if (entry && params.documentId && (entry[POSITION_ATTRIBUTE] ?? null) === null) {\n await customOrder.syncDocumentPosition(context.uid, params.documentId);\n }\n\n return result;\n }\n\n default:\n return next();\n }\n };\n\nexport { createDocumentMiddleware };\n"],"names":["setParams","context","params","Object","assign","DEFAULT_SORT","POSITION_ATTRIBUTE","createDocumentMiddleware","customOrder","next","isOrderable","uid","data","omit","isEnabled","action","sort","withTieBreaker","isEmptySort","withOrderLock","position","getTopPosition","result","entry","documentId","syncDocumentPosition"],"mappings":";;;;;;AAsBA,MAAMA,SAAAA,GAAY,CAACC,OAAAA,EAAkBC,MAAAA,GAAAA;IACnCC,MAAAA,CAAOC,MAAM,CAACH,OAAAA,EAAS;AAAEC,QAAAA;AAAO,KAAA,CAAA;AAClC,CAAA;AAEA,MAAMG,YAAAA,GAAe;AAAE,IAAA,CAACC,+BAAqB;AAAM,CAAA;AAEnD;;;;AAIC,IACD,MAAMC,wBAAAA,GACJ,CAACC,WAAAA,GACD,OAAOP,OAAAA,EAASQ,IAAAA,GAAAA;AACd,QAAA,IAAI,CAACD,WAAAA,CAAYE,WAAW,CAACT,OAAAA,CAAQU,GAAG,CAAA,EAAG;YACzC,OAAOF,IAAAA,EAAAA;AACT,QAAA;AAEA,QAAA,IAAIP,MAAAA,GAAUD,OAAAA,CAAQC,MAAM,IAAI,EAAC;;;;AAKjC,QAAA,IAAIA,OAAOU,IAAI,IAAIN,4BAAAA,IAAsBJ,MAAAA,CAAOU,IAAI,EAAE;YACpDV,MAAAA,GAAS;AAAE,gBAAA,GAAGA,MAAM;gBAAEU,IAAAA,EAAMC,MAAAA,CAAKX,MAAAA,CAAOU,IAAI,EAAEN,4BAAAA;AAAoB,aAAA;AAClEN,YAAAA,SAAAA,CAAUC,OAAAA,EAASC,MAAAA,CAAAA;AACrB,QAAA;AAEA,QAAA,IAAI,CAACM,WAAAA,CAAYM,SAAS,CAACb,OAAAA,CAAQU,GAAG,CAAA,EAAG;YACvC,OAAOF,IAAAA,EAAAA;AACT,QAAA;AAEA,QAAA,OAAQR,QAAQc,MAAM;YACpB,KAAK,UAAA;YACL,KAAK,WAAA;AAAa,gBAAA;oBAChB,MAAMC,IAAAA,GAAOC,qBAAeC,iBAAAA,CAAYhB,MAAAA,CAAOc,IAAI,CAAA,GAAIX,YAAAA,GAAeH,OAAOc,IAAI,CAAA;oBAEjF,IAAIA,IAAAA,KAASd,MAAAA,CAAOc,IAAI,EAAE;AACxBhB,wBAAAA,SAAAA,CAAUC,OAAAA,EAAS;AAAE,4BAAA,GAAGC,MAAM;AAAEc,4BAAAA;AAAK,yBAAA,CAAA;AACvC,oBAAA;oBAEA,OAAOP,IAAAA,EAAAA;AACT,gBAAA;YAEA,KAAK,QAAA;YACL,KAAK,OAAA;AAAS,gBAAA;;;AAGZ,oBAAA,OAAOD,WAAAA,CAAYW,aAAa,CAAClB,OAAAA,CAAQU,GAAG,EAAE,UAAA;AAC5C,wBAAA,MAAMS,WAAW,MAAMZ,WAAAA,CAAYa,cAAc,CAACpB,QAAQU,GAAG,CAAA;AAE7DX,wBAAAA,SAAAA,CAAUC,OAAAA,EAAS;AACjB,4BAAA,GAAGC,MAAM;4BACTU,IAAAA,EAAM;AAAE,gCAAA,GAAGV,OAAOU,IAAI;AAAE,gCAAA,CAACN,+BAAqBc;AAAS;AACzD,yBAAA,CAAA;wBAEA,OAAOX,IAAAA,EAAAA;AACT,oBAAA,CAAA,CAAA;AACF,gBAAA;YAEA,KAAK,QAAA;AAAU,gBAAA;AACb,oBAAA,MAAMa,SAAS,MAAMb,IAAAA,EAAAA;AACrB,oBAAA,MAAMc,KAAAA,GAAQD,MAAAA;;AAGd,oBAAA,IAAIC,KAAAA,IAASrB,MAAAA,CAAOsB,UAAU,IAAI,CAACD,KAAK,CAACjB,4BAAAA,CAAmB,IAAI,IAAG,MAAO,IAAA,EAAM;AAC9E,wBAAA,MAAME,YAAYiB,oBAAoB,CAACxB,QAAQU,GAAG,EAAET,OAAOsB,UAAU,CAAA;AACvE,oBAAA;oBAEA,OAAOF,MAAAA;AACT,gBAAA;AAEA,YAAA;gBACE,OAAOb,IAAAA,EAAAA;AACX;AACF,IAAA;;;;"}
@@ -0,0 +1,81 @@
1
+ import { omit } from 'lodash';
2
+ import { POSITION_ATTRIBUTE } from '../constants.mjs';
3
+ import { withTieBreaker, isEmptySort } from './utils.mjs';
4
+
5
+ const setParams = (context, params)=>{
6
+ Object.assign(context, {
7
+ params
8
+ });
9
+ };
10
+ const DEFAULT_SORT = {
11
+ [POSITION_ATTRIBUTE]: 'asc'
12
+ };
13
+ /**
14
+ * Keeps the Document Service in line with the custom order of a content type:
15
+ * documents come back in that order unless a sort is requested, new documents go on top,
16
+ * and the position can only be changed by moving a document.
17
+ */ const createDocumentMiddleware = (customOrder)=>async (context, next)=>{
18
+ if (!customOrder.isOrderable(context.uid)) {
19
+ return next();
20
+ }
21
+ let params = context.params ?? {};
22
+ // The position is not content: whatever a client puts in the data is dropped, whether
23
+ // custom order is on for the content type or not. A value written while it is off would
24
+ // otherwise be taken for a real position once it is turned on.
25
+ if (params.data && POSITION_ATTRIBUTE in params.data) {
26
+ params = {
27
+ ...params,
28
+ data: omit(params.data, POSITION_ATTRIBUTE)
29
+ };
30
+ setParams(context, params);
31
+ }
32
+ if (!customOrder.isEnabled(context.uid)) {
33
+ return next();
34
+ }
35
+ switch(context.action){
36
+ case 'findMany':
37
+ case 'findFirst':
38
+ {
39
+ const sort = withTieBreaker(isEmptySort(params.sort) ? DEFAULT_SORT : params.sort);
40
+ if (sort !== params.sort) {
41
+ setParams(context, {
42
+ ...params,
43
+ sort
44
+ });
45
+ }
46
+ return next();
47
+ }
48
+ case 'create':
49
+ case 'clone':
50
+ {
51
+ // The top position is read then written with the document: under the lock, so that
52
+ // two new documents can't get the same one, and in one transaction with the insert.
53
+ return customOrder.withOrderLock(context.uid, async ()=>{
54
+ const position = await customOrder.getTopPosition(context.uid);
55
+ setParams(context, {
56
+ ...params,
57
+ data: {
58
+ ...params.data,
59
+ [POSITION_ATTRIBUTE]: position
60
+ }
61
+ });
62
+ return next();
63
+ });
64
+ }
65
+ case 'update':
66
+ {
67
+ const result = await next();
68
+ const entry = result;
69
+ // Updating a locale that does not exist yet creates a row without position
70
+ if (entry && params.documentId && (entry[POSITION_ATTRIBUTE] ?? null) === null) {
71
+ await customOrder.syncDocumentPosition(context.uid, params.documentId);
72
+ }
73
+ return result;
74
+ }
75
+ default:
76
+ return next();
77
+ }
78
+ };
79
+
80
+ export { createDocumentMiddleware };
81
+ //# sourceMappingURL=document-middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-middleware.mjs","sources":["../../../../server/src/custom-order/services/document-middleware.ts"],"sourcesContent":["import type { Modules, UID } from '@strapi/types';\nimport { omit } from 'lodash';\n\nimport { POSITION_ATTRIBUTE } from '../constants';\nimport { isEmptySort, withTieBreaker } from './utils';\n\ntype Context = Modules.Documents.Middleware.Context;\n\ninterface Params {\n sort?: unknown;\n data?: Record<string, unknown>;\n documentId?: string;\n}\n\ninterface CustomOrder {\n isOrderable(uid: string): boolean;\n isEnabled(uid: string): boolean;\n getTopPosition(uid: UID.ContentType): Promise<number>;\n syncDocumentPosition(uid: UID.ContentType, documentId: string): Promise<void>;\n withOrderLock<T>(uid: UID.ContentType, fn: () => Promise<T>): Promise<T>;\n}\n\nconst setParams = (context: Context, params: Params) => {\n Object.assign(context, { params });\n};\n\nconst DEFAULT_SORT = { [POSITION_ATTRIBUTE]: 'asc' };\n\n/**\n * Keeps the Document Service in line with the custom order of a content type:\n * documents come back in that order unless a sort is requested, new documents go on top,\n * and the position can only be changed by moving a document.\n */\nconst createDocumentMiddleware =\n (customOrder: CustomOrder): Modules.Documents.Middleware.Middleware =>\n async (context, next) => {\n if (!customOrder.isOrderable(context.uid)) {\n return next();\n }\n\n let params = (context.params ?? {}) as Params;\n\n // The position is not content: whatever a client puts in the data is dropped, whether\n // custom order is on for the content type or not. A value written while it is off would\n // otherwise be taken for a real position once it is turned on.\n if (params.data && POSITION_ATTRIBUTE in params.data) {\n params = { ...params, data: omit(params.data, POSITION_ATTRIBUTE) };\n setParams(context, params);\n }\n\n if (!customOrder.isEnabled(context.uid)) {\n return next();\n }\n\n switch (context.action) {\n case 'findMany':\n case 'findFirst': {\n const sort = withTieBreaker(isEmptySort(params.sort) ? DEFAULT_SORT : params.sort);\n\n if (sort !== params.sort) {\n setParams(context, { ...params, sort });\n }\n\n return next();\n }\n\n case 'create':\n case 'clone': {\n // The top position is read then written with the document: under the lock, so that\n // two new documents can't get the same one, and in one transaction with the insert.\n return customOrder.withOrderLock(context.uid, async () => {\n const position = await customOrder.getTopPosition(context.uid);\n\n setParams(context, {\n ...params,\n data: { ...params.data, [POSITION_ATTRIBUTE]: position },\n });\n\n return next();\n });\n }\n\n case 'update': {\n const result = await next();\n const entry = result as Record<string, unknown> | null | undefined;\n\n // Updating a locale that does not exist yet creates a row without position\n if (entry && params.documentId && (entry[POSITION_ATTRIBUTE] ?? null) === null) {\n await customOrder.syncDocumentPosition(context.uid, params.documentId);\n }\n\n return result;\n }\n\n default:\n return next();\n }\n };\n\nexport { createDocumentMiddleware };\n"],"names":["setParams","context","params","Object","assign","DEFAULT_SORT","POSITION_ATTRIBUTE","createDocumentMiddleware","customOrder","next","isOrderable","uid","data","omit","isEnabled","action","sort","withTieBreaker","isEmptySort","withOrderLock","position","getTopPosition","result","entry","documentId","syncDocumentPosition"],"mappings":";;;;AAsBA,MAAMA,SAAAA,GAAY,CAACC,OAAAA,EAAkBC,MAAAA,GAAAA;IACnCC,MAAAA,CAAOC,MAAM,CAACH,OAAAA,EAAS;AAAEC,QAAAA;AAAO,KAAA,CAAA;AAClC,CAAA;AAEA,MAAMG,YAAAA,GAAe;AAAE,IAAA,CAACC,qBAAqB;AAAM,CAAA;AAEnD;;;;AAIC,IACD,MAAMC,wBAAAA,GACJ,CAACC,WAAAA,GACD,OAAOP,OAAAA,EAASQ,IAAAA,GAAAA;AACd,QAAA,IAAI,CAACD,WAAAA,CAAYE,WAAW,CAACT,OAAAA,CAAQU,GAAG,CAAA,EAAG;YACzC,OAAOF,IAAAA,EAAAA;AACT,QAAA;AAEA,QAAA,IAAIP,MAAAA,GAAUD,OAAAA,CAAQC,MAAM,IAAI,EAAC;;;;AAKjC,QAAA,IAAIA,OAAOU,IAAI,IAAIN,kBAAAA,IAAsBJ,MAAAA,CAAOU,IAAI,EAAE;YACpDV,MAAAA,GAAS;AAAE,gBAAA,GAAGA,MAAM;gBAAEU,IAAAA,EAAMC,IAAAA,CAAKX,MAAAA,CAAOU,IAAI,EAAEN,kBAAAA;AAAoB,aAAA;AAClEN,YAAAA,SAAAA,CAAUC,OAAAA,EAASC,MAAAA,CAAAA;AACrB,QAAA;AAEA,QAAA,IAAI,CAACM,WAAAA,CAAYM,SAAS,CAACb,OAAAA,CAAQU,GAAG,CAAA,EAAG;YACvC,OAAOF,IAAAA,EAAAA;AACT,QAAA;AAEA,QAAA,OAAQR,QAAQc,MAAM;YACpB,KAAK,UAAA;YACL,KAAK,WAAA;AAAa,gBAAA;oBAChB,MAAMC,IAAAA,GAAOC,eAAeC,WAAAA,CAAYhB,MAAAA,CAAOc,IAAI,CAAA,GAAIX,YAAAA,GAAeH,OAAOc,IAAI,CAAA;oBAEjF,IAAIA,IAAAA,KAASd,MAAAA,CAAOc,IAAI,EAAE;AACxBhB,wBAAAA,SAAAA,CAAUC,OAAAA,EAAS;AAAE,4BAAA,GAAGC,MAAM;AAAEc,4BAAAA;AAAK,yBAAA,CAAA;AACvC,oBAAA;oBAEA,OAAOP,IAAAA,EAAAA;AACT,gBAAA;YAEA,KAAK,QAAA;YACL,KAAK,OAAA;AAAS,gBAAA;;;AAGZ,oBAAA,OAAOD,WAAAA,CAAYW,aAAa,CAAClB,OAAAA,CAAQU,GAAG,EAAE,UAAA;AAC5C,wBAAA,MAAMS,WAAW,MAAMZ,WAAAA,CAAYa,cAAc,CAACpB,QAAQU,GAAG,CAAA;AAE7DX,wBAAAA,SAAAA,CAAUC,OAAAA,EAAS;AACjB,4BAAA,GAAGC,MAAM;4BACTU,IAAAA,EAAM;AAAE,gCAAA,GAAGV,OAAOU,IAAI;AAAE,gCAAA,CAACN,qBAAqBc;AAAS;AACzD,yBAAA,CAAA;wBAEA,OAAOX,IAAAA,EAAAA;AACT,oBAAA,CAAA,CAAA;AACF,gBAAA;YAEA,KAAK,QAAA;AAAU,gBAAA;AACb,oBAAA,MAAMa,SAAS,MAAMb,IAAAA,EAAAA;AACrB,oBAAA,MAAMc,KAAAA,GAAQD,MAAAA;;AAGd,oBAAA,IAAIC,KAAAA,IAASrB,MAAAA,CAAOsB,UAAU,IAAI,CAACD,KAAK,CAACjB,kBAAAA,CAAmB,IAAI,IAAG,MAAO,IAAA,EAAM;AAC9E,wBAAA,MAAME,YAAYiB,oBAAoB,CAACxB,QAAQU,GAAG,EAAET,OAAOsB,UAAU,CAAA;AACvE,oBAAA;oBAEA,OAAOF,MAAAA;AACT,gBAAA;AAEA,YAAA;gBACE,OAAOb,IAAAA,EAAAA;AACX;AACF,IAAA;;;;"}
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ var customOrder = require('./custom-order.js');
4
+
5
+ const services = {
6
+ 'custom-order': customOrder.createCustomOrderService
7
+ };
8
+
9
+ exports.services = services;
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../../server/src/custom-order/services/index.ts"],"sourcesContent":["import type { Plugin } from '@strapi/types';\nimport { createCustomOrderService } from './custom-order';\n\nexport const services = {\n 'custom-order': createCustomOrderService,\n} satisfies Plugin.LoadedPlugin['services'];\n"],"names":["services","createCustomOrderService"],"mappings":";;;;MAGaA,QAAAA,GAAW;IACtB,cAAA,EAAgBC;AAClB;;;;"}
@@ -0,0 +1,8 @@
1
+ import { createCustomOrderService } from './custom-order.mjs';
2
+
3
+ const services = {
4
+ 'custom-order': createCustomOrderService
5
+ };
6
+
7
+ export { services };
8
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../../../server/src/custom-order/services/index.ts"],"sourcesContent":["import type { Plugin } from '@strapi/types';\nimport { createCustomOrderService } from './custom-order';\n\nexport const services = {\n 'custom-order': createCustomOrderService,\n} satisfies Plugin.LoadedPlugin['services'];\n"],"names":["services","createCustomOrderService"],"mappings":";;MAGaA,QAAAA,GAAW;IACtB,cAAA,EAAgBC;AAClB;;;;"}