@communecter/cocolight-api-client 1.0.155 → 1.0.156

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 (329) hide show
  1. package/dist/1035.cocolight-api-client.browser.js +1 -1
  2. package/dist/1035.cocolight-api-client.cjs +1 -1
  3. package/dist/1483.cocolight-api-client.browser.js +1 -1
  4. package/dist/1483.cocolight-api-client.cjs +1 -1
  5. package/dist/1491.cocolight-api-client.browser.js +1 -1
  6. package/dist/1491.cocolight-api-client.cjs +1 -1
  7. package/dist/1494.cocolight-api-client.browser.js +1 -1
  8. package/dist/1494.cocolight-api-client.cjs +1 -1
  9. package/dist/1641.cocolight-api-client.browser.js +1 -1
  10. package/dist/1641.cocolight-api-client.cjs +1 -1
  11. package/dist/1678.cocolight-api-client.browser.js +1 -1
  12. package/dist/1678.cocolight-api-client.cjs +1 -1
  13. package/dist/2041.cocolight-api-client.browser.js +1 -1
  14. package/dist/2041.cocolight-api-client.cjs +1 -1
  15. package/dist/2302.cocolight-api-client.browser.js +1 -1
  16. package/dist/2302.cocolight-api-client.cjs +1 -1
  17. package/dist/2313.cocolight-api-client.browser.js +1 -1
  18. package/dist/2313.cocolight-api-client.cjs +1 -1
  19. package/dist/2379.cocolight-api-client.browser.js +1 -1
  20. package/dist/2379.cocolight-api-client.cjs +1 -1
  21. package/dist/2439.cocolight-api-client.browser.js +1 -1
  22. package/dist/2439.cocolight-api-client.cjs +1 -1
  23. package/dist/2594.cocolight-api-client.browser.js +1 -1
  24. package/dist/2594.cocolight-api-client.cjs +1 -1
  25. package/dist/2766.cocolight-api-client.browser.js +1 -1
  26. package/dist/2766.cocolight-api-client.cjs +1 -1
  27. package/dist/2812.cocolight-api-client.browser.js +1 -1
  28. package/dist/2812.cocolight-api-client.cjs +1 -1
  29. package/dist/2918.cocolight-api-client.browser.js +1 -1
  30. package/dist/2918.cocolight-api-client.cjs +1 -1
  31. package/dist/3028.cocolight-api-client.browser.js +1 -1
  32. package/dist/3028.cocolight-api-client.cjs +1 -1
  33. package/dist/3330.cocolight-api-client.browser.js +1 -1
  34. package/dist/3330.cocolight-api-client.cjs +1 -1
  35. package/dist/3371.cocolight-api-client.browser.js +1 -1
  36. package/dist/3371.cocolight-api-client.cjs +1 -1
  37. package/dist/3401.cocolight-api-client.browser.js +1 -1
  38. package/dist/3401.cocolight-api-client.cjs +1 -1
  39. package/dist/3516.cocolight-api-client.browser.js +1 -1
  40. package/dist/3516.cocolight-api-client.cjs +1 -1
  41. package/dist/3593.cocolight-api-client.browser.js +1 -1
  42. package/dist/3593.cocolight-api-client.cjs +1 -1
  43. package/dist/3764.cocolight-api-client.browser.js +1 -1
  44. package/dist/3764.cocolight-api-client.cjs +1 -1
  45. package/dist/3873.cocolight-api-client.browser.js +1 -1
  46. package/dist/3873.cocolight-api-client.cjs +1 -1
  47. package/dist/3911.cocolight-api-client.browser.js +1 -1
  48. package/dist/3911.cocolight-api-client.cjs +1 -1
  49. package/dist/4031.cocolight-api-client.browser.js +1 -1
  50. package/dist/4031.cocolight-api-client.cjs +1 -1
  51. package/dist/4152.cocolight-api-client.browser.js +1 -1
  52. package/dist/4152.cocolight-api-client.cjs +1 -1
  53. package/dist/4203.cocolight-api-client.browser.js +1 -1
  54. package/dist/4203.cocolight-api-client.cjs +1 -1
  55. package/dist/424.cocolight-api-client.browser.js +1 -1
  56. package/dist/424.cocolight-api-client.cjs +1 -1
  57. package/dist/435.cocolight-api-client.browser.js +1 -1
  58. package/dist/435.cocolight-api-client.cjs +1 -1
  59. package/dist/4402.cocolight-api-client.browser.js +1 -1
  60. package/dist/4402.cocolight-api-client.cjs +1 -1
  61. package/dist/4425.cocolight-api-client.browser.js +1 -1
  62. package/dist/4425.cocolight-api-client.cjs +1 -1
  63. package/dist/4472.cocolight-api-client.browser.js +1 -1
  64. package/dist/4472.cocolight-api-client.cjs +1 -1
  65. package/dist/4914.cocolight-api-client.browser.js +1 -1
  66. package/dist/4914.cocolight-api-client.cjs +1 -1
  67. package/dist/4959.cocolight-api-client.browser.js +1 -1
  68. package/dist/4959.cocolight-api-client.cjs +1 -1
  69. package/dist/5100.cocolight-api-client.browser.js +1 -1
  70. package/dist/5100.cocolight-api-client.cjs +1 -1
  71. package/dist/5111.cocolight-api-client.browser.js +1 -1
  72. package/dist/5111.cocolight-api-client.cjs +1 -1
  73. package/dist/5286.cocolight-api-client.browser.js +1 -1
  74. package/dist/5286.cocolight-api-client.cjs +1 -1
  75. package/dist/5380.cocolight-api-client.browser.js +1 -1
  76. package/dist/5380.cocolight-api-client.cjs +1 -1
  77. package/dist/5383.cocolight-api-client.browser.js +1 -1
  78. package/dist/5383.cocolight-api-client.cjs +1 -1
  79. package/dist/5480.cocolight-api-client.browser.js +1 -1
  80. package/dist/5480.cocolight-api-client.cjs +1 -1
  81. package/dist/5541.cocolight-api-client.browser.js +1 -1
  82. package/dist/5541.cocolight-api-client.cjs +1 -1
  83. package/dist/5839.cocolight-api-client.browser.js +1 -1
  84. package/dist/5839.cocolight-api-client.cjs +1 -1
  85. package/dist/5862.cocolight-api-client.browser.js +1 -1
  86. package/dist/5862.cocolight-api-client.cjs +1 -1
  87. package/dist/5869.cocolight-api-client.browser.js +1 -1
  88. package/dist/5869.cocolight-api-client.cjs +1 -1
  89. package/dist/588.cocolight-api-client.browser.js +1 -1
  90. package/dist/588.cocolight-api-client.cjs +1 -1
  91. package/dist/601.cocolight-api-client.browser.js +1 -1
  92. package/dist/601.cocolight-api-client.cjs +1 -1
  93. package/dist/6012.cocolight-api-client.browser.js +1 -1
  94. package/dist/6012.cocolight-api-client.cjs +1 -1
  95. package/dist/6200.cocolight-api-client.browser.js +1 -1
  96. package/dist/6200.cocolight-api-client.cjs +1 -1
  97. package/dist/6369.cocolight-api-client.browser.js +1 -1
  98. package/dist/6369.cocolight-api-client.cjs +1 -1
  99. package/dist/7009.cocolight-api-client.browser.js +1 -1
  100. package/dist/7009.cocolight-api-client.cjs +1 -1
  101. package/dist/725.cocolight-api-client.browser.js +1 -1
  102. package/dist/725.cocolight-api-client.cjs +1 -1
  103. package/dist/7320.cocolight-api-client.browser.js +1 -1
  104. package/dist/7339.cocolight-api-client.browser.js +1 -1
  105. package/dist/7339.cocolight-api-client.cjs +1 -1
  106. package/dist/7398.cocolight-api-client.browser.js +1 -1
  107. package/dist/7398.cocolight-api-client.cjs +1 -1
  108. package/dist/7888.cocolight-api-client.browser.js +1 -1
  109. package/dist/7888.cocolight-api-client.cjs +1 -1
  110. package/dist/7975.cocolight-api-client.browser.js +1 -1
  111. package/dist/7975.cocolight-api-client.cjs +1 -1
  112. package/dist/809.cocolight-api-client.browser.js +1 -1
  113. package/dist/809.cocolight-api-client.cjs +1 -1
  114. package/dist/8300.cocolight-api-client.browser.js +1 -1
  115. package/dist/8300.cocolight-api-client.cjs +1 -1
  116. package/dist/8317.cocolight-api-client.browser.js +1 -1
  117. package/dist/8317.cocolight-api-client.cjs +1 -1
  118. package/dist/8525.cocolight-api-client.browser.js +1 -1
  119. package/dist/8525.cocolight-api-client.cjs +1 -1
  120. package/dist/8894.cocolight-api-client.browser.js +1 -1
  121. package/dist/8894.cocolight-api-client.cjs +1 -1
  122. package/dist/9301.cocolight-api-client.browser.js +1 -1
  123. package/dist/9301.cocolight-api-client.cjs +1 -1
  124. package/dist/9438.cocolight-api-client.browser.js +1 -1
  125. package/dist/9438.cocolight-api-client.cjs +1 -1
  126. package/dist/9478.cocolight-api-client.browser.js +1 -1
  127. package/dist/9478.cocolight-api-client.cjs +1 -1
  128. package/dist/9568.cocolight-api-client.browser.js +1 -1
  129. package/dist/9568.cocolight-api-client.cjs +1 -1
  130. package/dist/cocolight-api-client.browser.js +6 -6
  131. package/dist/cocolight-api-client.cjs +1 -1
  132. package/dist/cocolight-api-client.vite.mjs.js +1 -1
  133. package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
  134. package/package.json +7 -17
  135. package/types/Api.d.ts +1 -0
  136. package/types/ApiClient.d.ts +10 -1
  137. package/types/api/BaseEntity.d.ts +2 -0
  138. package/types/utils/OfflineClientManager.d.ts +8 -0
  139. package/dist/1035.cocolight-api-client.mjs.js +0 -1
  140. package/dist/1483.cocolight-api-client.mjs.js +0 -1
  141. package/dist/1491.cocolight-api-client.mjs.js +0 -1
  142. package/dist/1494.cocolight-api-client.mjs.js +0 -1
  143. package/dist/1641.cocolight-api-client.mjs.js +0 -1
  144. package/dist/1678.cocolight-api-client.mjs.js +0 -1
  145. package/dist/2041.cocolight-api-client.mjs.js +0 -1
  146. package/dist/2302.cocolight-api-client.mjs.js +0 -1
  147. package/dist/2313.cocolight-api-client.mjs.js +0 -1
  148. package/dist/2379.cocolight-api-client.mjs.js +0 -1
  149. package/dist/2439.cocolight-api-client.mjs.js +0 -1
  150. package/dist/2594.cocolight-api-client.mjs.js +0 -1
  151. package/dist/2766.cocolight-api-client.mjs.js +0 -1
  152. package/dist/2812.cocolight-api-client.mjs.js +0 -1
  153. package/dist/2918.cocolight-api-client.mjs.js +0 -1
  154. package/dist/3028.cocolight-api-client.mjs.js +0 -1
  155. package/dist/3330.cocolight-api-client.mjs.js +0 -1
  156. package/dist/3371.cocolight-api-client.mjs.js +0 -1
  157. package/dist/3401.cocolight-api-client.mjs.js +0 -1
  158. package/dist/3516.cocolight-api-client.mjs.js +0 -1
  159. package/dist/3593.cocolight-api-client.mjs.js +0 -1
  160. package/dist/3764.cocolight-api-client.mjs.js +0 -1
  161. package/dist/3873.cocolight-api-client.mjs.js +0 -1
  162. package/dist/3911.cocolight-api-client.mjs.js +0 -1
  163. package/dist/4031.cocolight-api-client.mjs.js +0 -1
  164. package/dist/4152.cocolight-api-client.mjs.js +0 -1
  165. package/dist/4203.cocolight-api-client.mjs.js +0 -1
  166. package/dist/424.cocolight-api-client.mjs.js +0 -1
  167. package/dist/435.cocolight-api-client.mjs.js +0 -1
  168. package/dist/4402.cocolight-api-client.mjs.js +0 -1
  169. package/dist/4425.cocolight-api-client.mjs.js +0 -1
  170. package/dist/4472.cocolight-api-client.mjs.js +0 -1
  171. package/dist/4914.cocolight-api-client.mjs.js +0 -1
  172. package/dist/4959.cocolight-api-client.mjs.js +0 -1
  173. package/dist/5100.cocolight-api-client.mjs.js +0 -1
  174. package/dist/5111.cocolight-api-client.mjs.js +0 -1
  175. package/dist/5286.cocolight-api-client.mjs.js +0 -1
  176. package/dist/5380.cocolight-api-client.mjs.js +0 -1
  177. package/dist/5383.cocolight-api-client.mjs.js +0 -1
  178. package/dist/5480.cocolight-api-client.mjs.js +0 -1
  179. package/dist/5541.cocolight-api-client.mjs.js +0 -1
  180. package/dist/5839.cocolight-api-client.mjs.js +0 -1
  181. package/dist/5862.cocolight-api-client.mjs.js +0 -1
  182. package/dist/5869.cocolight-api-client.mjs.js +0 -1
  183. package/dist/588.cocolight-api-client.mjs.js +0 -1
  184. package/dist/601.cocolight-api-client.mjs.js +0 -1
  185. package/dist/6012.cocolight-api-client.mjs.js +0 -1
  186. package/dist/6200.cocolight-api-client.mjs.js +0 -1
  187. package/dist/6369.cocolight-api-client.mjs.js +0 -1
  188. package/dist/7009.cocolight-api-client.mjs.js +0 -1
  189. package/dist/725.cocolight-api-client.mjs.js +0 -1
  190. package/dist/7339.cocolight-api-client.mjs.js +0 -1
  191. package/dist/7398.cocolight-api-client.mjs.js +0 -1
  192. package/dist/7888.cocolight-api-client.mjs.js +0 -1
  193. package/dist/7975.cocolight-api-client.mjs.js +0 -1
  194. package/dist/809.cocolight-api-client.mjs.js +0 -1
  195. package/dist/8300.cocolight-api-client.mjs.js +0 -1
  196. package/dist/8317.cocolight-api-client.mjs.js +0 -1
  197. package/dist/8525.cocolight-api-client.mjs.js +0 -1
  198. package/dist/8894.cocolight-api-client.mjs.js +0 -1
  199. package/dist/9301.cocolight-api-client.mjs.js +0 -1
  200. package/dist/9438.cocolight-api-client.mjs.js +0 -1
  201. package/dist/9478.cocolight-api-client.mjs.js +0 -1
  202. package/dist/9568.cocolight-api-client.mjs.js +0 -1
  203. package/dist/cocolight-api-client.mjs.js +0 -1
  204. package/src/Api.ts +0 -348
  205. package/src/ApiClient.ts +0 -1994
  206. package/src/EJSONType.ts +0 -103
  207. package/src/api/Action.ts +0 -665
  208. package/src/api/Answer.ts +0 -1392
  209. package/src/api/Badge.ts +0 -143
  210. package/src/api/BaseEntity.ts +0 -6384
  211. package/src/api/Classified.ts +0 -175
  212. package/src/api/Comment.ts +0 -280
  213. package/src/api/EndpointApi.ts +0 -2270
  214. package/src/api/EndpointApi.types.ts +0 -6566
  215. package/src/api/EntityRegistry.ts +0 -324
  216. package/src/api/Event.ts +0 -360
  217. package/src/api/Form.ts +0 -282
  218. package/src/api/News.ts +0 -547
  219. package/src/api/Notifications.ts +0 -196
  220. package/src/api/Organization.ts +0 -771
  221. package/src/api/Poi.ts +0 -242
  222. package/src/api/Project.ts +0 -545
  223. package/src/api/User.ts +0 -1889
  224. package/src/api/UserApi.ts +0 -192
  225. package/src/api/serverDataType/Action.ts +0 -177
  226. package/src/api/serverDataType/Answer.ts +0 -106
  227. package/src/api/serverDataType/Classified.ts +0 -152
  228. package/src/api/serverDataType/Comment.ts +0 -88
  229. package/src/api/serverDataType/Country.ts +0 -1
  230. package/src/api/serverDataType/Event.ts +0 -112
  231. package/src/api/serverDataType/Form.ts +0 -157
  232. package/src/api/serverDataType/News.ts +0 -138
  233. package/src/api/serverDataType/Notification.ts +0 -81
  234. package/src/api/serverDataType/Organization.ts +0 -111
  235. package/src/api/serverDataType/Poi.ts +0 -75
  236. package/src/api/serverDataType/Project.ts +0 -115
  237. package/src/api/serverDataType/User.ts +0 -112
  238. package/src/api/serverDataType/Zone.ts +0 -33
  239. package/src/api/serverDataType/common.ts +0 -84
  240. package/src/costum/normalize.ts +0 -7
  241. package/src/costum/registry.ts +0 -138
  242. package/src/costum/runtime-map.ts +0 -221
  243. package/src/costum/runtime.ts +0 -274
  244. package/src/costum/schemas/CercleTravailleursSociauxLiberauxLaPlumeSociale.ts +0 -19
  245. package/src/costum/schemas/FabEn.ts +0 -19
  246. package/src/costum/schemas/LivingLabInnovationSocialeBraspanon.ts +0 -19
  247. package/src/costum/schemas/alfph.ts +0 -27
  248. package/src/costum/schemas/campagne.ts +0 -19
  249. package/src/costum/schemas/chtitube.ts +0 -19
  250. package/src/costum/schemas/circuitCourt.ts +0 -19
  251. package/src/costum/schemas/cmcasHauteBretagne.ts +0 -19
  252. package/src/costum/schemas/coFinancementTierslieux.ts +0 -27
  253. package/src/costum/schemas/coeurNumerique.ts +0 -19
  254. package/src/costum/schemas/communLundi.ts +0 -19
  255. package/src/costum/schemas/cressReunion.ts +0 -35
  256. package/src/costum/schemas/cressReunionNetwork.ts +0 -35
  257. package/src/costum/schemas/ctenat.ts +0 -43
  258. package/src/costum/schemas/cyberReunion.ts +0 -19
  259. package/src/costum/schemas/ekisphere.ts +0 -35
  260. package/src/costum/schemas/equipementsSportifs974.ts +0 -19
  261. package/src/costum/schemas/federationDesCae.ts +0 -19
  262. package/src/costum/schemas/formationGenerique.ts +0 -35
  263. package/src/costum/schemas/franceTierslieux.ts +0 -19
  264. package/src/costum/schemas/hinaura.ts +0 -27
  265. package/src/costum/schemas/hubmayotte.ts +0 -19
  266. package/src/costum/schemas/hubnouvellecaledonie.ts +0 -19
  267. package/src/costum/schemas/hubpolynesiefrancaise.ts +0 -19
  268. package/src/costum/schemas/hubreunion.ts +0 -19
  269. package/src/costum/schemas/hubterritoires.ts +0 -19
  270. package/src/costum/schemas/hubwallisetfutuna.ts +0 -19
  271. package/src/costum/schemas/hva.ts +0 -27
  272. package/src/costum/schemas/ibv2.ts +0 -19
  273. package/src/costum/schemas/institutBleu.ts +0 -19
  274. package/src/costum/schemas/journalInsoumisChambery.ts +0 -27
  275. package/src/costum/schemas/kpacite.ts +0 -19
  276. package/src/costum/schemas/laCompagnieDesTierslieux.ts +0 -43
  277. package/src/costum/schemas/laMaisonDesFaiseurs.ts +0 -19
  278. package/src/costum/schemas/laReunionPourUnNumeriqueInclusif.ts +0 -19
  279. package/src/costum/schemas/laSemaineDesTierslieux.ts +0 -19
  280. package/src/costum/schemas/labEnCommuns.ts +0 -19
  281. package/src/costum/schemas/lakou.ts +0 -27
  282. package/src/costum/schemas/lesCommunsDesTierslieux.ts +0 -19
  283. package/src/costum/schemas/mayottePourUnNumeriqueInclusif.ts +0 -19
  284. package/src/costum/schemas/meir.ts +0 -27
  285. package/src/costum/schemas/navigatorDesTierslieux.ts +0 -19
  286. package/src/costum/schemas/notragora.ts +0 -27
  287. package/src/costum/schemas/nouvellecaledoniePourUnNumeriqueInclusif.ts +0 -19
  288. package/src/costum/schemas/openAtlas.ts +0 -19
  289. package/src/costum/schemas/polynesieFrancaisePourUnNumeriqueInclusif.ts +0 -19
  290. package/src/costum/schemas/prdr.ts +0 -19
  291. package/src/costum/schemas/reseauTierslieux.ts +0 -19
  292. package/src/costum/schemas/rfflabs.ts +0 -19
  293. package/src/costum/schemas/ries.ts +0 -19
  294. package/src/costum/schemas/siteDuPactePourLaTransition.ts +0 -27
  295. package/src/costum/schemas/sportSanteBienetre.ts +0 -43
  296. package/src/costum/schemas/suppa.ts +0 -19
  297. package/src/costum/schemas/terramies.ts +0 -19
  298. package/src/costum/schemas/testeTplFormation.ts +0 -35
  299. package/src/costum/schemas/tierslieuxbelgique.ts +0 -19
  300. package/src/costum/schemas/tierslieuxorg1.ts +0 -35
  301. package/src/costum/schemas/transiter.ts +0 -35
  302. package/src/costum/schemas/vhelioPeiTour2025.ts +0 -19
  303. package/src/costum/schemas/wallisetfutunaPourUnNumeriqueInclusif.ts +0 -19
  304. package/src/costum/symbol.ts +0 -26
  305. package/src/costum/types.ts +0 -895
  306. package/src/endpoints.module.ts +0 -4253
  307. package/src/error.ts +0 -86
  308. package/src/forms/entityForm.ts +0 -142
  309. package/src/index.ts +0 -161
  310. package/src/mixin/UserMixin.ts +0 -4
  311. package/src/types/api-responses.ts +0 -217
  312. package/src/types/entities.ts +0 -58
  313. package/src/types/error-guards.ts +0 -230
  314. package/src/types/index.ts +0 -41
  315. package/src/types/payloads.ts +0 -21
  316. package/src/types/transforms.ts +0 -119
  317. package/src/utils/FileOfflineStorageStrategy.node.ts +0 -50
  318. package/src/utils/FileStorageStrategy.node.ts +0 -63
  319. package/src/utils/MultiServerFileStorageStrategy.node.ts +0 -67
  320. package/src/utils/MultiServerTokenStorageStrategy.ts +0 -139
  321. package/src/utils/OfflineClientManager.ts +0 -216
  322. package/src/utils/OfflineQueueStorageStrategy.ts +0 -47
  323. package/src/utils/TokenStorage.ts +0 -77
  324. package/src/utils/compat.ts +0 -12
  325. package/src/utils/createDefaultMultiServerTokenStorageStrategy.ts +0 -35
  326. package/src/utils/createDefaultOfflineStrategy.ts +0 -34
  327. package/src/utils/createDefaultTokenStorageStrategy.ts +0 -33
  328. package/src/utils/reactive.ts +0 -243
  329. package/src/utils/stream-utils.node.ts +0 -12
