@plumix/core 0.5.0 → 0.7.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 (454) hide show
  1. package/dist/cache/embedded-tags.d.ts +5 -0
  2. package/dist/cache/embedded-tags.d.ts.map +1 -0
  3. package/dist/cache/embedded-tags.js +30 -0
  4. package/dist/cache/embedded-tags.js.map +1 -0
  5. package/dist/context/app.d.ts +12 -0
  6. package/dist/context/app.d.ts.map +1 -1
  7. package/dist/context/app.js +10 -4
  8. package/dist/context/app.js.map +1 -1
  9. package/dist/context/memo.d.ts +22 -0
  10. package/dist/context/memo.d.ts.map +1 -0
  11. package/dist/context/memo.js +27 -0
  12. package/dist/context/memo.js.map +1 -0
  13. package/dist/context/traced-slots.d.ts +17 -0
  14. package/dist/context/traced-slots.d.ts.map +1 -0
  15. package/dist/context/traced-slots.js +57 -0
  16. package/dist/context/traced-slots.js.map +1 -0
  17. package/dist/entries/read-service.d.ts +6 -0
  18. package/dist/entries/read-service.d.ts.map +1 -1
  19. package/dist/entries/read-service.js +19 -3
  20. package/dist/entries/read-service.js.map +1 -1
  21. package/dist/index.d.ts +6 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/plugin/errors.d.ts +7 -1
  26. package/dist/plugin/errors.d.ts.map +1 -1
  27. package/dist/plugin/errors.js +5 -0
  28. package/dist/plugin/errors.js.map +1 -1
  29. package/dist/plugin/fields/builder.d.ts +106 -0
  30. package/dist/plugin/fields/builder.d.ts.map +1 -0
  31. package/dist/plugin/fields/builder.js +162 -0
  32. package/dist/plugin/fields/builder.js.map +1 -0
  33. package/dist/plugin/fields/color.d.ts +78 -18
  34. package/dist/plugin/fields/color.d.ts.map +1 -1
  35. package/dist/plugin/fields/color.js +116 -25
  36. package/dist/plugin/fields/color.js.map +1 -1
  37. package/dist/plugin/fields/condition.d.ts +40 -0
  38. package/dist/plugin/fields/condition.d.ts.map +1 -0
  39. package/dist/plugin/fields/condition.js +115 -0
  40. package/dist/plugin/fields/condition.js.map +1 -0
  41. package/dist/plugin/fields/contributions.d.ts +233 -0
  42. package/dist/plugin/fields/contributions.d.ts.map +1 -0
  43. package/dist/plugin/fields/contributions.js +1 -0
  44. package/dist/plugin/fields/contributions.js.map +1 -0
  45. package/dist/plugin/fields/entry.d.ts +24 -20
  46. package/dist/plugin/fields/entry.d.ts.map +1 -1
  47. package/dist/plugin/fields/entry.js +13 -21
  48. package/dist/plugin/fields/entry.js.map +1 -1
  49. package/dist/plugin/fields/errors.d.ts +23 -11
  50. package/dist/plugin/fields/errors.d.ts.map +1 -1
  51. package/dist/plugin/fields/errors.js +20 -13
  52. package/dist/plugin/fields/errors.js.map +1 -1
  53. package/dist/plugin/fields/group.d.ts +90 -0
  54. package/dist/plugin/fields/group.d.ts.map +1 -0
  55. package/dist/plugin/fields/group.js +124 -0
  56. package/dist/plugin/fields/group.js.map +1 -0
  57. package/dist/plugin/fields/index.d.ts +22 -45
  58. package/dist/plugin/fields/index.d.ts.map +1 -1
  59. package/dist/plugin/fields/index.js +25 -30
  60. package/dist/plugin/fields/index.js.map +1 -1
  61. package/dist/plugin/fields/json.d.ts +71 -9
  62. package/dist/plugin/fields/json.d.ts.map +1 -1
  63. package/dist/plugin/fields/json.js +110 -16
  64. package/dist/plugin/fields/json.js.map +1 -1
  65. package/dist/plugin/fields/link.d.ts +96 -0
  66. package/dist/plugin/fields/link.d.ts.map +1 -0
  67. package/dist/plugin/fields/link.js +160 -0
  68. package/dist/plugin/fields/link.js.map +1 -0
  69. package/dist/plugin/fields/number.d.ts +86 -13
  70. package/dist/plugin/fields/number.d.ts.map +1 -1
  71. package/dist/plugin/fields/number.js +133 -20
  72. package/dist/plugin/fields/number.js.map +1 -1
  73. package/dist/plugin/fields/range.d.ts +101 -17
  74. package/dist/plugin/fields/range.d.ts.map +1 -1
  75. package/dist/plugin/fields/range.js +153 -38
  76. package/dist/plugin/fields/range.js.map +1 -1
  77. package/dist/plugin/fields/reference.d.ts +171 -0
  78. package/dist/plugin/fields/reference.d.ts.map +1 -0
  79. package/dist/plugin/fields/reference.js +193 -0
  80. package/dist/plugin/fields/reference.js.map +1 -0
  81. package/dist/plugin/fields/repeater.d.ts +111 -8
  82. package/dist/plugin/fields/repeater.d.ts.map +1 -1
  83. package/dist/plugin/fields/repeater.js +157 -66
  84. package/dist/plugin/fields/repeater.js.map +1 -1
  85. package/dist/plugin/fields/richtext-validate.d.ts +7 -0
  86. package/dist/plugin/fields/richtext-validate.d.ts.map +1 -1
  87. package/dist/plugin/fields/richtext-validate.js +11 -5
  88. package/dist/plugin/fields/richtext-validate.js.map +1 -1
  89. package/dist/plugin/fields/richtext.d.ts +78 -12
  90. package/dist/plugin/fields/richtext.d.ts.map +1 -1
  91. package/dist/plugin/fields/richtext.js +121 -26
  92. package/dist/plugin/fields/richtext.js.map +1 -1
  93. package/dist/plugin/fields/select.d.ts +150 -10
  94. package/dist/plugin/fields/select.d.ts.map +1 -1
  95. package/dist/plugin/fields/select.js +205 -17
  96. package/dist/plugin/fields/select.js.map +1 -1
  97. package/dist/plugin/fields/sub-fields.d.ts +12 -0
  98. package/dist/plugin/fields/sub-fields.d.ts.map +1 -0
  99. package/dist/plugin/fields/sub-fields.js +62 -0
  100. package/dist/plugin/fields/sub-fields.js.map +1 -0
  101. package/dist/plugin/fields/temporal.d.ts +124 -0
  102. package/dist/plugin/fields/temporal.d.ts.map +1 -0
  103. package/dist/plugin/fields/temporal.js +180 -0
  104. package/dist/plugin/fields/temporal.js.map +1 -0
  105. package/dist/plugin/fields/term.d.ts +11 -16
  106. package/dist/plugin/fields/term.d.ts.map +1 -1
  107. package/dist/plugin/fields/term.js +11 -19
  108. package/dist/plugin/fields/term.js.map +1 -1
  109. package/dist/plugin/fields/toggle.d.ts +97 -0
  110. package/dist/plugin/fields/toggle.d.ts.map +1 -0
  111. package/dist/plugin/fields/toggle.js +135 -0
  112. package/dist/plugin/fields/toggle.js.map +1 -0
  113. package/dist/plugin/fields/user.d.ts +14 -26
  114. package/dist/plugin/fields/user.d.ts.map +1 -1
  115. package/dist/plugin/fields/user.js +11 -25
  116. package/dist/plugin/fields/user.js.map +1 -1
  117. package/dist/plugin/lookup.d.ts +74 -10
  118. package/dist/plugin/lookup.d.ts.map +1 -1
  119. package/dist/plugin/manifest.d.ts +319 -160
  120. package/dist/plugin/manifest.d.ts.map +1 -1
  121. package/dist/plugin/manifest.js +94 -4
  122. package/dist/plugin/manifest.js.map +1 -1
  123. package/dist/plugin/setup-context.d.ts +10 -4
  124. package/dist/plugin/setup-context.d.ts.map +1 -1
  125. package/dist/plugin/setup-context.js +10 -3
  126. package/dist/plugin/setup-context.js.map +1 -1
  127. package/dist/plugin/validation/meta-box-fields.d.ts.map +1 -1
  128. package/dist/plugin/validation/meta-box-fields.js +16 -0
  129. package/dist/plugin/validation/meta-box-fields.js.map +1 -1
  130. package/dist/route/render/asset-manifest.d.ts +1 -1
  131. package/dist/route/render/asset-manifest.d.ts.map +1 -1
  132. package/dist/route/render/asset-manifest.js +7 -1
  133. package/dist/route/render/asset-manifest.js.map +1 -1
  134. package/dist/route/render/build-resolved-entries.d.ts.map +1 -1
  135. package/dist/route/render/build-resolved-entries.js +25 -12
  136. package/dist/route/render/build-resolved-entries.js.map +1 -1
  137. package/dist/route/render/render-template.d.ts +1 -1
  138. package/dist/route/render/render-template.d.ts.map +1 -1
  139. package/dist/route/render/render-template.js +50 -23
  140. package/dist/route/render/render-template.js.map +1 -1
  141. package/dist/route/render/template-builders.d.ts +15 -10
  142. package/dist/route/render/template-builders.d.ts.map +1 -1
  143. package/dist/route/render/template-builders.js.map +1 -1
  144. package/dist/rpc/base.d.ts +7 -0
  145. package/dist/rpc/base.d.ts.map +1 -1
  146. package/dist/rpc/errors.d.ts +7 -0
  147. package/dist/rpc/errors.d.ts.map +1 -1
  148. package/dist/rpc/errors.js +16 -0
  149. package/dist/rpc/errors.js.map +1 -1
  150. package/dist/rpc/meta/core.d.ts +92 -14
  151. package/dist/rpc/meta/core.d.ts.map +1 -1
  152. package/dist/rpc/meta/core.js +524 -406
  153. package/dist/rpc/meta/core.js.map +1 -1
  154. package/dist/rpc/meta/field-messages.d.ts +67 -0
  155. package/dist/rpc/meta/field-messages.d.ts.map +1 -0
  156. package/dist/rpc/meta/field-messages.js +67 -0
  157. package/dist/rpc/meta/field-messages.js.map +1 -0
  158. package/dist/rpc/meta/field-pipeline.d.ts +52 -0
  159. package/dist/rpc/meta/field-pipeline.d.ts.map +1 -0
  160. package/dist/rpc/meta/field-pipeline.js +501 -0
  161. package/dist/rpc/meta/field-pipeline.js.map +1 -0
  162. package/dist/rpc/procedures/auth/allowed-domains/create.d.ts +7 -0
  163. package/dist/rpc/procedures/auth/allowed-domains/create.d.ts.map +1 -1
  164. package/dist/rpc/procedures/auth/allowed-domains/delete.d.ts +7 -0
  165. package/dist/rpc/procedures/auth/allowed-domains/delete.d.ts.map +1 -1
  166. package/dist/rpc/procedures/auth/allowed-domains/index.d.ts +28 -0
  167. package/dist/rpc/procedures/auth/allowed-domains/index.d.ts.map +1 -1
  168. package/dist/rpc/procedures/auth/allowed-domains/list.d.ts +7 -0
  169. package/dist/rpc/procedures/auth/allowed-domains/list.d.ts.map +1 -1
  170. package/dist/rpc/procedures/auth/allowed-domains/update.d.ts +7 -0
  171. package/dist/rpc/procedures/auth/allowed-domains/update.d.ts.map +1 -1
  172. package/dist/rpc/procedures/auth/api-tokens/admin-list.d.ts +7 -0
  173. package/dist/rpc/procedures/auth/api-tokens/admin-list.d.ts.map +1 -1
  174. package/dist/rpc/procedures/auth/api-tokens/admin-revoke.d.ts +7 -0
  175. package/dist/rpc/procedures/auth/api-tokens/admin-revoke.d.ts.map +1 -1
  176. package/dist/rpc/procedures/auth/api-tokens/create.d.ts +7 -0
  177. package/dist/rpc/procedures/auth/api-tokens/create.d.ts.map +1 -1
  178. package/dist/rpc/procedures/auth/api-tokens/index.d.ts +35 -0
  179. package/dist/rpc/procedures/auth/api-tokens/index.d.ts.map +1 -1
  180. package/dist/rpc/procedures/auth/api-tokens/list.d.ts +7 -0
  181. package/dist/rpc/procedures/auth/api-tokens/list.d.ts.map +1 -1
  182. package/dist/rpc/procedures/auth/api-tokens/revoke.d.ts +7 -0
  183. package/dist/rpc/procedures/auth/api-tokens/revoke.d.ts.map +1 -1
  184. package/dist/rpc/procedures/auth/credentials/delete.d.ts +7 -0
  185. package/dist/rpc/procedures/auth/credentials/delete.d.ts.map +1 -1
  186. package/dist/rpc/procedures/auth/credentials/index.d.ts +21 -0
  187. package/dist/rpc/procedures/auth/credentials/index.d.ts.map +1 -1
  188. package/dist/rpc/procedures/auth/credentials/list.d.ts +7 -0
  189. package/dist/rpc/procedures/auth/credentials/list.d.ts.map +1 -1
  190. package/dist/rpc/procedures/auth/credentials/rename.d.ts +7 -0
  191. package/dist/rpc/procedures/auth/credentials/rename.d.ts.map +1 -1
  192. package/dist/rpc/procedures/auth/device-flow/approve.d.ts +7 -0
  193. package/dist/rpc/procedures/auth/device-flow/approve.d.ts.map +1 -1
  194. package/dist/rpc/procedures/auth/device-flow/deny.d.ts +7 -0
  195. package/dist/rpc/procedures/auth/device-flow/deny.d.ts.map +1 -1
  196. package/dist/rpc/procedures/auth/device-flow/index.d.ts +21 -0
  197. package/dist/rpc/procedures/auth/device-flow/index.d.ts.map +1 -1
  198. package/dist/rpc/procedures/auth/device-flow/lookup.d.ts +7 -0
  199. package/dist/rpc/procedures/auth/device-flow/lookup.d.ts.map +1 -1
  200. package/dist/rpc/procedures/auth/index.d.ts +154 -0
  201. package/dist/rpc/procedures/auth/index.d.ts.map +1 -1
  202. package/dist/rpc/procedures/auth/login-links.d.ts +7 -0
  203. package/dist/rpc/procedures/auth/login-links.d.ts.map +1 -1
  204. package/dist/rpc/procedures/auth/mailer/index.d.ts +7 -0
  205. package/dist/rpc/procedures/auth/mailer/index.d.ts.map +1 -1
  206. package/dist/rpc/procedures/auth/mailer/test-send.d.ts +7 -0
  207. package/dist/rpc/procedures/auth/mailer/test-send.d.ts.map +1 -1
  208. package/dist/rpc/procedures/auth/oauth-providers.d.ts +7 -0
  209. package/dist/rpc/procedures/auth/oauth-providers.d.ts.map +1 -1
  210. package/dist/rpc/procedures/auth/session.d.ts +7 -0
  211. package/dist/rpc/procedures/auth/session.d.ts.map +1 -1
  212. package/dist/rpc/procedures/auth/sessions/index.d.ts +21 -0
  213. package/dist/rpc/procedures/auth/sessions/index.d.ts.map +1 -1
  214. package/dist/rpc/procedures/auth/sessions/list.d.ts +7 -0
  215. package/dist/rpc/procedures/auth/sessions/list.d.ts.map +1 -1
  216. package/dist/rpc/procedures/auth/sessions/revoke-others.d.ts +7 -0
  217. package/dist/rpc/procedures/auth/sessions/revoke-others.d.ts.map +1 -1
  218. package/dist/rpc/procedures/auth/sessions/revoke.d.ts +7 -0
  219. package/dist/rpc/procedures/auth/sessions/revoke.d.ts.map +1 -1
  220. package/dist/rpc/procedures/entry/activity.d.ts +7 -0
  221. package/dist/rpc/procedures/entry/activity.d.ts.map +1 -1
  222. package/dist/rpc/procedures/entry/bulk.d.ts +21 -0
  223. package/dist/rpc/procedures/entry/bulk.d.ts.map +1 -1
  224. package/dist/rpc/procedures/entry/create-preview-link.d.ts +7 -0
  225. package/dist/rpc/procedures/entry/create-preview-link.d.ts.map +1 -1
  226. package/dist/rpc/procedures/entry/create.d.ts +8 -1
  227. package/dist/rpc/procedures/entry/create.d.ts.map +1 -1
  228. package/dist/rpc/procedures/entry/create.js +8 -8
  229. package/dist/rpc/procedures/entry/create.js.map +1 -1
  230. package/dist/rpc/procedures/entry/dashboard.d.ts +14 -0
  231. package/dist/rpc/procedures/entry/dashboard.d.ts.map +1 -1
  232. package/dist/rpc/procedures/entry/delete-permanent.d.ts +7 -0
  233. package/dist/rpc/procedures/entry/delete-permanent.d.ts.map +1 -1
  234. package/dist/rpc/procedures/entry/discard-draft.d.ts +7 -0
  235. package/dist/rpc/procedures/entry/discard-draft.d.ts.map +1 -1
  236. package/dist/rpc/procedures/entry/duplicate.d.ts +7 -0
  237. package/dist/rpc/procedures/entry/duplicate.d.ts.map +1 -1
  238. package/dist/rpc/procedures/entry/duplicate.js +2 -2
  239. package/dist/rpc/procedures/entry/duplicate.js.map +1 -1
  240. package/dist/rpc/procedures/entry/get.d.ts +7 -0
  241. package/dist/rpc/procedures/entry/get.d.ts.map +1 -1
  242. package/dist/rpc/procedures/entry/get.js +2 -2
  243. package/dist/rpc/procedures/entry/get.js.map +1 -1
  244. package/dist/rpc/procedures/entry/index.d.ts +156 -2
  245. package/dist/rpc/procedures/entry/index.d.ts.map +1 -1
  246. package/dist/rpc/procedures/entry/list.d.ts +7 -0
  247. package/dist/rpc/procedures/entry/list.d.ts.map +1 -1
  248. package/dist/rpc/procedures/entry/lookup.d.ts +8 -2
  249. package/dist/rpc/procedures/entry/lookup.d.ts.map +1 -1
  250. package/dist/rpc/procedures/entry/lookup.js +74 -12
  251. package/dist/rpc/procedures/entry/lookup.js.map +1 -1
  252. package/dist/rpc/procedures/entry/meta.d.ts +37 -3
  253. package/dist/rpc/procedures/entry/meta.d.ts.map +1 -1
  254. package/dist/rpc/procedures/entry/meta.js +63 -7
  255. package/dist/rpc/procedures/entry/meta.js.map +1 -1
  256. package/dist/rpc/procedures/entry/publish.d.ts +7 -0
  257. package/dist/rpc/procedures/entry/publish.d.ts.map +1 -1
  258. package/dist/rpc/procedures/entry/publish.js +6 -4
  259. package/dist/rpc/procedures/entry/publish.js.map +1 -1
  260. package/dist/rpc/procedures/entry/refresh-block-loader.d.ts +7 -0
  261. package/dist/rpc/procedures/entry/refresh-block-loader.d.ts.map +1 -1
  262. package/dist/rpc/procedures/entry/restore.d.ts +7 -0
  263. package/dist/rpc/procedures/entry/restore.d.ts.map +1 -1
  264. package/dist/rpc/procedures/entry/revisions.d.ts +56 -0
  265. package/dist/rpc/procedures/entry/revisions.d.ts.map +1 -1
  266. package/dist/rpc/procedures/entry/schemas.d.ts +2 -2
  267. package/dist/rpc/procedures/entry/schemas.d.ts.map +1 -1
  268. package/dist/rpc/procedures/entry/schemas.js +9 -2
  269. package/dist/rpc/procedures/entry/schemas.js.map +1 -1
  270. package/dist/rpc/procedures/entry/trash.d.ts +7 -0
  271. package/dist/rpc/procedures/entry/trash.d.ts.map +1 -1
  272. package/dist/rpc/procedures/entry/update.d.ts +8 -1
  273. package/dist/rpc/procedures/entry/update.d.ts.map +1 -1
  274. package/dist/rpc/procedures/entry/update.js +77 -18
  275. package/dist/rpc/procedures/entry/update.js.map +1 -1
  276. package/dist/rpc/procedures/lookup/index.d.ts +14 -0
  277. package/dist/rpc/procedures/lookup/index.d.ts.map +1 -1
  278. package/dist/rpc/procedures/lookup/list.d.ts +7 -0
  279. package/dist/rpc/procedures/lookup/list.d.ts.map +1 -1
  280. package/dist/rpc/procedures/lookup/resolve.d.ts +7 -0
  281. package/dist/rpc/procedures/lookup/resolve.d.ts.map +1 -1
  282. package/dist/rpc/procedures/lookup/resolve.js +1 -1
  283. package/dist/rpc/procedures/lookup/resolve.js.map +1 -1
  284. package/dist/rpc/procedures/lookup.errors.d.ts +2 -1
  285. package/dist/rpc/procedures/lookup.errors.d.ts.map +1 -1
  286. package/dist/rpc/procedures/lookup.errors.js +3 -0
  287. package/dist/rpc/procedures/lookup.errors.js.map +1 -1
  288. package/dist/rpc/procedures/search/index.d.ts +7 -0
  289. package/dist/rpc/procedures/search/index.d.ts.map +1 -1
  290. package/dist/rpc/procedures/settings/get.d.ts +7 -0
  291. package/dist/rpc/procedures/settings/get.d.ts.map +1 -1
  292. package/dist/rpc/procedures/settings/index.d.ts +14 -0
  293. package/dist/rpc/procedures/settings/index.d.ts.map +1 -1
  294. package/dist/rpc/procedures/settings/upsert.d.ts +7 -0
  295. package/dist/rpc/procedures/settings/upsert.d.ts.map +1 -1
  296. package/dist/rpc/procedures/settings/upsert.js +9 -0
  297. package/dist/rpc/procedures/settings/upsert.js.map +1 -1
  298. package/dist/rpc/procedures/term/create.d.ts +7 -0
  299. package/dist/rpc/procedures/term/create.d.ts.map +1 -1
  300. package/dist/rpc/procedures/term/create.js +3 -3
  301. package/dist/rpc/procedures/term/create.js.map +1 -1
  302. package/dist/rpc/procedures/term/delete.d.ts +7 -0
  303. package/dist/rpc/procedures/term/delete.d.ts.map +1 -1
  304. package/dist/rpc/procedures/term/get.d.ts +7 -0
  305. package/dist/rpc/procedures/term/get.d.ts.map +1 -1
  306. package/dist/rpc/procedures/term/index.d.ts +35 -0
  307. package/dist/rpc/procedures/term/index.d.ts.map +1 -1
  308. package/dist/rpc/procedures/term/list.d.ts +7 -0
  309. package/dist/rpc/procedures/term/list.d.ts.map +1 -1
  310. package/dist/rpc/procedures/term/lookup.d.ts +7 -2
  311. package/dist/rpc/procedures/term/lookup.d.ts.map +1 -1
  312. package/dist/rpc/procedures/term/lookup.js +29 -8
  313. package/dist/rpc/procedures/term/lookup.js.map +1 -1
  314. package/dist/rpc/procedures/term/meta.d.ts +3 -2
  315. package/dist/rpc/procedures/term/meta.d.ts.map +1 -1
  316. package/dist/rpc/procedures/term/meta.js +7 -5
  317. package/dist/rpc/procedures/term/meta.js.map +1 -1
  318. package/dist/rpc/procedures/term/update.d.ts +7 -0
  319. package/dist/rpc/procedures/term/update.d.ts.map +1 -1
  320. package/dist/rpc/procedures/term/update.js +4 -4
  321. package/dist/rpc/procedures/term/update.js.map +1 -1
  322. package/dist/rpc/procedures/user/cancel-email-change.d.ts +7 -0
  323. package/dist/rpc/procedures/user/cancel-email-change.d.ts.map +1 -1
  324. package/dist/rpc/procedures/user/delete.d.ts +7 -0
  325. package/dist/rpc/procedures/user/delete.d.ts.map +1 -1
  326. package/dist/rpc/procedures/user/disable.d.ts +7 -0
  327. package/dist/rpc/procedures/user/disable.d.ts.map +1 -1
  328. package/dist/rpc/procedures/user/enable.d.ts +7 -0
  329. package/dist/rpc/procedures/user/enable.d.ts.map +1 -1
  330. package/dist/rpc/procedures/user/get.d.ts +7 -0
  331. package/dist/rpc/procedures/user/get.d.ts.map +1 -1
  332. package/dist/rpc/procedures/user/get.js +2 -2
  333. package/dist/rpc/procedures/user/get.js.map +1 -1
  334. package/dist/rpc/procedures/user/index.d.ts +77 -0
  335. package/dist/rpc/procedures/user/index.d.ts.map +1 -1
  336. package/dist/rpc/procedures/user/invite.d.ts +7 -0
  337. package/dist/rpc/procedures/user/invite.d.ts.map +1 -1
  338. package/dist/rpc/procedures/user/list.d.ts +7 -0
  339. package/dist/rpc/procedures/user/list.d.ts.map +1 -1
  340. package/dist/rpc/procedures/user/lookup.d.ts +6 -2
  341. package/dist/rpc/procedures/user/lookup.d.ts.map +1 -1
  342. package/dist/rpc/procedures/user/lookup.js +29 -0
  343. package/dist/rpc/procedures/user/lookup.js.map +1 -1
  344. package/dist/rpc/procedures/user/meta.d.ts +3 -2
  345. package/dist/rpc/procedures/user/meta.d.ts.map +1 -1
  346. package/dist/rpc/procedures/user/meta.js +7 -5
  347. package/dist/rpc/procedures/user/meta.js.map +1 -1
  348. package/dist/rpc/procedures/user/pending-email-change.d.ts +7 -0
  349. package/dist/rpc/procedures/user/pending-email-change.d.ts.map +1 -1
  350. package/dist/rpc/procedures/user/request-email-change.d.ts +7 -0
  351. package/dist/rpc/procedures/user/request-email-change.d.ts.map +1 -1
  352. package/dist/rpc/procedures/user/set-locale.d.ts +7 -0
  353. package/dist/rpc/procedures/user/set-locale.d.ts.map +1 -1
  354. package/dist/rpc/procedures/user/update.d.ts +7 -0
  355. package/dist/rpc/procedures/user/update.d.ts.map +1 -1
  356. package/dist/rpc/procedures/user/update.js +4 -4
  357. package/dist/rpc/procedures/user/update.js.map +1 -1
  358. package/dist/rpc/router.d.ts +457 -2
  359. package/dist/rpc/router.d.ts.map +1 -1
  360. package/dist/rpc/validation.d.ts +1 -0
  361. package/dist/rpc/validation.d.ts.map +1 -1
  362. package/dist/rpc/validation.js +1 -0
  363. package/dist/rpc/validation.js.map +1 -1
  364. package/dist/runtime/dispatcher.d.ts.map +1 -1
  365. package/dist/runtime/dispatcher.js +90 -39
  366. package/dist/runtime/dispatcher.js.map +1 -1
  367. package/dist/seo/feed.d.ts.map +1 -1
  368. package/dist/seo/feed.js +7 -1
  369. package/dist/seo/feed.js.map +1 -1
  370. package/dist/settings-core.js +1 -1
  371. package/dist/settings-core.js.map +1 -1
  372. package/dist/telemetry-otel.d.ts +29 -0
  373. package/dist/telemetry-otel.d.ts.map +1 -0
  374. package/dist/telemetry-otel.js +218 -0
  375. package/dist/telemetry-otel.js.map +1 -0
  376. package/dist/template-deps-core.d.ts.map +1 -1
  377. package/dist/template-deps-core.js +31 -14
  378. package/dist/template-deps-core.js.map +1 -1
  379. package/dist/template-deps.d.ts.map +1 -1
  380. package/dist/template-deps.js +12 -8
  381. package/dist/template-deps.js.map +1 -1
  382. package/dist/template-registry.d.ts +0 -16
  383. package/dist/template-registry.d.ts.map +1 -1
  384. package/dist/terms/read-service.js +2 -2
  385. package/dist/terms/read-service.js.map +1 -1
  386. package/dist/test/index.d.ts +3 -0
  387. package/dist/test/index.d.ts.map +1 -1
  388. package/dist/test/index.js +4 -0
  389. package/dist/test/index.js.map +1 -1
  390. package/dist/test/traced-context.d.ts +17 -0
  391. package/dist/test/traced-context.d.ts.map +1 -0
  392. package/dist/test/traced-context.js +28 -0
  393. package/dist/test/traced-context.js.map +1 -0
  394. package/package.json +6 -2
  395. package/dist/plugin/fields/checkbox.d.ts +0 -19
  396. package/dist/plugin/fields/checkbox.d.ts.map +0 -1
  397. package/dist/plugin/fields/checkbox.js +0 -20
  398. package/dist/plugin/fields/checkbox.js.map +0 -1
  399. package/dist/plugin/fields/date.d.ts +0 -24
  400. package/dist/plugin/fields/date.d.ts.map +0 -1
  401. package/dist/plugin/fields/date.js +0 -23
  402. package/dist/plugin/fields/date.js.map +0 -1
  403. package/dist/plugin/fields/datetime.d.ts +0 -26
  404. package/dist/plugin/fields/datetime.d.ts.map +0 -1
  405. package/dist/plugin/fields/datetime.js +0 -25
  406. package/dist/plugin/fields/datetime.js.map +0 -1
  407. package/dist/plugin/fields/email.d.ts +0 -17
  408. package/dist/plugin/fields/email.d.ts.map +0 -1
  409. package/dist/plugin/fields/email.js +0 -18
  410. package/dist/plugin/fields/email.js.map +0 -1
  411. package/dist/plugin/fields/entry-list.d.ts +0 -28
  412. package/dist/plugin/fields/entry-list.d.ts.map +0 -1
  413. package/dist/plugin/fields/entry-list.js +0 -31
  414. package/dist/plugin/fields/entry-list.js.map +0 -1
  415. package/dist/plugin/fields/multiselect.d.ts +0 -21
  416. package/dist/plugin/fields/multiselect.d.ts.map +0 -1
  417. package/dist/plugin/fields/multiselect.js +0 -43
  418. package/dist/plugin/fields/multiselect.js.map +0 -1
  419. package/dist/plugin/fields/password.d.ts +0 -24
  420. package/dist/plugin/fields/password.d.ts.map +0 -1
  421. package/dist/plugin/fields/password.js +0 -25
  422. package/dist/plugin/fields/password.js.map +0 -1
  423. package/dist/plugin/fields/radio.d.ts +0 -16
  424. package/dist/plugin/fields/radio.d.ts.map +0 -1
  425. package/dist/plugin/fields/radio.js +0 -17
  426. package/dist/plugin/fields/radio.js.map +0 -1
  427. package/dist/plugin/fields/repeater-validate.d.ts +0 -34
  428. package/dist/plugin/fields/repeater-validate.d.ts.map +0 -1
  429. package/dist/plugin/fields/repeater-validate.js +0 -104
  430. package/dist/plugin/fields/repeater-validate.js.map +0 -1
  431. package/dist/plugin/fields/term-list.d.ts +0 -27
  432. package/dist/plugin/fields/term-list.d.ts.map +0 -1
  433. package/dist/plugin/fields/term-list.js +0 -30
  434. package/dist/plugin/fields/term-list.js.map +0 -1
  435. package/dist/plugin/fields/text.d.ts +0 -37
  436. package/dist/plugin/fields/text.d.ts.map +0 -1
  437. package/dist/plugin/fields/text.js +0 -31
  438. package/dist/plugin/fields/text.js.map +0 -1
  439. package/dist/plugin/fields/textarea.d.ts +0 -20
  440. package/dist/plugin/fields/textarea.d.ts.map +0 -1
  441. package/dist/plugin/fields/textarea.js +0 -21
  442. package/dist/plugin/fields/textarea.js.map +0 -1
  443. package/dist/plugin/fields/time.d.ts +0 -24
  444. package/dist/plugin/fields/time.d.ts.map +0 -1
  445. package/dist/plugin/fields/time.js +0 -23
  446. package/dist/plugin/fields/time.js.map +0 -1
  447. package/dist/plugin/fields/url.d.ts +0 -17
  448. package/dist/plugin/fields/url.d.ts.map +0 -1
  449. package/dist/plugin/fields/url.js +0 -18
  450. package/dist/plugin/fields/url.js.map +0 -1
  451. package/dist/plugin/fields/user-list.d.ts +0 -29
  452. package/dist/plugin/fields/user-list.d.ts.map +0 -1
  453. package/dist/plugin/fields/user-list.js +0 -31
  454. package/dist/plugin/fields/user-list.js.map +0 -1
@@ -1,6 +1,11 @@
1
1
  import { sql } from "drizzle-orm";
2
+ import { accumulateEmbeddedTags } from "../../cache/embedded-tags.js";
2
3
  import { eq } from "../../db/index.js";
4
+ import { isConditionHidden } from "../../plugin/fields/condition.js";
5
+ import { anchorTemporalUtc } from "../../plugin/manifest.js";
3
6
  import { MetaReferenceError } from "./errors.js";
7
+ import { META_FIELD_MESSAGES } from "./field-messages.js";
8
+ import { extractStringId, healReferenceValue, isGroupField, isRepeaterField, referenceTargetOf, runFieldPipeline, } from "./field-pipeline.js";
4
9
  // Shared meta plumbing for every entity that stores a `meta` JSON
5
10
  // column (entries, terms, and — eventually — users). The storage
6
11
  // contract: writes merge into the bag via SQLite `json_set` /
@@ -32,70 +37,100 @@ export class MetaSanitizationError extends Error {
32
37
  return new MetaSanitizationError(ctx.key, "value_too_large");
33
38
  }
34
39
  }
40
+ /**
41
+ * The whole-patch rejection produced by `sanitizeMetaInput` when any
42
+ * field's pipeline reports errors: every `{ path, message }` across
43
+ * every key of the request, so the admin form can address each
44
+ * offending input in one round-trip. Nothing is written when this
45
+ * throws.
46
+ */
47
+ export class MetaValidationError extends Error {
48
+ static {
49
+ MetaValidationError.prototype.name = "MetaValidationError";
50
+ }
51
+ errors;
52
+ constructor(errors) {
53
+ super(`meta validation failed: ${errors.map((error) => error.path).join(", ")}`);
54
+ this.errors = errors;
55
+ }
56
+ }
35
57
  /**
36
58
  * Validate an incoming meta map against a field-lookup fn produced by
37
59
  * the caller (entry vs term differ only in which registry they walk).
38
- * Unregistered keys and type-coercion failures throw so the caller can
39
- * surface them as 4xx before any write. `null` / `undefined` values
40
- * are deletion requests; everything else is coerced + sanitized.
60
+ * `null` / `undefined` values are deletion requests; everything else
61
+ * runs the per-field pipeline (coercion `.sanitize()` declarative
62
+ * constraints `.validate()`). Pipeline rejections aggregate across
63
+ * the whole patch into one `MetaValidationError`; unregistered keys
64
+ * and oversized values keep the legacy fail-fast
65
+ * `MetaSanitizationError` surface.
41
66
  */