package/src/api/Answer.ts DELETED
@@ -1,1392 +0,0 @@
1
- import { BaseEntity } from "./BaseEntity.js";
2
- import { ApiError } from "../error.js";
3
-
4
- import type { CoformAnswersByIdData, CoformGetAnswerFilesData, CoformUploadAnswerFileData, DeleteElementData, GetCoformAnswerHistoryData, GetCoformMultievalDataData, SaveCoformAnswerData } from "./EndpointApi.types.js";
5
- import type { AnswerItemNormalized } from "./serverDataType/Answer.js";
6
- import type { FormItemNormalized } from "./serverDataType/Form.js";
7
-
8
- type UploadInput = File | Blob | Buffer | import("stream").Readable;
9
-
10
- /**
11
- * Format des données utilisateur d'un CoForm — `{ subFormId: { inputId: value } }`.
12
- * Générique : la lib ne fait aucune supposition sur le type des valeurs.
13
- */
14
- export type AllStepsData = Record<string, Record<string, unknown>>;
15
-
16
- /**
17
- * Une entrée de l'historique d'audit d'une réponse (réponse de
18
- * `GET_COFORM_ANSWER_HISTORY`). `userName`/`userSlug` sont dénormalisés
19
- * au moment de la modification : survivent à une suppression de l'user.
20
- */
21
- export interface AnswerChange {
22
- userId: string;
23
- userName: string;
24
- userSlug: string;
25
- /** Timestamp unix en SECONDES (pas ms) — `new Date(at * 1000)`. */
26
- at: number;
27
- /** `""` possible : cast PHP `(string)` avec défaut vide si champ absent. */
28
- mutationType: "create" | "update" | "";
29
- changedFields: string[];
30
- }
31
-
32
- /** Un axe du radar multi-eval (= un input radioNew d'une step donnée). */
33
- export interface MultiEvalAxis {
34
- key: string;
35
- label: string;
36
- options: string[];
37
- }
38
-
39
- /** Un dataset = la contribution d'un user à un step (1 dataset = 1 user). */
40
- export interface MultiEvalDataset {
41
- userId: string;
42
- userName: string;
43
- userSlug: string;
44
- evaluatedAt: number | null;
45
- values: Record<string, number>;
46
- }
47
-
48
- /** Une step avec ses axes et ses contributeurs (élément de `getMultiEvalData`). */
49
- export interface MultiEvalStepData {
50
- stepKey: string;
51
- stepName: string;
52
- axes: MultiEvalAxis[];
53
- datasets: MultiEvalDataset[];
54
- }
55
-
56
- /** Réponse de `Answer.getMultiEvalData()`. */
57
- export interface MultiEvalDataResponse {
58
- steps: MultiEvalStepData[];
59
- }
60
-
61
- /**
62
- * Valeur d'un upload en attente — objet `{ name?, data: "data:...;base64,..." }`
63
- * inséré par les inputs uploader avant le pré-upload backend.
64
- */
65
- export interface PendingUploadValue {
66
- name?: string;
67
- data: string;
68
- }
69
-
70
- /**
71
- * Un pending upload détecté dans la structure `allData` — chemin imbriqué +
72
- * type d'input dérivé du schéma (`formData.inputs[subFormId].inputs[inputId].type`).
73
- */
74
- export interface PendingUpload {
75
- value: PendingUploadValue;
76
- path: (string | number)[];
77
- inputType?: string;
78
- }
79
-
80
- /**
81
- * Options de `Answer.processUploads()`.
82
- */
83
- export interface ProcessUploadsOptions {
84
- /**
85
- * Schéma du Form (pour déduire `inputType` des champs uploader/simpleTable).
86
- * Auto-récupéré via `this.parent.serverData` si parent est un Form.
87
- */
88
- formData?: FormItemNormalized;
89
- /** Nombre d'uploads parallèles par batch. Défaut : 4. */
90
- batchSize?: number;
91
- }
92
-
93
- /**
94
- * Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
95
- */
96
- export interface AnswerGetOptions {
97
- /**
98
- * Liste de champs à retourner côté backend (filtrage). Si absent, tous les
99
- * champs sont renvoyés. Utile pour optimiser la taille de la réponse.
100
- */
101
- fields?: string[];
102
- /**
103
- * ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
104
- * `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
105
- * via `this.parent.id`** quand le parent est un `Form` (cas typique
106
- * `form.answer({id})`).
107
- */
108
- formId?: string;
109
- /** Chemin Finder (rarement utilisé). */
110
- finderPath?: string;
111
- }
112
-
113
- /**
114
- * Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
115
- * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
116
- */
117
- export interface UploadAnswerFileOptions {
118
- /** Type de document : `image` (défaut) ou `file` (PDF, DOCX, etc.). */
119
- docType?: "image" | "file";
120
- /** Clé de contenu côté backend. `"slider"` (défaut) pour images, `"presentation"` pour inputs uploader. */
121
- contentKey?: string;
122
- /** Sous-clé optionnelle (ex: `"subFormId.inputId"` pour les inputs `*.uploader`). */
123
- subKey?: string;
124
- /** UUID client (compat fine-uploader). Auto-généré si absent. */
125
- qquuid?: string;
126
- /** Nom du fichier. Auto-déduit (File.name, stream.path, ou fallback) si absent. */
127
- qqfilename?: string;
128
- /**
129
- * Taille du fichier en octets. Auto-déduite pour `File` (`.size`), `Blob`
130
- * (`.size`), `Buffer` (`.length`), ou `Readable` annoté (`.size`).
131
- *
132
- * **À fournir explicitement pour un `Readable` non annoté** (ex:
133
- * `fs.createReadStream(path)` → faire `fs.statSync(path).size` au préalable),
134
- * sinon `0` est envoyé et le backend peut rejeter le multipart.
135
- */
136
- qqtotalfilesize?: number;
137
- }
138
-
139
- /**
140
- * Retour normalisé de `Answer.uploadFile()`.
141
- */
142
- export interface UploadAnswerFileResult {
143
- /** ID du document MongoDB (à stocker dans `{docId, docPath}` pour les inputs uploader). */
144
- docId: string;
145
- /** Chemin du document. URL absolue côté backend, à nettoyer en chemin relatif si besoin (cf. `cleanUploaderUrls` site-json). */
146
- docPath: string;
147
- /** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
148
- answerId: string;
149
- }
150
-
151
- /**
152
- * Options de `Answer.getFiles()`.
153
- */
154
- export interface GetFilesOptions {
155
- /** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
156
- subKey: string;
157
- /** Type de document attendu. Défaut backend : `"file"`. */
158
- docType?: "image" | "file";
159
- /** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
160
- contentKey?: string;
161
- }
162
-
163
- /**
164
- * Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
165
- * Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
166
- * que pour `docType: "image"`.
167
- */
168
- export interface AnswerFileItem {
169
- /** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
170
- docId: string;
171
- /** Chemin relatif du document côté backend. */
172
- docPath: string;
173
- /** Nom original du fichier. */
174
- name?: string;
175
- /** Taille en octets. */
176
- size?: number;
177
- /** Chemin de l'image (uniquement si `docType: "image"`). */
178
- imagePath?: string | null;
179
- /** Chemin du thumbnail (uniquement si `docType: "image"`). */
180
- imageThumbPath?: string | null;
181
- /** Chemin de la version medium (uniquement si `docType: "image"`). */
182
- imageMediumPath?: string | null;
183
- }
184
-
185
- export class Answer extends BaseEntity<AnswerItemNormalized> {
186
- static override entityType = "answers";
187
-
188
- static override entityTag = "Answer";
189
- static override SCHEMA_CONSTANTS: string[] = [
190
- "SAVE_COFORM_ANSWER"
191
- ];
192
-
193
- static ADD_BLOCKS = new Map([
194
- ["SAVE_COFORM_ANSWER", "saveCoformAnswer"]
195
- ] as const);
196
-
197
- static UPDATE_BLOCKS = new Map([
198
- ["SAVE_COFORM_ANSWER", "saveCoformAnswer"]
199
- ] as const);
200
-
201
- override defaultFields: Record<string, any> = {
202
- };
203
-
204
- /**
205
- * Champs `serverData` à ne pas renvoyer au serveur dans le payload de save.
206
- * `answers`, `addedOptions`, `links`, `form` sont conservés (envoyés au serveur).
207
- * Les autres sont calculés/posés par le backend.
208
- */
209
- override removeFields: string[] = [
210
- "_id", "collection", "user", "created", "updated",
211
- "modified", "draft", "finished", "context",
212
- "voteCount", "vote", "project"
213
- ];
214
-
215
- /**
216
- * {@inheritDoc BaseEntity#_isAdminViaHierarchy}
217
- *
218
- * Override Answer : la hiérarchie d'Answer passe par son **parent instance
219
- * Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
220
- *
221
- * Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
222
- *
223
- * Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
224
- * renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
225
- *
226
- * @protected
227
- */
228
- protected override _isAdminViaHierarchy(): boolean {
229
- const form = this.parent;
230
- if (!form || typeof (form as any).getEntityType !== "function"
231
- || (form as any).getEntityType() !== "forms") {
232
- return false;
233
- }
234
-
235
- const formParents = (form as { serverData?: { parent?: Record<string, unknown> } }).serverData?.parent;
236
- if (!formParents || typeof formParents !== "object") {
237
- return false;
238
- }
239
-
240
- for (const [parentId, parentRef] of Object.entries(formParents)) {
241
- if (!parentRef || typeof parentRef !== "object") continue;
242
- const ref = parentRef as { type?: string };
243
- if (!ref.type) continue;
244
- if (this._isAdminOfParent(parentId, ref.type)) {
245
- return true;
246
- }
247
- }
248
- return false;
249
- }
250
-
251
- /**
252
- * {@inheritDoc BaseEntity#isAuthor}
253
- *
254
- * Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
255
- * comme la version par défaut de BaseEntity). Le `user` peut être :
256
- * - un `string` (ID MongoDB brut renvoyé par le backend)
257
- * - une instance `User` (après `_transformServerData` qui le link en entité)
258
- *
259
- * Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
260
- * la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
261
- */
262
- override isAuthor(options?: { silent?: boolean }): boolean {
263
- const silent = options?.silent ?? true;
264
- try {
265
- // Note : on utilise `isConnected` (user logged in) au lieu de `isMe`
266
- // (l'entité IS le user logged in) qui est sémantiquement faux sur Answer.
267
- // `_checkAccess` du parent est private et utilise isMe — non réutilisable ici.
268
- if (!this.isConnected) throw new ApiError("Vous devez être connecté pour vérifier l'auteur.", 401);
269
- if (!this.id) throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
270
- if (!this.serverData) throw new ApiError("Aucune donnée serveur disponible.", 404);
271
- if (!this.userId) throw new ApiError("Utilisateur non connecté.", 401);
272
- } catch (e) {
273
- if (silent) return false;
274
- throw e;
275
- }
276
-
277
- const user = this._serverData.user;
278
- if (!user) return false;
279
-
280
- const authorId = typeof user === "string" ? user : (user as { id?: string }).id;
281
- return Boolean(
282
- this.userId
283
- && typeof authorId === "string"
284
- && authorId === this.userId
285
- );
286
- }
287
-
288
- /**
289
- * Transforme les champs imbriqués (user, context etc.) en instances d'entités.
290
- * @param data - Les données brutes du serveur.
291
- * @returns Les données transformées.
292
- * @protected
293
- */
294
- protected override _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized {
295
-
296
- // Transformer user en instance User
297
- if(data.user && typeof data.user === "object" && "id" in data.user) {
298
- data.user = this._linkNestedEntity({ type: "citoyens", collection: "citoyens", ...data.user });
299
- }
300
-
301
- // Normalisation legacy uploader : si parent Form connu, convertit les valeurs
302
- // d'inputs `*.uploader` du format Array `[{docId, docPath}]` vers le format
303
- // canonique `{updateDate, files: {docId: docPath}}`. Symétrique de ce que
304
- // `processUploads()` écrit. Si parent pas Form → skip (pas de schéma pour
305
- // identifier les inputs uploader).
306
- const formData = this._resolveFormData();
307
- if (formData && data.answers && typeof data.answers === "object") {
308
- data.answers = this._normalizeLegacyUploaderInAnswers(
309
- data.answers as Record<string, Record<string, unknown>>,
310
- formData,
311
- );
312
- }
313
-
314
- return data;
315
- }
316
-
317
- /**
318
- * Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
319
- *
320
- * **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
321
- * `form.answer({id})`), le param `formId` est automatiquement envoyé pour
322
- * que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
323
- * Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
324
- *
325
- * @param opts - Params optionnels :
326
- * - `fields` : filtrage des champs côté backend
327
- * - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
328
- * - `finderPath` : chemin Finder (rare)
329
- *
330
- * @example
331
- * // Cas commun : form.answer({id}) → get() auto avec formId
332
- * const answer = await form.answer({ id: answerId });
333
- * answer.serverData.canEdit; // true/false calculé backend
334
- * answer.serverData.editDeniedReason; // raison si false
335
- *
336
- * @example
337
- * // Cas avancé : filtrage de champs pour optim perf
338
- * const answer = await form.answer();
339
- * answer._id(answerId);
340
- * await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
341
- */
342
- override async get(opts: AnswerGetOptions = {}): Promise<Record<string, any>> {
343
- if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.", 400);
344
- const answerId = this.id; // Type narrowing
345
-
346
- // Auto-injection du formId via parent Form (sauf si caller a explicitement
347
- // passé opts.formId — même chaîne vide pour disable).
348
- const formId = "formId" in opts ? opts.formId : this._autoFormIdFromParent();
349
-
350
- const payload: CoformAnswersByIdData = { answerId };
351
- if (opts.fields && opts.fields.length > 0) payload.fields = opts.fields;
352
- if (typeof formId === "string" && formId.length > 0) payload.formId = formId;
353
- if (opts.finderPath) payload.finderPath = opts.finderPath;
354
-
355
- const answer = await this.endpointApi.coformAnswersById(payload);
356
-
357
- if (answer?.data?.id) {
358
- this._setData(answer.data as AnswerItemNormalized, { forceInitialDraftReset: true });
359
- return this.serverData;
360
- }
361
- throw new ApiError(`Aucune réponse trouvée pour l'ID ${this.id}`, 404);
362
- }
363
-
364
- /**
365
- * Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
366
- * @private
367
- */
368
- private _autoFormIdFromParent(): string | undefined {
369
- const parent = this.parent;
370
- if (parent && typeof (parent as any).getEntityType === "function"
371
- && (parent as any).getEntityType() === "forms"
372
- && typeof (parent as any).id === "string") {
373
- return (parent as any).id as string;
374
- }
375
- return undefined;
376
- }
377
-
378
- override async form(): Promise<never> {
379
- throw new ApiError(`form n'existe pas dans ${this.constructor.name}`, 501);
380
- }
381
-
382
- /**
383
- * Supprime cette Answer côté serveur via `DELETE_ELEMENT`
384
- * (`type=answers`, `id=this.id`).
385
- *
386
- * Après succès :
387
- * - `serverData` et `draftData` sont vidés (sans casser la réactivité)
388
- * - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
389
- *
390
- * **Guard** : autorisation côté client si l'utilisateur courant est :
391
- * - l'**auteur** de l'Answer (`serverData.user === userId`), OU
392
- * - **admin** d'un parent du Form (org/project) via la hiérarchie
393
- * (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
394
- *
395
- * Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
396
- * l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
397
- *
398
- * **Pré-requis** :
399
- * - `this.parent` doit être un Form (cas `form.answer({id})`)
400
- * - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
401
- * Sans ces pré-requis, seul `isAuthor` peut autoriser.
402
- *
403
- * @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
404
- * @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
405
- * @throws {ApiError} 403 si ni auteur ni admin du parent.
406
- *
407
- * @example
408
- * const answer = await form.answer({ id: "..." });
409
- * await answer.delete(); // OK si auteur OU admin org/project parent du Form
410
- * answer._isDeleted; // true
411
- */
412
- async delete(reason: string = "delete coform answer"): Promise<void> {
413
- if (!this.id) {
414
- throw new ApiError("Vous devez fournir un id pour supprimer une Answer.", 400);
415
- }
416
-
417
- // Guard : auteur OU admin du parent Form (org/project).
418
- if (!this.isAuthorOrAdmin({ checkHierarchy: true })) {
419
- throw new ApiError(
420
- "Suppression refusée — vous devez être l'auteur de l'Answer "
421
- + "ou administrateur du parent (organisation/projet) du Form.",
422
- 403
423
- );
424
- }
425
-
426
- const data: DeleteElementData = {
427
- reason,
428
- pathParams: { type: "answers", id: this.id },
429
- };
430
-
431
- await this.callIsConnected(() => this.endpointApi.deleteElement(data));
432
-
433
- // Vider les objets réactifs sans casser la réactivité (cf. Comment.delete)
434
- Object.keys(this._draftData).forEach(key => delete this._draftData[key]);
435
- Object.keys(this._serverData).forEach(key => delete (this._serverData as any)[key]);
436
-
437
- this._isDeleted = true;
438
- }
439
-
440
- /**
441
- * {@inheritDoc BaseEntity#deleteFile}
442
- *
443
- * Override avec **cleanup local** : après suppression backend, retire le
444
- * `docId` des structures uploader normalisées dans `serverData.answers` et
445
- * `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
446
- * produit par `processUploads()` et `_transformServerData`).
447
- *
448
- * Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
449
- * reflète directement l'état post-suppression.
450
- *
451
- * @example
452
- * const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
453
- * await answer.deleteFile(files[0].docId);
454
- * // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
455
- */
456
- override async deleteFile(docId: string): Promise<void> {
457
- await super.deleteFile(docId);
458
-
459
- // Cleanup local : retire le docId des structures uploader normalisées
460
- this._removeDocIdFromUploaderFields(this._serverData.answers, docId);
461
- this._removeDocIdFromUploaderFields(this._draftData.answers as Record<string, unknown> | undefined, docId);
462
- }
463
-
464
- /**
465
- * Walk `answers[subFormId][inputId]` et retire `docId` des structures
466
- * `{updateDate, files: {docId: docPath}}` (format canonique uploader).
467
- * Mutation in-place pour préserver la réactivité.
468
- * @private
469
- */
470
- private _removeDocIdFromUploaderFields(
471
- answers: Record<string, unknown> | undefined,
472
- docId: string,
473
- ): void {
474
- if (!answers || typeof answers !== "object") return;
475
-
476
- for (const subFormData of Object.values(answers)) {
477
- if (!this._isObjectRecord(subFormData)) continue;
478
- for (const inputValue of Object.values(subFormData)) {
479
- if (!this._isObjectRecord(inputValue)) continue;
480
- const files = (inputValue as { files?: unknown }).files;
481
- if (this._isObjectRecord(files) && docId in files) {
482
- delete (files as Record<string, unknown>)[docId];
483
- }
484
- }
485
- }
486
- }
487
-
488
- /**
489
- * Récupère la liste des fichiers attachés à un input précis de cette Answer
490
- * via `COFORM_GET_ANSWER_FILES`.
491
- *
492
- * **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
493
- * en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
494
- * recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
495
- *
496
- * Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
497
- * les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
498
- * — pas besoin d'appel réseau.
499
- *
500
- * @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
501
- * @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
502
- * Tableau vide si aucun fichier trouvé.
503
- * @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
504
- *
505
- * @example
506
- * const form = await org.form({ id: formId });
507
- * const answer = await form.answer({ id: answerId });
508
- * const files = await answer.getFiles({
509
- * subKey: `${subFormId}.${inputId}`,
510
- * docType: "image",
511
- * });
512
- * files[0].docId; // ID du document (pour suppression future)
513
- * files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
514
- */
515
- async getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]> {
516
- if (!this.id) {
517
- throw new ApiError("Answer sans id, impossible de lister ses fichiers.", 400);
518
- }
519
- if (!opts.subKey || opts.subKey.length === 0) {
520
- throw new ApiError("`subKey` requis pour lister les fichiers d'un input.", 400);
521
- }
522
-
523
- const payload: CoformGetAnswerFilesData = {
524
- answerId: this.id,
525
- subKey: opts.subKey,
526
- };
527
- if (opts.docType) payload.docType = opts.docType;
528
- if (opts.contentKey) payload.contentKey = opts.contentKey;
529
-
530
- // Forme observée du backend (cf. site-json useCoFormAnswerFiles.tsx
531
- // et schema COFORM_GET_ANSWER_FILES) :
532
- // { result, answerId, subKey, contentKey, count, files: [{ id, name, size,
533
- // docPath, imagePath, imageThumbPath, imageMediumPath }] }
534
- const response = await this.callIsConnected(() =>
535
- this.endpointApi.coformGetAnswerFiles(payload)
536
- ) as {
537
- result?: boolean;
538
- msg?: string;
539
- count?: number;
540
- files?: Array<{
541
- id?: string | null;
542
- name?: string | null;
543
- size?: number | null;
544
- docPath?: string;
545
- imagePath?: string | null;
546
- imageThumbPath?: string | null;
547
- imageMediumPath?: string | null;
548
- }>;
549
- };
550
-
551
- // Filtre les entrées invalides (id + docPath requis) et normalise.
552
- return (response.files ?? [])
553
- .filter(f => typeof f.id === "string" && typeof f.docPath === "string")
554
- .map(f => ({
555
- docId: f.id!,
556
- docPath: f.docPath!,
557
- name: f.name ?? undefined,
558
- size: f.size ?? undefined,
559
- imagePath: f.imagePath ?? undefined,
560
- imageThumbPath: f.imageThumbPath ?? undefined,
561
- imageMediumPath: f.imageMediumPath ?? undefined,
562
- }));
563
- }
564
-
565
- /**
566
- * Récupère l'historique d'audit de cette Answer.
567
- *
568
- * Trace `create` + `update` triée du plus récent au plus ancien, max 200
569
- * entrées (cap backend). Auth côté serveur : owner OU canAdminAnswer
570
- * (admin du parent du form, admin/membre finder selon
571
- * `membersCanEditSharedAnswer`, ou form `publicCanEditSharedAnswer`).
572
- *
573
- * Constant : `GET_COFORM_ANSWER_HISTORY` (POST
574
- * `/survey/coform/getanswerhistory`, auth bearer).
575
- *
576
- * @returns Liste des changements ; tableau vide si l'historique est vide.
577
- * @throws {ApiError} 400 si Answer sans id.
578
- * @throws {ApiAuthenticationError} si non connecté.
579
- *
580
- * @example
581
- * const form = await org.form({ id: formId });
582
- * const answer = await form.answer({ id: answerId });
583
- * const history = await answer.getHistory();
584
- * new Date(history[0].at * 1000); // date de la dernière modif (at en SECONDES)
585
- */
586
- async getHistory(): Promise<AnswerChange[]> {
587
- if (!this.id) {
588
- throw new ApiError("Answer sans id, impossible de récupérer son historique.", 400);
589
- }
590
- const payload: GetCoformAnswerHistoryData = { answerId: this.id };
591
- // EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {history}}.
592
- const body = await this.callIsConnected(() =>
593
- this.endpointApi.getCoformAnswerHistory(payload)
594
- ) as { result?: boolean; data?: { history?: AnswerChange[] } };
595
- const list = body?.data?.history;
596
- return Array.isArray(list) ? list : [];
597
- }
598
-
599
- /**
600
- * Récupère les datasets agrégés des évaluations multiples (radar) de cette
601
- * Answer. Pour une réponse partagée contenant des inputs `radioNew` avec
602
- * `activeMultieval: true`, retourne un dataset par contributeur pour
603
- * chaque step.
604
- *
605
- * Constant : `GET_COFORM_MULTIEVAL_DATA` (POST
606
- * `/survey/coform/getmultievaldata`, auth bearer). Auth côté serveur :
607
- * owner OU canAdminAnswer.
608
- *
609
- * @param opts.stepKey - Optionnel : filtrer aux datasets d'une seule
610
- * step. Sinon retourne toutes les steps.
611
- * @throws {ApiError} 400 si Answer sans id.
612
- * @throws {ApiAuthenticationError} si non connecté.
613
- *
614
- * @example
615
- * const data = await answer.getMultiEvalData();
616
- * data.steps.forEach(s => renderRadar(s));
617
- */
618
- async getMultiEvalData(opts: { stepKey?: string | null } = {}): Promise<MultiEvalDataResponse> {
619
- if (!this.id) {
620
- throw new ApiError("Answer sans id, impossible de récupérer ses évaluations multiples.", 400);
621
- }
622
- const payload: GetCoformMultievalDataData = { answerId: this.id };
623
- if (opts.stepKey) payload.stepKey = opts.stepKey;
624
-
625
- // EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {steps}}.
626
- const body = await this.callIsConnected(() =>
627
- this.endpointApi.getCoformMultievalData(payload)
628
- ) as { result?: boolean; data?: { steps?: MultiEvalStepData[] } };
629
- const steps = body?.data?.steps;
630
- return { steps: Array.isArray(steps) ? steps : [] };
631
- }
632
-
633
- /**
634
- * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
635
- *
636
- * **Building block du pipeline** : conçu pour être utilisé via
637
- * `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
638
- * est en état "placeholder" côté backend (champ `user` non posé) — `save()`
639
- * finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
640
- * upload-only sans save échouera : le backend la considère comme orpheline
641
- * (`editDeniedReason: "not_owner"`, stub sans `data.id`).
642
- *
643
- * **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
644
- * renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
645
- * — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
646
- * en update.
647
- *
648
- * **Cas uploads suivants (Answer avec id)** : `answerId: this.id` est envoyé.
649
- *
650
- * Compatible browser (File/Blob) et Node (Buffer/Readable) via
651
- * `BaseEntity._prepareUploadFile()` — le multipart encoder du package
652
- * `form-data` (utilisé en Node) ignore les Buffer bruts, donc conversion en
653
- * stream annoté.
654
- *
655
- * @param file - Fichier à uploader (File/Blob côté browser, Buffer/Readable côté Node)
656
- * @param opts - `docType`, `contentKey`, `subKey`, `qquuid`, `qqfilename`
657
- * @returns `{ docId, docPath, answerId }` — toujours fournis par le backend.
658
- * @throws {ApiError} si `formId` introuvable, ou réponse invalide.
659
- *
660
- * @example
661
- * // Browser (depuis un <input type="file">)
662
- * const form = await org.form({ id: formId });
663
- * const answer = await form.answer(); // draft
664
- * const { docId, docPath } = await answer.uploadFile(htmlFile, {
665
- * contentKey: "presentation",
666
- * subKey: `${subFormId}.${inputId}`,
667
- * });
668
- * // answer.id désormais défini → uploads suivants utilisent cet id.
669
- *
670
- * @example
671
- * // Node (depuis un Buffer)
672
- * const buffer = await fs.promises.readFile("./photo.jpg");
673
- * const result = await answer.uploadFile(buffer, { docType: "image" });
674
- */
675
- async uploadFile(
676
- file: UploadInput,
677
- opts: UploadAnswerFileOptions = {},
678
- ): Promise<UploadAnswerFileResult> {
679
- const formId = this._resolveFormId({});
680
-
681
- const fallbackName = opts.qqfilename ?? `upload-${Date.now()}`;
682
- const { qqfile, qqfilename, qqtotalfilesize } =
683
- await this._prepareUploadFile(file, fallbackName);
684
-
685
- const payload: CoformUploadAnswerFileData = {
686
- formId,
687
- docType: opts.docType ?? "image",
688
- contentKey: opts.contentKey ?? "slider",
689
- qquuid: opts.qquuid ?? this._generateUuid(),
690
- qqfilename: opts.qqfilename ?? qqfilename,
691
- // Priorité : override caller > taille déduite (File/Blob/Buffer/Readable.size)
692
- qqtotalfilesize: opts.qqtotalfilesize ?? qqtotalfilesize,
693
- qqfile: qqfile as unknown as Record<string, unknown>,
694
- };
695
-
696
- if (opts.subKey) payload.subKey = opts.subKey;
697
- if (this.id) payload.answerId = this.id;
698
-
699
- const response = await this.callIsConnected(() =>
700
- this.endpointApi.coformUploadAnswerFile(payload)
701
- ) as {
702
- result?: boolean;
703
- success?: boolean;
704
- msg?: string;
705
- answerId?: string;
706
- docPath?: string;
707
- id?: string;
708
- };
709
-
710
- // Forme observée (cf. upload-answer-shape.test.ts) :
711
- // { result, success, answerId, docPath, id, name, size, msg, _id }
712
- if (response?.result !== true && response?.success !== true) {
713
- throw new ApiError(response?.msg || "Échec de l'upload coform.", 500);
714
- }
715
- if (!response.answerId || !response.docPath) {
716
- throw new ApiError("Réponse d'upload incomplète (answerId ou docPath manquant).", 500);
717
- }
718
-
719
- // Pose this.id si premier upload (Answer auto-créée par le backend)
720
- if (!this.id) {
721
- this._draftData.id = response.answerId;
722
- }
723
-
724
- return {
725
- docId: response.id ?? "",
726
- docPath: response.docPath,
727
- answerId: response.answerId,
728
- };
729
- }
730
-
731
- /**
732
- * Orchestre l'upload de tous les fichiers pendants (`data:URI`) trouvés dans
733
- * `allData`, puis renvoie la structure transformée prête pour `answer.save()`.
734
- *
735
- * **Pipeline complet** :
736
- * 1. Walk récursif de `allData` → collecte les `data:URI` avec leur path + inputType
737
- * 2. Premier upload (si Answer sans id) → backend crée l'Answer + pose `this.id`
738
- * 3. Uploads restants en batches parallèles (défaut 4)
739
- * 4. Remplacement dans la structure :
740
- * - inputs `*.uploader` → `{ docId, docPath }`
741
- * - autres → `docPath` string
742
- * 5. Normalisation legacy uploader : `Array<{docId, docPath}>` → `{ updateDate, files }`
743
- * 6. Nettoyage URLs absolues → chemins relatifs (uploader/simpleTable uniquement)
744
- *
745
- * Ne touche PAS `addedOptions` ni `links` — caller les pose séparément avant `save()`.
746
- *
747
- * @param allData - Données utilisateur `{ subFormId: { inputId: value } }`
748
- * @param options - `formData` (auto via parent Form), `batchSize` (défaut 4)
749
- * @returns `allData` transformé, prêt à être affecté à `answer.data.answers`
750
- *
751
- * @example
752
- * const form = await org.form({ id: formId });
753
- * const answer = await form.answer(); // ou form.answer({ id }) en édition
754
- *
755
- * const prepared = await answer.processUploads(allStepsDataFromForm);
756
- * answer.data.answers = prepared;
757
- * answer.data.addedOptions = addedOptions; // optionnel
758
- * answer.data.links = finderLinks; // optionnel
759
- * await answer.save();
760
- */
761
- async processUploads(
762
- allData: AllStepsData,
763
- options: ProcessUploadsOptions = {},
764
- ): Promise<AllStepsData> {
765
- // Résoudre formData : argument > parent Form
766
- const formData = options.formData ?? this._resolveFormData();
767
-
768
- // 1. Collecter tous les pending uploads
769
- const pending = this._collectPendingUploads(allData, [], formData);
770
-
771
- if (pending.length === 0) {
772
- // Pas de fichiers : on nettoie quand même les URLs absolues côté uploader
773
- return this._cleanUploaderUrls(allData, formData);
774
- }
775
-
776
- let prepared = allData;
777
- let startIndex = 0;
778
-
779
- // 2. Premier upload (séparé) si pas d'id — pour récupérer answerId
780
- if (!this.id) {
781
- const first = pending[0];
782
- const replaced = await this._uploadAndReplace(first, prepared, 1);
783
- prepared = replaced;
784
- startIndex = 1;
785
- }
786
-
787
- // 3. Uploads restants en batches parallèles
788
- if (startIndex < pending.length) {
789
- const remaining = pending.slice(startIndex);
790
- const batchSize = options.batchSize ?? 4;
791
-
792
- const replacements = await this._uploadInBatches(
793
- remaining,
794
- (item, idx) => this._uploadPendingItem(item, startIndex + idx + 1),
795
- batchSize,
796
- );
797
-
798
- remaining.forEach((item, idx) => {
799
- prepared = this._setValueAtPath(prepared, item.path, replacements[idx]) as AllStepsData;
800
- });
801
- }
802
-
803
- // 4. Normalisation legacy uploader (Array → {updateDate, files})
804
- const uploaderFieldPaths = new Map<string, (string | number)[]>();
805
- for (const item of pending) {
806
- if (item.inputType?.endsWith(".uploader") && item.path.length >= 2) {
807
- const key = JSON.stringify(item.path.slice(0, 2));
808
- if (!uploaderFieldPaths.has(key)) {
809
- uploaderFieldPaths.set(key, item.path.slice(0, 2));
810
- }
811
- }
812
- }
813
- for (const fieldPath of uploaderFieldPaths.values()) {
814
- const value = this._getValueAtPath(prepared, fieldPath);
815
- prepared = this._setValueAtPath(
816
- prepared,
817
- fieldPath,
818
- this._normalizeUploaderValue(value),
819
- ) as AllStepsData;
820
- }
821
-
822
- // 5. Clean URLs absolues → chemins relatifs (uploader/simpleTable)
823
- return this._cleanUploaderUrls(prepared, formData);
824
- }
825
-
826
- /**
827
- * UUID v4 compatible browser + Node.
828
- * Utilise `crypto.randomUUID()` si dispo, sinon fallback Math.random.
829
- * @private
830
- */
831
- private _generateUuid(): string {
832
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
833
- return crypto.randomUUID();
834
- }
835
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
836
- const r = Math.random() * 16 | 0;
837
- const v = c === "x" ? r : (r & 0x3 | 0x8);
838
- return v.toString(16);
839
- });
840
- }
841
-
842
- /**
843
- * Sauvegarde une nouvelle Answer (sans `id`) via `SAVE_COFORM_ANSWER`.
844
- *
845
- * Le payload caller-side attendu (dans `draftData`) :
846
- * - `answers` (requis) — objet `{ subFormId: { inputId: value } }`
847
- * - `form` ou parent Form (requis indirectement) — pour résoudre `formId`
848
- * - `addedOptions` (optionnel) — multiCheckboxPlus dynamiques
849
- * - `links` (optionnel) — sélections Finder à attacher à l'answer
850
- *
851
- * Sérialise automatiquement `answers`/`addedOptions`/`links` en JSON
852
- * (le wire-format `application/x-www-form-urlencoded` exige des strings).
853
- *
854
- * Après succès, l'ID retourné par le serveur est posé dans `_draftData.id`
855
- * → le `refresh()` de `BaseEntity.save()` peut alors récupérer l'Answer canonique.
856
- *
857
- * @protected — utiliser `answer.save()` (BaseEntity dispatch automatiquement).
858
- */
859
- override _add = async (payload: Record<string, any>): Promise<void> => {
860
- if (!this._calledFromSave) {
861
- throw new Error("utilisation invalide de _add, utilisez save");
862
- }
863
-
864
- const formId = this._resolveFormId(payload);
865
- const requestData = this._buildSavePayload(formId, payload, /* withAnswerId */ false);
866
-
867
- const response = await this.callIsConnected(() =>
868
- this.endpointApi.saveCoformAnswer(requestData)
869
- ) as { result?: boolean; msg?: string; data?: { id?: string } };
870
-
871
- // Forme observée du backend (cf. save-answer-shape.test.ts) :
872
- // { result: true, msg, data: { id: "<24hex>", _id: {_str}, collection, ... } }
873
- const newId = response?.data?.id;
874
- if (!this.id && typeof newId === "string" && newId.length === 24) {
875
- this._draftData.id = newId;
876
- }
877
- };
878
-
879
- /**
880
- * Met à jour une Answer existante via `SAVE_COFORM_ANSWER` (avec `answerId`).
881
- *
882
- * Si `payload.answers` est `undefined` (le caller a modifié uniquement
883
- * `addedOptions` ou `links`), on retombe sur `serverData.answers` pour ne
884
- * pas envoyer un payload vide qui écraserait les réponses existantes.
885
- *
886
- * @protected — utiliser `answer.save()`.
887
- */
888
- override _update = async (payload: Record<string, any>): Promise<boolean> => {
889
- if (!this._calledFromSave) {
890
- throw new Error("utilisation invalide de _update, utilisez save");
891
- }
892
- if (!this.id) {
893
- throw new ApiError("Answer sans id, impossible de mettre à jour.", 400);
894
- }
895
-
896
- const formId = this._resolveFormId(payload);
897
- const requestData = this._buildSavePayload(formId, payload, /* withAnswerId */ true);
898
-
899
- await this.callIsConnected(() => this.endpointApi.saveCoformAnswer(requestData));
900
- return true;
901
- };
902
-
903
- /**
904
- * Résout le `formId` à envoyer à `saveCoformAnswer`.
905
- *
906
- * Priorité :
907
- * 1. `payload.form` (posé via `draftData.form` par `form.answer()`)
908
- * 2. `serverData.form` (Answer chargée via `get()`)
909
- * 3. `this.parent.id` si le parent est un `Form`
910
- *
911
- * @throws {ApiError} 400 si aucune source ne fournit un formId valide.
912
- * @private
913
- */
914
- private _resolveFormId(payload: Record<string, any>): string {
915
- const fromDraftOrPayload = typeof payload.form === "string" ? payload.form : undefined;
916
- const fromServer = typeof this._serverData.form === "string" ? this._serverData.form : undefined;
917
- const parent = this.parent;
918
- const fromParent =
919
- parent && typeof (parent as any).getEntityType === "function"
920
- && (parent as any).getEntityType() === "forms"
921
- && typeof (parent as any).id === "string"
922
- ? (parent as any).id as string
923
- : undefined;
924
-
925
- const formId = fromDraftOrPayload ?? fromServer ?? fromParent;
926
- if (typeof formId !== "string" || formId.length === 0) {
927
- throw new ApiError(
928
- "formId introuvable pour sauvegarder l'Answer. "
929
- + "Créez l'Answer via `form.answer()` ou définissez `answer.data.form` manuellement.",
930
- 400
931
- );
932
- }
933
- return formId;
934
- }
935
-
936
- /**
937
- * Construit le payload `SaveCoformAnswerData` en sérialisant les champs JSON
938
- * requis par le wire-format `application/x-www-form-urlencoded`.
939
- *
940
- * Si `payload.answers` est absent en update, retombe sur `serverData.answers`
941
- * (cf. design point 6.5 — indulgent envers le caller).
942
- *
943
- * @private
944
- */
945
- private _buildSavePayload(
946
- formId: string,
947
- payload: Record<string, any>,
948
- withAnswerId: boolean
949
- ): SaveCoformAnswerData {
950
- const answersValue = payload.answers ?? (withAnswerId ? this._serverData.answers : undefined);
951
- if (answersValue === undefined || answersValue === null) {
952
- throw new ApiError(
953
- "answer.data.answers est requis (objet `{ subFormId: { inputId: value } }`).",
954
- 400
955
- );
956
- }
957
-
958
- const data: SaveCoformAnswerData = {
959
- formId,
960
- answers: JSON.stringify(answersValue),
961
- };
962
-
963
- if (withAnswerId) {
964
- data.answerId = this.id!;
965
- }
966
-
967
- if (payload.addedOptions && typeof payload.addedOptions === "object"
968
- && Object.keys(payload.addedOptions).length > 0) {
969
- data.addedOptions = JSON.stringify(payload.addedOptions);
970
- }
971
-
972
- if (payload.links && typeof payload.links === "object"
973
- && Object.keys(payload.links).length > 0) {
974
- data.links = JSON.stringify(payload.links);
975
- }
976
-
977
- return data;
978
- }
979
-
980
- // ===========================================================================
981
- // Helpers internes pour processUploads — équivalents portés depuis
982
- // site-json/coform/actions/mutations/uploadHelpers.ts (Sprint 3).
983
- // ===========================================================================
984
-
985
- /**
986
- * Récupère `formData` (schéma du Form) depuis le parent si c'est un Form.
987
- * Renvoie `undefined` si pas exploitable — `processUploads` continuera mais
988
- * sans détection automatique d'`inputType` (pas de normalisation uploader,
989
- * pas de clean URLs).
990
- * @private
991
- */
992
- private _resolveFormData(): FormItemNormalized | undefined {
993
- const parent = this.parent;
994
- if (parent && typeof (parent as any).getEntityType === "function"
995
- && (parent as any).getEntityType() === "forms") {
996
- return (parent as any).serverData as FormItemNormalized;
997
- }
998
- return undefined;
999
- }
1000
-
1001
- private _isObjectRecord(value: unknown): value is Record<string, unknown> {
1002
- return typeof value === "object" && value !== null && !Array.isArray(value);
1003
- }
1004
-
1005
- private _isDataUri(value: string): boolean {
1006
- return /^data:[^;]+;base64,/.test(value);
1007
- }
1008
-
1009
- private _isPendingUploadValue(value: unknown): value is PendingUploadValue {
1010
- if (!this._isObjectRecord(value)) return false;
1011
- return typeof value.data === "string" && this._isDataUri(value.data);
1012
- }
1013
-
1014
- private _parseMimeType(dataUri: string): string {
1015
- const match = dataUri.match(/^data:([^;]+);base64,/i);
1016
- return match?.[1]?.toLowerCase() ?? "application/octet-stream";
1017
- }
1018
-
1019
- private _inferExtensionFromMimeType(mimeType: string): string {
1020
- const map: Record<string, string> = {
1021
- "image/jpeg": "jpg",
1022
- "image/jpg": "jpg",
1023
- "image/png": "png",
1024
- "image/gif": "gif",
1025
- "image/webp": "webp",
1026
- "image/svg+xml": "svg",
1027
- "application/pdf": "pdf",
1028
- "text/plain": "txt",
1029
- "text/csv": "csv",
1030
- "application/msword": "doc",
1031
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
1032
- "application/vnd.ms-excel": "xls",
1033
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
1034
- "application/vnd.ms-powerpoint": "ppt",
1035
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
1036
- "application/zip": "zip",
1037
- "application/x-rar-compressed": "rar",
1038
- };
1039
- return map[mimeType] ?? "bin";
1040
- }
1041
-
1042
- private _sanitizeBaseName(fileName: string): string {
1043
- const raw = fileName.replace(/\.[^.]+$/, "");
1044
- const cleaned = raw.replace(/[^a-zA-Z0-9_. -]+/g, "-").trim();
1045
- return cleaned || "upload";
1046
- }
1047
-
1048
- /**
1049
- * Convertit un `PendingUploadValue` (data:URI) en `{file, docType, filename}`
1050
- * exploitable par `uploadFile()`. Compatible browser + Node :
1051
- * - Browser → `File` natif
1052
- * - Node → `Buffer` (sera converti en stream par `_prepareUploadFile()`)
1053
- * @private
1054
- */
1055
- private async _dataUriToFile(
1056
- value: PendingUploadValue,
1057
- fallbackName: string,
1058
- ): Promise<{ file: UploadInput; docType: "image" | "file"; filename: string }> {
1059
- const mime = this._parseMimeType(value.data);
1060
- const docType: "image" | "file" = mime.startsWith("image/") ? "image" : "file";
1061
-
1062
- const extension = this._inferExtensionFromMimeType(mime);
1063
- const sourceName = value.name && value.name.trim() !== "" ? value.name : fallbackName;
1064
- const hasExtension = /\.[a-zA-Z0-9]+$/.test(sourceName);
1065
- const filename = hasExtension ? sourceName : `${this._sanitizeBaseName(sourceName)}.${extension}`;
1066
-
1067
- const isBrowser = typeof window !== "undefined" && typeof File !== "undefined";
1068
-
1069
- if (isBrowser) {
1070
- const blob = await (await fetch(value.data)).blob();
1071
- return { file: new File([blob], filename, { type: mime }), docType, filename };
1072
- }
1073
-
1074
- // Node : Buffer (sera converti en stream par uploadFile → _prepareUploadFile)
1075
- const base64Match = value.data.match(/^data:[^;]+;base64,(.*)$/);
1076
- if (!base64Match) {
1077
- throw new ApiError("data:URI invalide (base64 attendu).", 400);
1078
- }
1079
- const buffer = Buffer.from(base64Match[1], "base64");
1080
- return { file: buffer, docType, filename };
1081
- }
1082
-
1083
- /**
1084
- * Parcourt récursivement `allData` pour trouver tous les `data:URI` à uploader.
1085
- * Renvoie chaque pending avec son `path` (chemin imbriqué) et `inputType`
1086
- * (déduit du schéma `formData` si fourni).
1087
- * @private
1088
- */
1089
- private _collectPendingUploads(
1090
- node: unknown,
1091
- path: (string | number)[] = [],
1092
- formData?: FormItemNormalized,
1093
- ): PendingUpload[] {
1094
- const uploads: PendingUpload[] = [];
1095
-
1096
- const getInputType = (): string | undefined => {
1097
- if (!formData || path.length < 2) return undefined;
1098
- const [subFormId, inputId] = path;
1099
- if (typeof subFormId !== "string" || typeof inputId !== "string") return undefined;
1100
- const subForm = formData.inputs?.[subFormId];
1101
- if (!subForm) return undefined;
1102
- const input = subForm.inputs?.[inputId];
1103
- return input && typeof input === "object" && "type" in input
1104
- ? (input.type as string | undefined)
1105
- : undefined;
1106
- };
1107
-
1108
- if (typeof node === "string") {
1109
- if (this._isDataUri(node)) {
1110
- uploads.push({ value: { data: node }, path, inputType: getInputType() });
1111
- }
1112
- return uploads;
1113
- }
1114
-
1115
- if (this._isPendingUploadValue(node)) {
1116
- uploads.push({ value: node, path, inputType: getInputType() });
1117
- return uploads;
1118
- }
1119
-
1120
- if (Array.isArray(node)) {
1121
- node.forEach((item, index) => {
1122
- uploads.push(...this._collectPendingUploads(item, [...path, index], formData));
1123
- });
1124
- return uploads;
1125
- }
1126
-
1127
- if (this._isObjectRecord(node)) {
1128
- Object.entries(node).forEach(([key, value]) => {
1129
- uploads.push(...this._collectPendingUploads(value, [...path, key], formData));
1130
- });
1131
- }
1132
-
1133
- return uploads;
1134
- }
1135
-
1136
- /**
1137
- * Détermine `contentKey` et `subKey` pour un upload selon le type d'input :
1138
- * - `*.uploader` → `presentation` + subKey `"subFormId.inputId"`
1139
- * - autres → `slider` sans subKey
1140
- * @private
1141
- */
1142
- private _getUploadKeys(
1143
- inputType: string | undefined,
1144
- path: (string | number)[],
1145
- ): { contentKey: string; subKey?: string } {
1146
- if (inputType && (inputType === "uploader" || inputType.endsWith(".uploader"))) {
1147
- const subKey = path
1148
- .slice(0, 2)
1149
- .filter(p => typeof p === "string")
1150
- .join(".");
1151
- return { contentKey: "presentation", subKey: subKey || undefined };
1152
- }
1153
- return { contentKey: "slider" };
1154
- }
1155
-
1156
- private _getValueAtPath(obj: unknown, path: (string | number)[]): unknown {
1157
- if (path.length === 0) return obj;
1158
- const [first, ...rest] = path;
1159
- if (Array.isArray(obj)) return this._getValueAtPath(obj[first as number], rest);
1160
- if (this._isObjectRecord(obj)) return this._getValueAtPath(obj[first as string], rest);
1161
- return undefined;
1162
- }
1163
-
1164
- private _setValueAtPath(obj: unknown, path: (string | number)[], value: unknown): unknown {
1165
- if (path.length === 0) return value;
1166
- const [first, ...rest] = path;
1167
- if (Array.isArray(obj)) {
1168
- const next = [...obj];
1169
- next[first as number] = this._setValueAtPath(next[first as number], rest, value);
1170
- return next;
1171
- }
1172
- if (this._isObjectRecord(obj)) {
1173
- return { ...obj, [first]: this._setValueAtPath(obj[first as string], rest, value) };
1174
- }
1175
- return obj;
1176
- }
1177
-
1178
- /**
1179
- * Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
1180
- * inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
1181
- *
1182
- * Utilisé par `_transformServerData()` pour harmoniser les Answer en format
1183
- * legacy Array `[{docId, docPath}]` avec le format moderne (ce que
1184
- * `processUploads()` écrit).
1185
- *
1186
- * Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
1187
- * @private
1188
- */
1189
- private _normalizeLegacyUploaderInAnswers(
1190
- answers: Record<string, Record<string, unknown>>,
1191
- formData: FormItemNormalized,
1192
- ): Record<string, Record<string, unknown>> {
1193
- const normalized: Record<string, Record<string, unknown>> = {};
1194
-
1195
- for (const [subFormId, subFormData] of Object.entries(answers)) {
1196
- if (!this._isObjectRecord(subFormData)) {
1197
- normalized[subFormId] = subFormData as any;
1198
- continue;
1199
- }
1200
-
1201
- const subForm = formData.inputs?.[subFormId];
1202
- if (!subForm) {
1203
- normalized[subFormId] = subFormData;
1204
- continue;
1205
- }
1206
-
1207
- const cleanedSubForm: Record<string, unknown> = {};
1208
- for (const [inputId, inputValue] of Object.entries(subFormData)) {
1209
- const input = subForm.inputs?.[inputId];
1210
- const inputType = input && typeof input === "object" && "type" in input
1211
- ? (input.type as string | undefined)
1212
- : undefined;
1213
-
1214
- cleanedSubForm[inputId] = inputType?.endsWith(".uploader")
1215
- ? this._normalizeUploaderValue(inputValue)
1216
- : inputValue;
1217
- }
1218
- normalized[subFormId] = cleanedSubForm;
1219
- }
1220
-
1221
- return normalized;
1222
- }
1223
-
1224
- /**
1225
- * Normalise une valeur d'input uploader vers le format legacy backend :
1226
- * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
1227
- * @private
1228
- */
1229
- private _normalizeUploaderValue(value: unknown): unknown {
1230
- const today = new Date().toLocaleDateString("fr-FR");
1231
-
1232
- const toFilesObject = (arr: unknown[]): Record<string, string> => {
1233
- const obj: Record<string, string> = {};
1234
- for (const item of arr) {
1235
- if (this._isObjectRecord(item)
1236
- && typeof item.docId === "string"
1237
- && typeof item.docPath === "string") {
1238
- obj[item.docId as string] = item.docPath as string;
1239
- } else if (typeof item === "string") {
1240
- obj[item] = item;
1241
- }
1242
- }
1243
- return obj;
1244
- };
1245
-
1246
- if (this._isObjectRecord(value) && "updateDate" in value && Array.isArray(value.updateDate)) {
1247
- if (this._isObjectRecord(value.files)) return value;
1248
- if (Array.isArray(value.files)) return { ...value, files: toFilesObject(value.files) };
1249
- return value;
1250
- }
1251
-
1252
- if (Array.isArray(value)) {
1253
- return { updateDate: [today], files: toFilesObject(value) };
1254
- }
1255
-
1256
- return value;
1257
- }
1258
-
1259
- private _cleanUrlToRelativePath(value: unknown): unknown {
1260
- if (typeof value === "string") {
1261
- if (this._isDataUri(value)) return value;
1262
- const urlMatch = value.match(/^https?:\/\/[^/]+(\/.*)/i);
1263
- if (urlMatch) return urlMatch[1];
1264
- return value;
1265
- }
1266
-
1267
- if (Array.isArray(value)) return value.map(v => this._cleanUrlToRelativePath(v));
1268
-
1269
- if (this._isObjectRecord(value)) {
1270
- if ("data" in value && typeof value.data === "string") {
1271
- const cleanedData = !this._isDataUri(value.data)
1272
- ? this._cleanUrlToRelativePath(value.data)
1273
- : value.data;
1274
- return { ...value, data: cleanedData };
1275
- }
1276
- const cleaned: Record<string, unknown> = {};
1277
- for (const [k, v] of Object.entries(value)) {
1278
- cleaned[k] = this._cleanUrlToRelativePath(v);
1279
- }
1280
- return cleaned;
1281
- }
1282
-
1283
- return value;
1284
- }
1285
-
1286
- /**
1287
- * Suffixes de types d'inputs dont les valeurs contiennent des URLs de fichiers
1288
- * à nettoyer avant envoi au serveur.
1289
- * @private
1290
- */
1291
- private static readonly INPUT_TYPES_TO_CLEAN_URLS = [".uploader", ".simpleTable"] as const;
1292
-
1293
- /**
1294
- * Nettoie les URLs absolues → chemins relatifs **uniquement** sur les champs
1295
- * uploader/simpleTable. Les autres champs (finder, text…) peuvent contenir
1296
- * des URLs légitimes qu'il ne faut pas tronquer.
1297
- * @private
1298
- */
1299
- private _cleanUploaderUrls(
1300
- data: AllStepsData,
1301
- formData: FormItemNormalized | undefined,
1302
- ): AllStepsData {
1303
- if (!formData) return data;
1304
- const result: Record<string, unknown> = {};
1305
-
1306
- for (const [subFormId, subFormData] of Object.entries(data)) {
1307
- if (!this._isObjectRecord(subFormData)) {
1308
- result[subFormId] = subFormData;
1309
- continue;
1310
- }
1311
- const subForm = formData.inputs?.[subFormId];
1312
- if (!subForm) {
1313
- result[subFormId] = subFormData;
1314
- continue;
1315
- }
1316
- const cleaned: Record<string, unknown> = {};
1317
- for (const [inputId, inputValue] of Object.entries(subFormData as Record<string, unknown>)) {
1318
- const input = subForm.inputs?.[inputId];
1319
- const inputType = input && typeof input === "object" && "type" in input
1320
- ? (input.type as string | undefined)
1321
- : undefined;
1322
- const shouldClean = !!inputType
1323
- && Answer.INPUT_TYPES_TO_CLEAN_URLS.some(suffix => inputType.endsWith(suffix));
1324
- cleaned[inputId] = shouldClean
1325
- ? this._cleanUrlToRelativePath(inputValue)
1326
- : inputValue;
1327
- }
1328
- result[subFormId] = cleaned;
1329
- }
1330
- return result as AllStepsData;
1331
- }
1332
-
1333
- /**
1334
- * Upload par batches parallèles (défaut : 4 fichiers simultanés).
1335
- * @private
1336
- */
1337
- private async _uploadInBatches<T, R>(
1338
- items: T[],
1339
- uploadFn: (item: T, index: number) => Promise<R>,
1340
- batchSize: number = 4,
1341
- ): Promise<R[]> {
1342
- const results: R[] = [];
1343
- for (let i = 0; i < items.length; i += batchSize) {
1344
- const batch = items.slice(i, i + batchSize);
1345
- const batchResults = await Promise.all(
1346
- batch.map((item, batchIndex) => uploadFn(item, i + batchIndex)),
1347
- );
1348
- results.push(...batchResults);
1349
- }
1350
- return results;
1351
- }
1352
-
1353
- /**
1354
- * Upload un PendingUpload, retourne la valeur de remplacement à insérer
1355
- * dans `allData` (`{docId, docPath}` pour uploader, `docPath` string sinon).
1356
- * @private
1357
- */
1358
- private async _uploadPendingItem(
1359
- item: PendingUpload,
1360
- index: number,
1361
- ): Promise<{ docId: string; docPath: string } | string> {
1362
- const fallbackName = `upload-${index}`;
1363
- const { file, docType, filename } = await this._dataUriToFile(item.value, fallbackName);
1364
- const { contentKey, subKey } = this._getUploadKeys(item.inputType, item.path);
1365
-
1366
- const result = await this.uploadFile(file, {
1367
- docType,
1368
- contentKey,
1369
- subKey,
1370
- qqfilename: filename,
1371
- });
1372
-
1373
- const isUploader = item.inputType?.endsWith(".uploader") ?? false;
1374
- return isUploader
1375
- ? { docId: result.docId, docPath: result.docPath }
1376
- : result.docPath;
1377
- }
1378
-
1379
- /**
1380
- * Upload un PendingUpload + remplace immédiatement dans `prepared` à son path.
1381
- * Utilisé pour le premier upload (séquentiel) qui crée l'Answer.
1382
- * @private
1383
- */
1384
- private async _uploadAndReplace(
1385
- item: PendingUpload,
1386
- prepared: AllStepsData,
1387
- index: number,
1388
- ): Promise<AllStepsData> {
1389
- const replacement = await this._uploadPendingItem(item, index);
1390
- return this._setValueAtPath(prepared, item.path, replacement) as AllStepsData;
1391
- }
1392
- }