42
- export function sanitizeMetaInput(findField, input) {
67
+ export async function sanitizeMetaInput(findField, input, mode = "strict") {
43
68
  if (input === undefined)
44
69
  return null;
45
70
  const upserts = new Map();
46
71
  const deletes = [];
72
+ const fieldErrors = [];
47
73
  for (const [key, rawValue] of Object.entries(input)) {
48
74
  const field = findField(key);
49
75
  if (!field) {
76
+ // A delete request (null/undefined) for a key the field system doesn't
77
+ // own is a harmless no-op — never fail the whole write over an untracked
78
+ // foreign key (e.g. one written by another plugin). Unknown *upserts*
79
+ // still reject so junk can't enter through this gate.
80
+ if (rawValue === null || rawValue === undefined)
81
+ continue;
50
82
  throw MetaSanitizationError.notRegistered({ key });
51
83
  }
52
- if (rawValue === null || rawValue === undefined) {
84
+ if (isConditionHidden(field, input))
85
+ continue;
86
+ const result = await runFieldPipeline(field, rawValue, key, mode);
87
+ if (result.errors.length > 0) {
88
+ fieldErrors.push(...result.errors);
89
+ continue;
90
+ }
91
+ if (result.isDeletion === true) {
53
92
  deletes.push(key);
54
93
  continue;
55
94
  }
56
- const coerced = coerceToType(field.type, key, rawValue);
57
- const sanitized = field.sanitize
58
- ? runSanitize(field.sanitize, key, coerced)
59
- : coerced;
60
- assertEncodedSize(key, sanitized);
61
- upserts.set(key, sanitized);
95
+ assertEncodedSize(key, result.value);
96
+ upserts.set(key, result.value);
97
+ }
98
+ if (fieldErrors.length > 0) {
99
+ throw new MetaValidationError(fieldErrors);
62
100
  }
63
101
  return { upserts, deletes };
64
102
  }
65
103
  /**
66
- * Run a field's sanitize callback inside a try/catch any thrown
67
- * `MetaSanitizationError` propagates as-is (so callbacks that already
68
- * use the precise error type can opt into specific reasons), but
69
- * generic errors (including plain `Error("invalid_value")` thrown
70
- * from builder-injected default sanitizers) are translated into a
71
- * uniform `invalid_value` failure. Callbacks therefore stay free to
72
- * throw vanilla errors without importing the package-internal error
73
- * class.
104
+ * Thin wrapper that translates thrown meta errors into the RPC
105
+ * handler's CONFLICT envelope. Pipeline rejections ship their
106
+ * `{ path, message }` list under `data.errors` (with `key` pointing
107
+ * at the first error's top-level field for legacy consumers); the
108
+ * fail-fast `MetaSanitizationError` reasons keep their existing
109
+ * `data.reason`/`data.key` shape.
74
110
  */
75
- function runSanitize(sanitize, key, value) {
76
- try {
77
- return sanitize(value);
78
- }
79
- catch (error) {
80
- if (error instanceof MetaSanitizationError)
81
- throw error;
82
- // Buggy sanitize callbacks otherwise round to a generic
83
- // `invalid_value` envelope, which is fine for the editor's UX
84
- // but loses the underlying stack. Log it before translating so
85
- // server logs preserve the diagnostic trail.
86
- console.error(`[plumix] sanitize callback for meta key ${JSON.stringify(key)} threw:`, error);
87
- throw MetaSanitizationError.invalidValue({ key });
88
- }
89
- }
90
111
  /**
91
- * Thin wrapper that translates a thrown `MetaSanitizationError` into
92
- * the RPC handler's CONFLICT envelope.
112
+ * Map a whole-patch meta validation failure onto the RPC `CONFLICT`
113
+ * envelope — one place so the wire shape (`key` at the first error's
114
+ * top-level field, the full `errors` list) can't drift between the write
115
+ * path and the publish gate.
93
116
  */
94
- export function sanitizeMetaForRpc(findField, input, errors) {
117
+ export function metaValidationConflict(error, errors) {
118
+ return errors.CONFLICT({
119
+ data: {
120
+ reason: "meta_invalid_value",
121
+ key: error.errors[0]?.path.split(".")[0],
122
+ errors: [...error.errors],
123
+ },
124
+ });
125
+ }
126
+ export async function sanitizeMetaForRpc(findField, input, errors, mode = "strict") {
95
127
  try {
96
- return sanitizeMetaInput(findField, input);
128
+ return await sanitizeMetaInput(findField, input, mode);
97
129
  }
98
130
  catch (error) {
131
+ if (error instanceof MetaValidationError) {
132
+ throw metaValidationConflict(error, errors);
133
+ }
99
134
  if (error instanceof MetaSanitizationError) {
100
135
  throw errors.CONFLICT({
101
136
  data: { reason: `meta_${error.reason}`, key: error.key },
@@ -104,6 +139,58 @@ export function sanitizeMetaForRpc(findField, input, errors) {
104
139
  throw error;
105
140
  }
106
141
  }
142
+ /**
143
+ * Strict publish gate: validate a whole *stored* meta bag against the full
144
+ * field list before `entry.publish` promotes it onto the live row. Draft
145
+ * autosaves are lenient (business rules skipped so work-in-progress always
146
+ * saves), so publish is where required fields, bounds, formats, option
147
+ * membership, and row counts are finally enforced. It walks the field
148
+ * *definitions*, not just the bag, so a required field ABSENT from the bag
149
+ * is caught as well as one stored empty — `runFieldPipeline` sees
150
+ * `undefined` and rejects it in strict mode.
151
+ *
152
+ * Rejections aggregate across the bag into one `MetaValidationError` the
153
+ * RPC layer maps onto the admin inputs, blocking the publish. A passing bag
154
+ * comes back canonicalized (each registered value re-run through
155
+ * `.sanitize()`); a conditionally-hidden field can't be required, so it's
156
+ * skipped; keys the field system doesn't own (e.g. from an uninstalled
157
+ * plugin) pass through untouched, matching the read path. Field
158
+ * capabilities and reference existence are not re-checked here — a
159
+ * whole-bag gate would block a publisher over a co-author's field, and both
160
+ * were already enforced at write time.
161
+ */
162
+ export async function validateAndPromoteMetaBag(fields, bag) {
163
+ const out = {};
164
+ const owned = new Set();
165
+ const fieldErrors = [];
166
+ for (const field of fields) {
167
+ owned.add(field.key);
168
+ // A hidden field is inactive, so it can't be required — but its stored
169
+ // value is kept untouched (not validated, not dropped), or the value a
170
+ // driver hides would be lost on publish and gone when the driver flips
171
+ // back.
172
+ if (isConditionHidden(field, bag)) {
173
+ if (field.key in bag)
174
+ out[field.key] = bag[field.key];
175
+ continue;
176
+ }
177
+ const result = await runFieldPipeline(field, bag[field.key], field.key);
178
+ if (result.errors.length > 0) {
179
+ fieldErrors.push(...result.errors);
180
+ continue;
181
+ }
182
+ if (result.isDeletion === true)
183
+ continue;
184
+ out[field.key] = result.value;
185
+ }
186
+ for (const [key, value] of Object.entries(bag)) {
187
+ if (!owned.has(key))
188
+ out[key] = value;
189
+ }
190
+ if (fieldErrors.length > 0)
191
+ throw new MetaValidationError(fieldErrors);
192
+ return out;
193
+ }
107
194
  /**
108
195
  * Walk a sanitized patch, group every reference upsert by
109
196
  * `(kind, scope)`, and issue one `LookupAdapter.list({ ids })`
@@ -118,7 +205,7 @@ export function sanitizeMetaForRpc(findField, input, errors) {
118
205
  * TOCTOU note: validate runs in a separate query from the eventual
119
206
  * `applyMetaPatch`, and callers don't share a transaction. A
120
207
  * concurrent delete between validate and apply leaves an orphan id
121
- * in the meta bag; `filterMetaOrphans` masks it on read. Wrap the
208
+ * in the meta bag; `hydrateMetaBags` masks it on read. Wrap the
122
209
  * validate/apply pair in `ctx.db.transaction()` if a caller needs
123
210
  * serializable consistency.
124
211
  */
@@ -133,58 +220,41 @@ export async function validateMetaReferences(ctx, findField, patch) {
133
220
  throw MetaSanitizationError.invalidValue({ key });
134
221
  }
135
222
  const ids = referenceIdsForValidation(key, value, target, fieldMax(field));
223
+ // Normalize eagerly — a validation failure below aborts the whole
224
+ // save, so a rewritten patch never persists on the failure path.
225
+ patch.upserts.set(key, target.multiple ? ids : ids[0]);
136
226
  const group = upsertGroup(groups, target, registered);
137
227
  for (const id of ids)
138
228
  group.ids.add(id);
139
- group.contributions.push({
140
- errorKey: key,
141
- ids,
142
- applyNormalize: (rowsById) => {
143
- if (target.valueShape !== "object")
144
- return;
145
- if (target.multiple) {
146
- patch.upserts.set(key, ids.map((id) => buildCachedReference(id, rowsById)));
147
- return;
148
- }
149
- const [singleId] = ids;
150
- if (singleId !== undefined) {
151
- patch.upserts.set(key, buildCachedReference(singleId, rowsById));
152
- }
153
- },
154
- });
229
+ group.contributions.push({ errorKey: key, ids });
155
230
  continue;
156
231
  }
157
- // Repeater fields don't carry a `referenceTarget` themselves but
158
- // their rows can. Walk into rows so nested `entry` / `term` /
159
- // `user` / `media` refs flow through the same `(kind, scope)`
160
- // batch as top-level fields. Errors attribute to the top-level
161
- // repeater key the row index + subKey live in the developer
162
- // log, not the wire response (per slice acceptance).
163
- if (!isRepeaterField(field))
164
- continue;
165
- if (!Array.isArray(value))
166
- continue;
167
- collectRepeaterReferences(ctx, key, field, value, groups);
232
+ // Composite fields (repeater / group) don't carry a
233
+ // `referenceTarget` themselves but their rows / members can, at any
234
+ // nesting depth. Walk them so nested `entry` / `term` / `user` /
235
+ // `media` refs flow through the same `(kind, scope)` batch as
236
+ // top-level fields. Errors attribute to the top-level key the
237
+ // dotted sub-path lives in the developer log, not the wire response.
238
+ if (isRepeaterField(field) || isGroupField(field)) {
239
+ collectCompositeReferences(ctx, key, key, field, value, groups);
240
+ }
168
241
  }
169
242
  for (const group of groups.values()) {
170
243
  if (group.ids.size === 0)
171
244
  continue;
172
- const liveRows = await fetchLiveRows(ctx, group.registered, group.scope, group.ids, "validateMetaReferences");
173
- const rowsById = new Map(liveRows.map((r) => [r.id, r]));
245
+ const liveIds = await fetchLiveIds(ctx, group.registered, group.scope, group.ids, "validateMetaReferences");
174
246
  for (const contribution of group.contributions) {
175
247
  for (const id of contribution.ids) {
176
- if (!rowsById.has(id)) {
248
+ if (!liveIds.has(id)) {
177
249
  if (contribution.diagnostic !== undefined) {
178
250
  // Wire error keys on the top-level field; this log line
179
- // is the only place the row index + subKey surface, so
180
- // an engineer debugging a `meta_invalid_value` on a
181
- // repeater can locate the offending subField without
182
- // bisecting the saved bag. `JSON.stringify(id)` escapes
183
- // control characters so a user-supplied id with newlines
184
- // can't poison the log stream.
185
- console.error(`[plumix] meta repeater ${JSON.stringify(contribution.errorKey)} ` +
186
- `row ${String(contribution.diagnostic.rowIdx)} ` +
187
- `subField ${JSON.stringify(contribution.diagnostic.subKey)} ` +
251
+ // is the only place the nested sub-path surfaces, so an
252
+ // engineer debugging a `meta_invalid_value` on a composite
253
+ // field can locate the offending cell without bisecting the
254
+ // saved bag. `JSON.stringify` escapes control characters so
255
+ // a user-supplied id with newlines can't poison the log.
256
+ console.error(`[plumix] meta composite ${JSON.stringify(contribution.errorKey)} ` +
257
+ `at ${JSON.stringify(contribution.diagnostic.path)} ` +
188
258
  `references missing id ${JSON.stringify(id)}`);
189
259
  }
190
260
  throw MetaSanitizationError.invalidValue({
@@ -192,61 +262,60 @@ export async function validateMetaReferences(ctx, findField, patch) {
192
262
  });
193
263
  }
194
264
  }
195
- contribution.applyNormalize(rowsById);
196
265
  }
197
266
  }
198
267
  }
199
- function isRepeaterField(field) {
200
- if (!field)
201
- return false;
202
- return field.inputType === "repeater";
203
- }
204
- function collectRepeaterReferences(ctx, topKey, field, rows, groups) {
205
- // Nested repeaters are rejected at registration by `repeater()`, so
206
- // any subField that's itself a repeater wouldn't get here in
207
- // practice. We don't recurse into subField repeaters either way —
208
- // single-source-of-truth on the registration guard.
209
- for (const [rowIdx, row] of rows.entries()) {
210
- if (!row || typeof row !== "object" || Array.isArray(row))
211
- continue;
212
- const rowObj = row;
213
- for (const subField of field.subFields) {
214
- const target = referenceTargetOf(subField);
215
- if (!target)
268
+ // Walk a composite field's live value, collecting + normalizing every
269
+ // nested reference in place. `path` is the dotted address of `field`
270
+ // from the top-level key (developer-log only). Recurses through nested
271
+ // repeaters and groups, so a reference at any depth flows through the
272
+ // same `(kind, scope)` batch and is orphan-checked at write time.
273
+ function collectCompositeReferences(ctx, topKey, path, field, value, groups) {
274
+ if (isRepeaterField(field)) {
275
+ if (!Array.isArray(value))
276
+ return;
277
+ for (const [rowIdx, row] of value.entries()) {
278
+ if (!isPlainObject(row))
216
279
  continue;
217
- const subValue = rowObj[subField.key];
280
+ collectMemberReferences(ctx, topKey, `${path}.${String(rowIdx)}`, field.subFields, row, groups);
281
+ }
282
+ return;
283
+ }
284
+ if (isGroupField(field)) {
285
+ if (!isPlainObject(value))
286
+ return;
287
+ collectMemberReferences(ctx, topKey, path, field.fields, value, groups);
288
+ }
289
+ }
290
+ function collectMemberReferences(ctx, topKey, path, members, container, groups) {
291
+ for (const member of members) {
292
+ const memberPath = `${path}.${member.key}`;
293
+ const subValue = container[member.key];
294
+ const target = referenceTargetOf(member);
295
+ if (target) {
218
296
  if (subValue === undefined || subValue === null)
219
297
  continue;
220
298
  const registered = ctx.plugins.lookupAdapters.get(target.kind);
221
299
  if (!registered) {
222
300
  throw MetaSanitizationError.invalidValue({ key: topKey });
223
301
  }
224
- const ids = referenceIdsForValidation(topKey, subValue, target, fieldMax(subField));
302
+ const ids = referenceIdsForValidation(topKey, subValue, target, fieldMax(member));
303
+ // Normalize the cell in place — the top-level upsert value is the
304
+ // live storage shape, so mutating an object inside it is what the
305
+ // caller serializes.
306
+ container[member.key] = target.multiple ? ids : ids[0];
225
307
  const group = upsertGroup(groups, target, registered);
226
308
  for (const id of ids)
227
309
  group.ids.add(id);
228
- const subKey = subField.key;
229
310
  group.contributions.push({
230
311
  errorKey: topKey,
231
312
  ids,
232
- diagnostic: { rowIdx, subKey },
233
- applyNormalize: (rowsById) => {
234
- if (target.valueShape !== "object")
235
- return;
236
- // Rewrite the row's cached-object value in place — the
237
- // top-level upsert array is the live storage shape, so
238
- // mutating an object inside it is what the caller will
239
- // serialize.
240
- if (target.multiple) {
241
- rowObj[subKey] = ids.map((id) => buildCachedReference(id, rowsById));
242
- return;
243
- }
244
- const [singleId] = ids;
245
- if (singleId !== undefined) {
246
- rowObj[subKey] = buildCachedReference(singleId, rowsById);
247
- }
248
- },
313
+ diagnostic: { path: memberPath },
249
314
  });
315
+ continue;
316
+ }
317
+ if (isRepeaterField(member) || isGroupField(member)) {
318
+ collectCompositeReferences(ctx, topKey, memberPath, member, subValue, groups);
250
319
  }
251
320
  }
252
321
  }
@@ -264,13 +333,6 @@ function upsertGroup(groups, target, registered) {
264
333
  }
265
334
  return group;
266
335
  }
267
- // Adapter cached fields override anything; user-supplied non-id keys
268
- // don't survive. Per-usage user fields (e.g. alt overrides) are a
269
- // post-v0.1 feature — when added, this is where they'd merge in.
270
- function buildCachedReference(id, rowsById) {
271
- const row = rowsById.get(id);
272
- return { id, ...(row?.cached ?? {}) };
273
- }
274
336
  // Defense-in-depth ceiling on the internal-aggregated id-batch size.
275
337
  // Above this, the live-id fetch throws rather than silently truncating.
276
338
  // The wire cap (100) and per-field cap (`HARD_MULTI_REFERENCE_LIMIT`,
@@ -305,20 +367,17 @@ function referenceGroupKey(target) {
305
367
  // 100, so we only hit the ceiling when many fields share `(kind, scope)`
306
368
  // and aggregate — at which point throwing beats silent truncation
307
369
  // (truncation would either reject valid writes or hide live targets).
308
- //
309
- // Returns `LookupResult[]` rather than just live ids so the validator
310
- // can read `cached` for the cached-object normalize step. Callers that
311
- // only care about presence build a `Set` from the row ids.
312
- async function fetchLiveRows(ctx, registered, scope, ids, callsite) {
370
+ async function fetchLiveIds(ctx, registered, scope, ids, callsite) {
313
371
  if (ids.size > MAX_REFERENCE_GROUP_BATCH) {
314
372
  throw MetaReferenceError.batchSizeExceeded(callsite, ids.size, MAX_REFERENCE_GROUP_BATCH);
315
373
  }
316
374
  const idList = [...ids];
317
- return registered.adapter.list(ctx, {
375
+ const rows = await registered.adapter.list(ctx, {
318
376
  ids: idList,
319
377
  scope,
320
378
  limit: idList.length,
321
379
  });
380
+ return new Set(rows.map((row) => row.id));
322
381
  }
323
382
  // Defensive upper bound on multi-reference array length, applied
324
383
  // per-field before grouping. Even with the batched `list({ ids })`
@@ -328,18 +387,11 @@ async function fetchLiveRows(ctx, registered, scope, ids, callsite) {
328
387
  // can declare a lower `max` and the validator picks the smaller.
329
388
  const HARD_MULTI_REFERENCE_LIMIT = 100;
330
389
  // Validates the wire shape of a reference value and returns the ids
331
- // to feed into the group's batched `list({ ids })` call. Four shapes
332
- // are accepted, dispatching on `target.multiple` + `target.valueShape`:
333
- //
334
- // - multi=true, valueShape="id" (default): string[] of non-empty ids
335
- // - multi=true, valueShape="object": `{ id }[]` — each item a
336
- // cached-object reference
337
- // (or bare strings for
338
- // leniency on first write)
339
- // - multi=false, valueShape="id" (default): bare string id
340
- // - multi=false, valueShape="object": `{ id: string, ... }`
341
- // (or a bare string for
342
- // leniency on first write)
390
+ // to feed into the group's batched `list({ ids })` call. Storage is
391
+ // plain ids a bare string (single) or string[] (multi) — but each
392
+ // slot leniently accepts the retired cached-object shape
393
+ // (`{ id, ... }`) so legacy values self-heal to the plain form on
394
+ // the entity's next save.
343
395
  function referenceIdsForValidation(key, value, target, max) {
344
396
  if (target.multiple) {
345
397
  if (!Array.isArray(value)) {
@@ -351,56 +403,17 @@ function referenceIdsForValidation(key, value, target, max) {
351
403
  if (max !== undefined && value.length > max) {
352
404
  throw MetaSanitizationError.invalidValue({ key });
353
405
  }
354
- if (target.valueShape === "object") {
355
- // Lenient on input: each item can be `"42"` (first-write
356
- // convenience) or `{ id: "42", ... }`. Always rewritten to the
357
- // canonical `{ id, ...cached }` shape after the live-id check.
358
- const ids = [];
359
- for (const item of value) {
360
- if (typeof item === "string" && item !== "") {
361
- ids.push(item);
362
- continue;
363
- }
364
- const id = extractStringId(item);
365
- if (id !== null && id !== "") {
366
- ids.push(id);
367
- continue;
368
- }
369
- throw MetaSanitizationError.invalidValue({ key });
370
- }
371
- return ids;
372
- }
373
- for (const item of value) {
374
- if (typeof item !== "string" || item === "") {
375
- throw MetaSanitizationError.invalidValue({ key });
376
- }
377
- }
378
- return value;
406
+ return value.map((item) => referenceItemId(key, item));
379
407
  }
380
- if (target.valueShape === "object") {
381
- // Lenient on input: accept either `"42"` (first-write convenience)
382
- // or `{ id: "42", ... }`. Always rewritten to the canonical
383
- // `{ id, ...cached }` shape after the live-id check.
384
- if (typeof value === "string" && value !== "")
385
- return [value];
386
- const id = extractStringId(value);
387
- if (id !== null && id !== "")
388
- return [id];
389
- throw MetaSanitizationError.invalidValue({ key });
390
- }
391
- if (typeof value !== "string") {
392
- throw MetaSanitizationError.invalidValue({ key });
393
- }
394
- return [value];
395
- }
396
- // Returns the `id` string of a `{ id: string, ... }` object, or null
397
- // for any other shape (string, array, null, primitive, missing key).
398
- function extractStringId(value) {
399
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
400
- return null;
401
- }
402
- const id = value.id;
403
- return typeof id === "string" ? id : null;
408
+ return [referenceItemId(key, value)];
409
+ }
410
+ function referenceItemId(key, item) {
411
+ if (typeof item === "string" && item !== "")
412
+ return item;
413
+ const id = extractStringId(item);
414
+ if (id !== null && id !== "")
415
+ return id;
416
+ throw MetaSanitizationError.invalidValue({ key });
404
417
  }
405
418
  function fieldMax(field) {
406
419
  return field?.max;
@@ -418,201 +431,318 @@ export async function validateMetaReferencesForRpc(ctx, findField, patch, errors
418
431
  catch (error) {
419
432
  if (error instanceof MetaSanitizationError) {
420
433
  throw errors.CONFLICT({
421
- data: { reason: `meta_${error.reason}`, key: error.key },
434
+ data: {
435
+ reason: `meta_${error.reason}`,
436
+ key: error.key,
437
+ // Reference failures address the top-level field (row/subKey
438
+ // detail stays in the server log) — shipping them under
439
+ // `errors` too lets the admin form surface them inline.
440
+ errors: [
441
+ { path: error.key, message: META_FIELD_MESSAGES.invalidOption },
442
+ ],
443
+ },
422
444
  });
423
445
  }
424
446
  throw error;
425
447
  }
426
448
  }
449
+ // A reference field authored with `.returns("id")` reads the bare
450
+ // stored id — the hydration walk skips it so no lookup query runs and
451
+ // the id survives untouched. `"returns" in field` narrows to the field
452
+ // variants that carry the flag (temporal's `"date"` never matches).
453
+ function readsRawReferenceId(field) {
454
+ return field !== undefined && "returns" in field && field.returns === "id";
455
+ }
427
456
  /**
428
457
  * Yield every reference-field occurrence in a decoded meta bag: top-level
429
- * reference fields, plus reference subFields inside each repeater row. One
430
- * structural walk so the orphan-strip pass doesn't reinline the
431
- * top-level/repeater traversal twice.
458
+ * reference fields, plus references nested inside repeater rows and
459
+ * groups at any depth. One structural walk so the hydration pass doesn't
460
+ * reinline the traversal twice.
432
461
  */
433
462
  function* referenceOccurrences(entries, findField) {
434
463
  for (const [key, value] of entries) {
435
- const field = findField(key);
436
- const target = referenceTargetOf(field);
437
- if (target) {
438
- yield { key, target, value };
439
- continue;
440
- }
441
- if (!isRepeaterField(field))
442
- continue;
464
+ yield* fieldOccurrences([key], findField(key), value);
465
+ }
466
+ }
467
+ // Recurse a single field's decoded value, yielding reference occurrences
468
+ // with their full path. Nested field definitions come straight off the
469
+ // composite field (`subFields` / `fields`) — `findField` only resolves
470
+ // top-level keys.
471
+ function* fieldOccurrences(path, field, value) {
472
+ const target = referenceTargetOf(field);
473
+ if (target) {
474
+ // `.returns("id")` opts out of the hydration join at any depth —
475
+ // leave the stored id(s) untouched (no resolve, no orphan-strip).
476
+ if (!readsRawReferenceId(field))
477
+ yield { path, target, value };
478
+ return;
479
+ }
480
+ if (isRepeaterField(field)) {
443
481
  if (!Array.isArray(value))
444
- continue;
482
+ return;
445
483
  for (const [rowIdx, row] of value.entries()) {
446
- if (!row || typeof row !== "object" || Array.isArray(row))
484
+ if (!isPlainObject(row))
447
485
  continue;
448
- const rowObj = row;
449
486
  for (const subField of field.subFields) {
450
- const subTarget = referenceTargetOf(subField);
451
- if (!subTarget)
452
- continue;
453
- yield {
454
- key,
455
- target: subTarget,
456
- value: rowObj[subField.key],
457
- nested: { rowIdx, subKey: subField.key },
458
- };
487
+ yield* fieldOccurrences([...path, rowIdx, subField.key], subField, row[subField.key]);
459
488
  }
460
489
  }
490
+ return;
491
+ }
492
+ if (isGroupField(field)) {
493
+ if (!isPlainObject(value))
494
+ return;
495
+ for (const member of field.fields) {
496
+ yield* fieldOccurrences([...path, member.key], member, value[member.key]);
497
+ }
461
498
  }
462
499
  }
463
- export async function filterMetaOrphans(ctx, findField, decoded) {
500
+ /** Single-bag convenience over {@link hydrateMetaBags}. */
501
+ export async function hydrateMetaReferences(ctx, findField, decoded) {
502
+ const [bag] = await hydrateMetaBags(ctx, [{ findField, decoded }]);
503
+ // hydrateMetaBags returns one bag per input by construction.
504
+ return bag ?? decoded;
505
+ }
506
+ export async function hydrateMetaBags(ctx, bags) {
464
507
  const candidates = [];
465
508
  const groups = new Map();
466
- // Top-level refs and repeater-row subField refs get identical orphan-strip
467
- // treatment, so the single `referenceOccurrences` walk feeds both. Nested
468
- // candidates carry their `rowIdx`/`subKey`; Pass 3 rewrites the row slot in
469
- // a per-key clone so the caller's `decoded` bag stays untouched.
470
- for (const occ of referenceOccurrences(Object.entries(decoded), findField)) {
471
- const registered = ctx.plugins.lookupAdapters.get(occ.target.kind);
472
- if (!registered)
473
- continue;
474
- const ids = orphanCandidateIds(occ.target, occ.value);
475
- if (ids === null)
476
- continue; // non-array multi / non-string single — leave untouched
477
- const groupKey = referenceGroupKey(occ.target);
478
- candidates.push({
479
- key: occ.key,
480
- multiple: occ.target.multiple === true,
481
- valueShape: occ.target.valueShape ?? "id",
482
- groupKey,
483
- ids,
484
- nested: occ.nested,
485
- });
486
- if (ids.length === 0)
487
- continue;
488
- let group = groups.get(groupKey);
489
- if (!group) {
490
- group = { registered, scope: occ.target.scope, ids: new Set() };
491
- groups.set(groupKey, group);
492
- }
493
- for (const id of ids)
494
- group.ids.add(id);
495
- }
496
- // Pass 2: one `list({ ids })` per group, keyed for O(1) apply lookup.
497
- const liveIdsByGroup = new Map();
498
- for (const [groupKey, group] of groups) {
499
- const rows = await fetchLiveRows(ctx, group.registered, group.scope, group.ids, "filterMetaOrphans");
500
- liveIdsByGroup.set(groupKey, new Set(rows.map((row) => row.id)));
501
- }
502
- // Pass 3: apply the filter. Orphan handling differs by shape:
503
- // - multi + id: filter the string[] in place, dropping missing
504
- // - multi + object: filter the object[] preserving order
505
- // - single + id: null on missing
506
- // - single + object: null on missing; otherwise keep the stored
507
- // cached value as-is (no read-time refresh — write rewrites)
508
- // Nested-in-repeater candidates rewrite the row's subField slot in
509
- // a per-key cloned array so the input bag stays untouched.
510
- const out = { ...decoded };
511
- const empty = new Set();
512
- for (const candidate of candidates) {
513
- const { key, multiple, valueShape, groupKey, ids, nested } = candidate;
514
- const liveIds = liveIdsByGroup.get(groupKey) ?? empty;
515
- if (nested !== undefined) {
516
- const rowObj = takeWritableRow(out, decoded, key, nested.rowIdx);
517
- if (!rowObj)
509
+ // Top-level refs and nested (repeater / group) refs get identical
510
+ // treatment, so the single `referenceOccurrences` walk feeds both.
511
+ // Every candidate carries its full path; Pass 3 rewrites the slot in a
512
+ // per-path copy-on-write so callers' bags stay untouched.
513
+ const out = [];
514
+ for (const bag of bags) {
515
+ const outBag = { ...bag.decoded };
516
+ out.push(outBag);
517
+ for (const occ of referenceOccurrences(Object.entries(bag.decoded), bag.findField)) {
518
+ const registered = ctx.plugins.lookupAdapters.get(occ.target.kind);
519
+ if (!registered)
518
520
  continue;
519
- applyOrphanToSlot(rowObj, nested.subKey, multiple, valueShape, ids, liveIds);
520
- continue;
521
- }
522
- if (multiple) {
523
- if (valueShape === "object") {
524
- const stored = decoded[key];
525
- out[key] = stored.filter((item) => {
526
- const id = extractStringId(item);
527
- return id !== null && liveIds.has(id);
528
- });
521
+ const ids = referenceCandidateIds(occ.target, occ.value);
522
+ if (ids === null)
523
+ continue; // non-array multi / non-string single — leave untouched
524
+ const groupKey = referenceGroupKey(occ.target);
525
+ candidates.push({
526
+ outBag,
527
+ decoded: bag.decoded,
528
+ path: occ.path,
529
+ multiple: occ.target.multiple === true,
530
+ groupKey,
531
+ ids,
532
+ });
533
+ if (ids.length === 0)
529
534
  continue;
535
+ let group = groups.get(groupKey);
536
+ if (!group) {
537
+ group = { registered, scope: occ.target.scope, ids: new Set() };
538
+ groups.set(groupKey, group);
530
539
  }
531
- out[key] = ids.filter((id) => liveIds.has(id));
532
- continue;
540
+ for (const id of ids)
541
+ group.ids.add(id);
533
542
  }
534
- const [singleId] = ids;
535
- if (singleId === undefined)
536
- continue; // single non-string already filtered upstream
537
- if (!liveIds.has(singleId))
538
- out[key] = null;
543
+ }
544
+ // Pass 2: one `hydrate({ ids })` (or `list({ ids })` fallback) per
545
+ // group, keyed for O(1) apply lookup. Groups are independent, so a
546
+ // bag mixing entry + user + media refs resolves them concurrently.
547
+ const resolutions = new Map(await Promise.all([...groups].map(async ([groupKey, group]) => [
548
+ groupKey,
549
+ await resolveGroup(ctx, group.registered.adapter, group.scope, group.ids),
550
+ ])));
551
+ // Pass 3: apply. Hydrated groups replace ids with their payloads;
552
+ // id-only groups keep ids. Multi refs stay dense and in stored
553
+ // order; single refs null on missing. Nested candidates copy-on-write
554
+ // each container down their path so callers' bags stay untouched.
555
+ const emptyResolution = { kind: "ids", liveIds: new Set() };
556
+ for (const candidate of candidates) {
557
+ const { outBag, decoded, path, multiple, groupKey, ids } = candidate;
558
+ const resolution = resolutions.get(groupKey) ?? emptyResolution;
559
+ const slot = takeWritableSlot(outBag, decoded, path);
560
+ if (!slot)
561
+ continue;
562
+ applyResolutionToSlot(slot.parent, slot.leafKey, multiple, ids, resolution);
539
563
  }
540
564
  return out;
541
565
  }
542
- function takeWritableRow(out, decoded, key, rowIdx) {
543
- // Lazily clone the rows array (and the targeted row inside it) on
544
- // first nested write so the caller's `decoded` bag stays untouched.
545
- // Subsequent nested writes hit the same cloned array.
546
- let arr = out[key];
547
- if (arr === decoded[key]) {
548
- if (!Array.isArray(arr))
549
- return null;
550
- const cloned = arr.map((row) => row && typeof row === "object" && !Array.isArray(row)
551
- ? { ...row }
552
- : row);
553
- out[key] = cloned;
554
- arr = cloned;
566
+ /**
567
+ * Batched, tag-accounted hydration of a raw id set for one reference kind
568
+ * the theme-facing counterpart to the meta pipeline's hydration (#1508).
569
+ * A theme holding an id-only reference field (a field declared
570
+ * `.returns("id")`, or one contributed by a third-party plugin) resolves
571
+ * it here instead of hand-rolled per-item fetches: ids resolve through the
572
+ * adapter's batched `hydrate` (chunked, one in-query per chunk) and every
573
+ * resolved entity folds its cache tag into the page through the same
574
+ * accumulator the meta pipeline uses, so the page is purged when an
575
+ * embedded entity changes.
576
+ *
577
+ * Returns the hydrated payloads dense and in the requested id order — ids
578
+ * that are gone or out of scope are dropped, mirroring multi-reference
579
+ * field hydration. An unregistered kind, an adapter without the `hydrate`
580
+ * contract, or an empty id set yields `[]`.
581
+ */
582
+ export async function hydrateReferences(ctx, kind, ids, options = {}) {
583
+ const registered = ctx.plugins.lookupAdapters.get(kind);
584
+ if (!registered?.adapter.hydrate)
585
+ return [];
586
+ const unique = new Set(ids.filter((id) => id !== ""));
587
+ if (unique.size === 0)
588
+ return [];
589
+ const resolution = await resolveGroup(ctx, registered.adapter, options.scope, unique);
590
+ if (resolution.kind !== "hydrated")
591
+ return [];
592
+ return ids
593
+ .map((id) => resolution.byId.get(id))
594
+ .filter((p) => p !== undefined);
595
+ }
596
+ // Resolve one `(kind, scope)` group's aggregated ids. Chunked at
597
+ // `HYDRATION_QUERY_ID_LIMIT` per in-query: a response-level group can
598
+ // legitimately aggregate more ids than one query may carry (a
599
+ // 100-entry archive × multi-reference fields), and a read-path throw
600
+ // would kill the render — unlike the write-side `fetchLiveIds`, which
601
+ // keeps throwing because a single patch exceeding the ceiling is a
602
+ // caller bug. Ids are de-duped before chunking, so per-query batches
603
+ // stay bounded and nothing is truncated.
604
+ async function resolveGroup(ctx, adapter, scope, ids) {
605
+ const idList = [...ids];
606
+ if (adapter.hydrate) {
607
+ const byId = new Map();
608
+ for (const chunk of chunkIds(idList)) {
609
+ const payloads = await adapter.hydrate(ctx, { ids: chunk, scope });
610
+ for (const payload of payloads) {
611
+ byId.set(payload.id, payload);
612
+ // Fold this embedded entity's cache tag into the page's tags so
613
+ // a change to it purges the page that hydrated it (#1508). Runs
614
+ // on every read surface; only the public read-through reads the
615
+ // accumulator back, so admin/REST reads populate it harmlessly.
616
+ if (adapter.embeddedCacheTags) {
617
+ accumulateEmbeddedTags(ctx, adapter.embeddedCacheTags(payload));
618
+ }
619
+ }
620
+ }
621
+ return { kind: "hydrated", byId };
622
+ }
623
+ const liveIds = new Set();
624
+ for (const chunk of chunkIds(idList)) {
625
+ const rows = await adapter.list(ctx, {
626
+ ids: chunk,
627
+ scope,
628
+ limit: chunk.length,
629
+ });
630
+ for (const row of rows)
631
+ liveIds.add(row.id);
632
+ }
633
+ return { kind: "ids", liveIds };
634
+ }
635
+ // Per-query id cap for the read path. 100 (not the 1000 aggregate
636
+ // ceiling) because Cloudflare D1 caps bound parameters at 100 per
637
+ // statement and `inArray` binds one per id — a bigger chunk works on
638
+ // local SQLite and dies in production.
639
+ const HYDRATION_QUERY_ID_LIMIT = 100;
640
+ function* chunkIds(ids) {
641
+ for (let i = 0; i < ids.length; i += HYDRATION_QUERY_ID_LIMIT) {
642
+ yield ids.slice(i, i + HYDRATION_QUERY_ID_LIMIT);
555
643
  }
556
- if (!Array.isArray(arr))
557
- return null;
558
- const row = arr[rowIdx];
559
- if (!row || typeof row !== "object" || Array.isArray(row))
560
- return null;
561
- return row;
562
644
  }
563
- function applyOrphanToSlot(rowObj, subKey, multiple, valueShape, ids, liveIds) {
645
+ // Write one candidate's resolved value into its slot — the leaf object
646
+ // key of a container reached by walking the candidate's path.
647
+ function applyResolutionToSlot(slot, key, multiple, ids, resolution) {
564
648
  if (multiple) {
565
- if (valueShape === "object") {
566
- const stored = rowObj[subKey];
567
- rowObj[subKey] = stored.filter((item) => {
568
- const id = extractStringId(item);
569
- return id !== null && liveIds.has(id);
570
- });
571
- return;
572
- }
573
- rowObj[subKey] = ids.filter((id) => liveIds.has(id));
649
+ slot[key] =
650
+ resolution.kind === "hydrated"
651
+ ? ids
652
+ .map((id) => resolution.byId.get(id))
653
+ .filter((payload) => payload !== undefined)
654
+ : ids.filter((id) => resolution.liveIds.has(id));
574
655
  return;
575
656
  }
576
657
  const [singleId] = ids;
577
658
  if (singleId === undefined)
659
+ return; // single non-string already filtered upstream
660
+ if (resolution.kind === "hydrated") {
661
+ slot[key] = resolution.byId.get(singleId) ?? null;
662
+ return;
663
+ }
664
+ if (!resolution.liveIds.has(singleId))
665
+ slot[key] = null;
666
+ }
667
+ // Walk the candidate's path from the output bag to the parent container
668
+ // of its leaf slot, copy-on-writing each container the first time it's
669
+ // descended so the caller's `decoded` bag stays untouched. Containers
670
+ // cloned by an earlier candidate (identity no longer matches `decoded`)
671
+ // are reused, so sibling references in the same row/group land in one
672
+ // clone. Returns the writable parent object and the leaf key, or null if
673
+ // any segment's runtime shape doesn't match the declared structure
674
+ // (hand-edited / migrated bags).
675
+ function takeWritableSlot(outBag, decoded, path) {
676
+ let outContainer = outBag;
677
+ let decContainer = decoded;
678
+ for (let i = 0; i < path.length - 1; i++) {
679
+ const seg = path[i];
680
+ if (seg === undefined)
681
+ return null;
682
+ const outChild = readSegment(outContainer, seg);
683
+ const decChild = readSegment(decContainer, seg);
684
+ let writable = outChild;
685
+ // Still the shared decoded child → clone it into the output tree.
686
+ if (outChild === decChild) {
687
+ const clone = cloneContainer(outChild);
688
+ if (clone === null)
689
+ return null;
690
+ writeSegment(outContainer, seg, clone);
691
+ writable = clone;
692
+ }
693
+ outContainer = writable;
694
+ decContainer = decChild;
695
+ }
696
+ const leaf = path[path.length - 1];
697
+ if (typeof leaf !== "string")
698
+ return null;
699
+ if (!isPlainObject(outContainer))
700
+ return null;
701
+ return { parent: outContainer, leafKey: leaf };
702
+ }
703
+ function readSegment(container, seg) {
704
+ if (typeof seg === "number") {
705
+ return Array.isArray(container) ? container[seg] : undefined;
706
+ }
707
+ return isPlainObject(container) ? container[seg] : undefined;
708
+ }
709
+ function writeSegment(container, seg, value) {
710
+ if (typeof seg === "number") {
711
+ if (Array.isArray(container))
712
+ container[seg] = value;
578
713
  return;
579
- if (!liveIds.has(singleId))
580
- rowObj[subKey] = null;
714
+ }
715
+ if (isPlainObject(container))
716
+ container[seg] = value;
717
+ }
718
+ // Shallow clone an array or plain object; null for any other shape (the
719
+ // path expected a container but the stored value isn't one).
720
+ function cloneContainer(value) {
721
+ // `Array.isArray` widens to `any[]`; cast before spread so the clone
722
+ // stays `unknown[]` rather than leaking `any` into the walk.
723
+ if (Array.isArray(value))
724
+ return [...value];
725
+ if (isPlainObject(value))
726
+ return { ...value };
727
+ return null;
728
+ }
729
+ function isPlainObject(value) {
730
+ return typeof value === "object" && value !== null && !Array.isArray(value);
581
731
  }
582
732
  // Returns `null` to mean "no candidates from this key" (skip), or
583
733
  // the ids to feed into the group's batch query. Mirrors the storage-
584
734
  // shape guards in the apply step so we don't enqueue work that's
585
- // going to be skipped. Cached-object values (`valueShape === "object"`)
586
- // extract `id` from the object the rest of the cached fields are
587
- // trusted on read (refresh happens on next write via the validator's
588
- // normalize step, not on every render).
589
- function orphanCandidateIds(target, value) {
735
+ // going to be skipped. Callers pass values already decoded through
736
+ // `decodeMetaBag`, so legacy object shapes have been healed to plain
737
+ // ids by the time this runs.
738
+ function referenceCandidateIds(target, value) {
590
739
  if (target.multiple) {
591
740
  if (!Array.isArray(value))
592
741
  return null;
593
- if (target.valueShape === "object") {
594
- const ids = [];
595
- for (const item of value) {
596
- const id = extractStringId(item);
597
- if (id !== null)
598
- ids.push(id);
599
- }
600
- return ids;
601
- }
602
742
  return value.filter((id) => typeof id === "string");
603
743
  }
604
- if (target.valueShape === "object") {
605
- const id = extractStringId(value);
606
- return id !== null ? [id] : null;
607
- }
608
744
  return typeof value === "string" ? [value] : null;
609
745
  }
610
- function referenceTargetOf(field) {
611
- if (!field)
612
- return undefined;
613
- return field
614
- .referenceTarget;
615
- }
616
746
  /**
617
747
  * Decode a raw meta bag (as returned by drizzle's JSON-mode column)
618
748
  * into the plugin-typed shape RPC consumers expect. Unregistered keys
@@ -626,10 +756,64 @@ export function decodeMetaBag(findField, raw) {
626
756
  const out = {};
627
757
  for (const [key, value] of Object.entries(raw)) {
628
758
  const field = findField(key);
629
- out[key] = field ? coerceOnRead(field.type, value) : value;
759
+ out[key] = field ? decodeFieldValue(field, value) : value;
630
760
  }
631
761
  return out;
632
762
  }
763
+ // Reference storage is plain ids, but bags written before the
764
+ // write-time snapshot machinery was removed may hold `{ id, ... }`
765
+ // objects. Reads yield the id; the next save persists the plain form
766
+ // (`referenceItemId` accepts the legacy shape on write).
767
+ function decodeFieldValue(field, value) {
768
+ const target = referenceTargetOf(field);
769
+ if (target)
770
+ return healReferenceValue(target, value);
771
+ if (isRepeaterField(field) && Array.isArray(value)) {
772
+ return value.map((row) => healRepeaterRow(field, row));
773
+ }
774
+ if (isTemporalField(field) && field.returns === "date") {
775
+ return projectTemporalDate(field.inputType, value);
776
+ }
777
+ return coerceOnRead(field.type, value);
778
+ }
779
+ function isTemporalField(field) {
780
+ return (field.inputType === "date" ||
781
+ field.inputType === "datetime" ||
782
+ field.inputType === "time");
783
+ }
784
+ // `.returns("date")` decode projection. All three variants anchor to
785
+ // UTC — `date` at UTC midnight, `time` on 1970-01-01 UTC — so the
786
+ // wall-clock components survive every server/browser timezone
787
+ // combination (decode runs server-side, often UTC on Workers, while
788
+ // the admin formats in the viewer's browser). Consumers read the
789
+ // parts back with `getUTC*` or `timeZone: "UTC"` formatting; the
790
+ // projection is the exact inverse of the write-side `Date` encoding
791
+ // (`formatTemporalValue`). Unparseable stored values round to "no
792
+ // value", matching the forgiving-read posture of `coerceOnRead`.
793
+ function projectTemporalDate(inputType, value) {
794
+ if (typeof value !== "string" || value === "")
795
+ return undefined;
796
+ const parsed = new Date(anchorTemporalUtc(inputType, value));
797
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
798
+ }
799
+ function healRepeaterRow(field, row) {
800
+ if (!row || typeof row !== "object" || Array.isArray(row))
801
+ return row;
802
+ const rowObj = row;
803
+ let healed = null;
804
+ for (const subField of field.subFields) {
805
+ const target = referenceTargetOf(subField);
806
+ if (!target)
807
+ continue;
808
+ const value = rowObj[subField.key];
809
+ const next = healReferenceValue(target, value);
810
+ if (next !== value) {
811
+ healed ??= { ...rowObj };
812
+ healed[subField.key] = next;
813
+ }
814
+ }
815
+ return healed ?? row;
816
+ }
633
817
  export function isEmptyMetaPatch(patch) {
634
818
  return (patch === null || (patch.upserts.size === 0 && patch.deletes.length === 0));
635
819
  }
@@ -674,72 +858,6 @@ export async function loadMeta(ctx, table, idColumn, id, findField) {
674
858
  return decodeMetaBag(findField, row?.meta);
675
859
  }
676
860
  // --- internals below ---------------------------------------------------
677
- function coerceToType(type, key, value) {
678
- switch (type) {
679
- case "string":
680
- return coerceString(key, value);
681
- case "number":
682
- return coerceNumber(key, value);
683
- case "boolean":
684
- return coerceBoolean(key, value);
685
- case "json":
686
- return coerceJson(key, value);
687
- }
688
- }
689
- function coerceString(key, value) {
690
- if (typeof value === "string")
691
- return value;
692
- if (typeof value === "number" && Number.isFinite(value))
693
- return String(value);
694
- if (typeof value === "boolean")
695
- return String(value);
696
- throw MetaSanitizationError.invalidValue({ key });
697
- }
698
- function coerceNumber(key, value) {
699
- if (typeof value === "number" && Number.isFinite(value))
700
- return value;
701
- if (typeof value === "string") {
702
- // Empty string comes from cleared form inputs; the admin dispatcher
703
- // already sends `null` for those, but a direct RPC caller might send
704
- // "" — reject here so we don't silently coerce to 0 (`Number("") === 0`).
705
- if (value.trim() === "") {
706
- throw MetaSanitizationError.invalidValue({ key });
707
- }
708
- const parsed = Number(value);
709
- if (Number.isFinite(parsed))
710
- return parsed;
711
- }
712
- if (typeof value === "boolean")
713
- return value ? 1 : 0;
714
- throw MetaSanitizationError.invalidValue({ key });
715
- }
716
- function coerceBoolean(key, value) {
717
- if (typeof value === "boolean")
718
- return value;
719
- if (value === 1 || value === "1" || value === "true")
720
- return true;
721
- if (value === 0 || value === "0" || value === "false")
722
- return false;
723
- throw MetaSanitizationError.invalidValue({ key });
724
- }
725
- function coerceJson(key, value) {
726
- // json keys take anything round-trippable through JSON.stringify —
727
- // we reject values that throw (BigInt) or silently drop (functions,
728
- // Symbols) so reads don't hand back `undefined` for something a
729
- // plugin thought it stored. TS types JSON.stringify as always-
730
- // string, but at runtime it returns `undefined` for unserializable
731
- // inputs — hence the cast.
732
- try {
733
- const encoded = JSON.stringify(value);
734
- if (encoded === undefined) {
735
- throw MetaSanitizationError.invalidValue({ key });
736
- }
737
- return JSON.parse(encoded);
738
- }
739
- catch {
740
- throw MetaSanitizationError.invalidValue({ key });
741
- }
742
- }
743
861
  function assertEncodedSize(key, value) {
744
862
  const encoded = JSON.stringify(value);
745
863
  if (encoded === undefined)