@elytracms/studio 0.0.93 → 0.0.95

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 (541) hide show
  1. package/convex/analytics-cost.test.ts +817 -0
  2. package/convex/asset-alt-migration.test.ts +2 -2
  3. package/convex/asset-meta-patch.test.ts +218 -0
  4. package/convex/asset-replace-delete-race.test.ts +127 -0
  5. package/convex/assets.ts +27 -2
  6. package/convex/auth.ts +5 -2
  7. package/convex/authority.ts +35 -11
  8. package/convex/contract-parity.test.ts +7 -0
  9. package/convex/delivery-redirects.test.ts +167 -0
  10. package/convex/delivery.ts +23 -10
  11. package/convex/guard.ts +9 -7
  12. package/convex/http-routes.test.ts +18 -8
  13. package/convex/http.ts +23 -17
  14. package/convex/ingest.test.ts +326 -0
  15. package/convex/ingest.ts +281 -0
  16. package/convex/invites.ts +13 -5
  17. package/convex/members.ts +10 -4
  18. package/convex/rail-write-parity.test.ts +6 -6
  19. package/convex/record-functions.ts +325 -0
  20. package/convex/records.test.ts +156 -0
  21. package/convex/redirect-migration.test.ts +205 -0
  22. package/convex/redirect-revalidate.test.ts +5 -1
  23. package/convex/redirects.ts +120 -79
  24. package/convex/role-policy.test.ts +315 -0
  25. package/convex/roles.ts +83 -0
  26. package/convex/schema.ts +36 -0
  27. package/convex/triggers.ts +21 -1
  28. package/dist/addon.d.ts +71 -0
  29. package/dist/addon.js +67 -0
  30. package/dist/addon.js.map +1 -0
  31. package/dist/components/account/account-settings.js +2 -2
  32. package/dist/components/account/account-settings.js.map +1 -1
  33. package/dist/components/account/credential-panel.js +2 -2
  34. package/dist/components/account/credential-panel.js.map +1 -1
  35. package/dist/components/account/self-account-schema.d.ts +6 -15
  36. package/dist/components/account/self-account-schema.js +52 -34
  37. package/dist/components/account/self-account-schema.js.map +1 -1
  38. package/dist/components/account/self-account-view-source.d.ts +1 -1
  39. package/dist/components/account/self-account-view-source.js +6 -4
  40. package/dist/components/account/self-account-view-source.js.map +1 -1
  41. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  42. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  43. package/dist/components/ai/ai-chat-panel.js +6 -6
  44. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  45. package/dist/components/ai/ai-cms-panel.js +2 -2
  46. package/dist/components/ai/ai-cms-panel.js.map +1 -1
  47. package/dist/components/ai/ai-panel-toggle.js +4 -4
  48. package/dist/components/ai/ai-panel-toggle.js.map +1 -1
  49. package/dist/components/ai/chat-ui.js +2 -2
  50. package/dist/components/ai/chat-ui.js.map +1 -1
  51. package/dist/components/ai/proposal-card.d.ts +2 -2
  52. package/dist/components/ai/proposal-card.js +2 -2
  53. package/dist/components/ai/proposal-card.js.map +1 -1
  54. package/dist/components/ai/proposal-issues.js +2 -2
  55. package/dist/components/ai/proposal-issues.js.map +1 -1
  56. package/dist/components/ai/translate-document-action.js +2 -2
  57. package/dist/components/ai/translate-document-action.js.map +1 -1
  58. package/dist/components/ai/translate-document-dialog.js +6 -6
  59. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  60. package/dist/components/ai/translate-skipped.d.ts +1 -1
  61. package/dist/components/ai/translate-skipped.js +2 -2
  62. package/dist/components/ai/translate-skipped.js.map +1 -1
  63. package/dist/components/app-views/default-modules.d.ts +50 -2
  64. package/dist/components/app-views/default-modules.js +65 -20
  65. package/dist/components/app-views/default-modules.js.map +1 -1
  66. package/dist/components/app-views/module.d.ts +53 -3
  67. package/dist/components/app-views/module.js +14 -7
  68. package/dist/components/app-views/module.js.map +1 -1
  69. package/dist/components/app-views/sidebar.js +1 -1
  70. package/dist/components/app-views/sidebar.js.map +1 -1
  71. package/dist/components/app-views/studio-config.d.ts +11 -2
  72. package/dist/components/app-views/studio-config.js.map +1 -1
  73. package/dist/components/assets/asset-detail.js +28 -13
  74. package/dist/components/assets/asset-detail.js.map +1 -1
  75. package/dist/components/assets/asset-grid.js +2 -2
  76. package/dist/components/assets/asset-grid.js.map +1 -1
  77. package/dist/components/assets/asset-locale.d.ts +9 -0
  78. package/dist/components/assets/asset-locale.js +16 -0
  79. package/dist/components/assets/asset-locale.js.map +1 -0
  80. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  81. package/dist/components/assets/asset-meta-label.js +15 -0
  82. package/dist/components/assets/asset-meta-label.js.map +1 -0
  83. package/dist/components/assets/asset-metadata.d.ts +69 -15
  84. package/dist/components/assets/asset-metadata.js +99 -11
  85. package/dist/components/assets/asset-metadata.js.map +1 -1
  86. package/dist/components/assets/asset-search.js +8 -2
  87. package/dist/components/assets/asset-search.js.map +1 -1
  88. package/dist/components/assets/asset-thumb.js +9 -5
  89. package/dist/components/assets/asset-thumb.js.map +1 -1
  90. package/dist/components/assets/assets-collection-view-source.d.ts +25 -11
  91. package/dist/components/assets/assets-collection-view-source.js +78 -26
  92. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  93. package/dist/components/assets/assets-module.js +1 -1
  94. package/dist/components/assets/assets-module.js.map +1 -1
  95. package/dist/components/assets/assets-store.d.ts +2 -2
  96. package/dist/components/assets/assets-store.js +22 -9
  97. package/dist/components/assets/assets-store.js.map +1 -1
  98. package/dist/components/assets/assets-view-preference.js +1 -1
  99. package/dist/components/assets/assets-view-preference.js.map +1 -1
  100. package/dist/components/assets/replace-file.js +1 -1
  101. package/dist/components/assets/replace-file.js.map +1 -1
  102. package/dist/components/auth-context.d.ts +1 -1
  103. package/dist/components/auth-context.js +6 -6
  104. package/dist/components/auth-context.js.map +1 -1
  105. package/dist/components/builder/blocks-model.d.ts +1 -1
  106. package/dist/components/builder/blocks-model.js +3 -3
  107. package/dist/components/builder/blocks-model.js.map +1 -1
  108. package/dist/components/builder/blocks-panel.d.ts +1 -1
  109. package/dist/components/builder/blocks-panel.js +4 -4
  110. package/dist/components/builder/blocks-panel.js.map +1 -1
  111. package/dist/components/builder/builder-store.d.ts +21 -0
  112. package/dist/components/builder/builder-store.js +24 -2
  113. package/dist/components/builder/builder-store.js.map +1 -1
  114. package/dist/components/builder/canvas-breakpoints.js +1 -1
  115. package/dist/components/builder/canvas-breakpoints.js.map +1 -1
  116. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  117. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  118. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  119. package/dist/components/builder/canvas-dnd.d.ts +2 -2
  120. package/dist/components/builder/canvas-dnd.js +1 -1
  121. package/dist/components/builder/canvas-dnd.js.map +1 -1
  122. package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
  123. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  124. package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
  125. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
  126. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  127. package/dist/components/builder/canvas-rebase.js +169 -0
  128. package/dist/components/builder/canvas-rebase.js.map +1 -0
  129. package/dist/components/builder/canvas-render-context.d.ts +7 -4
  130. package/dist/components/builder/canvas-render-context.js +21 -13
  131. package/dist/components/builder/canvas-render-context.js.map +1 -1
  132. package/dist/components/builder/canvas-theme-notice.js +2 -2
  133. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  134. package/dist/components/builder/canvas.js +3 -3
  135. package/dist/components/builder/canvas.js.map +1 -1
  136. package/dist/components/builder/clipboard-actions.d.ts +2 -2
  137. package/dist/components/builder/clipboard-actions.js +4 -4
  138. package/dist/components/builder/clipboard-actions.js.map +1 -1
  139. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  140. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  141. package/dist/components/builder/composition-field-editor.d.ts +21 -3
  142. package/dist/components/builder/composition-field-editor.js +48 -30
  143. package/dist/components/builder/composition-field-editor.js.map +1 -1
  144. package/dist/components/builder/drag-session.d.ts +1 -1
  145. package/dist/components/builder/drag-session.js.map +1 -1
  146. package/dist/components/builder/fixture-thumbnail.js +1 -1
  147. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  148. package/dist/components/builder/insert-context.d.ts +1 -1
  149. package/dist/components/builder/insert-context.js +3 -3
  150. package/dist/components/builder/insert-context.js.map +1 -1
  151. package/dist/components/builder/layout-inspector.js +1 -1
  152. package/dist/components/builder/layout-inspector.js.map +1 -1
  153. package/dist/components/builder/node-factory.js +15 -3
  154. package/dist/components/builder/node-factory.js.map +1 -1
  155. package/dist/components/builder/node-title.js +1 -1
  156. package/dist/components/builder/node-title.js.map +1 -1
  157. package/dist/components/builder/page-tree.d.ts +1 -1
  158. package/dist/components/builder/page-tree.js +5 -5
  159. package/dist/components/builder/page-tree.js.map +1 -1
  160. package/dist/components/builder/placement-refusal.js +3 -3
  161. package/dist/components/builder/placement-refusal.js.map +1 -1
  162. package/dist/components/builder/property-inspector.js +7 -5
  163. package/dist/components/builder/property-inspector.js.map +1 -1
  164. package/dist/components/builder/reuse-context.js +4 -4
  165. package/dist/components/builder/reuse-context.js.map +1 -1
  166. package/dist/components/builder/reuse.js +1 -1
  167. package/dist/components/builder/reuse.js.map +1 -1
  168. package/dist/components/builder/rich-text-toolbar.js +1 -1
  169. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  170. package/dist/components/builder/run-inspector.js +11 -7
  171. package/dist/components/builder/run-inspector.js.map +1 -1
  172. package/dist/components/builder/selection-overlay.js +3 -3
  173. package/dist/components/builder/selection-overlay.js.map +1 -1
  174. package/dist/components/builder/spacing-handles.d.ts +2 -2
  175. package/dist/components/builder/spacing-handles.js +3 -3
  176. package/dist/components/builder/spacing-handles.js.map +1 -1
  177. package/dist/components/cms/asset-field.d.ts +5 -2
  178. package/dist/components/cms/asset-field.js +36 -19
  179. package/dist/components/cms/asset-field.js.map +1 -1
  180. package/dist/components/cms/asset-picker.js +5 -3
  181. package/dist/components/cms/asset-picker.js.map +1 -1
  182. package/dist/components/cms/bulk-actions.d.ts +1 -1
  183. package/dist/components/cms/bulk-actions.js +2 -2
  184. package/dist/components/cms/bulk-actions.js.map +1 -1
  185. package/dist/components/cms/bulk-bar.d.ts +2 -2
  186. package/dist/components/cms/bulk-bar.js +2 -2
  187. package/dist/components/cms/bulk-bar.js.map +1 -1
  188. package/dist/components/cms/canvas-commit.d.ts +56 -0
  189. package/dist/components/cms/canvas-commit.js +35 -0
  190. package/dist/components/cms/canvas-commit.js.map +1 -0
  191. package/dist/components/cms/cell-edit.d.ts +1 -1
  192. package/dist/components/cms/cell-edit.js +1 -1
  193. package/dist/components/cms/cell-edit.js.map +1 -1
  194. package/dist/components/cms/cell.d.ts +1 -1
  195. package/dist/components/cms/cell.js +3 -3
  196. package/dist/components/cms/cell.js.map +1 -1
  197. package/dist/components/cms/cms-index.js +2 -2
  198. package/dist/components/cms/cms-index.js.map +1 -1
  199. package/dist/components/cms/cms-labels.js +2 -2
  200. package/dist/components/cms/cms-labels.js.map +1 -1
  201. package/dist/components/cms/cms-store.d.ts +2 -2
  202. package/dist/components/cms/cms-store.js +10 -10
  203. package/dist/components/cms/cms-store.js.map +1 -1
  204. package/dist/components/cms/collection-table.js +30 -14
  205. package/dist/components/cms/collection-table.js.map +1 -1
  206. package/dist/components/cms/commit-policy.d.ts +6 -2
  207. package/dist/components/cms/commit-policy.js +1 -1
  208. package/dist/components/cms/commit-policy.js.map +1 -1
  209. package/dist/components/cms/detail-editor.d.ts +5 -2
  210. package/dist/components/cms/detail-editor.js +31 -15
  211. package/dist/components/cms/detail-editor.js.map +1 -1
  212. package/dist/components/cms/details-pane.js +1 -1
  213. package/dist/components/cms/details-pane.js.map +1 -1
  214. package/dist/components/cms/doc-state.js +1 -1
  215. package/dist/components/cms/doc-state.js.map +1 -1
  216. package/dist/components/cms/document-diff-view.js +5 -5
  217. package/dist/components/cms/document-diff-view.js.map +1 -1
  218. package/dist/components/cms/document-editor-context.js +1 -1
  219. package/dist/components/cms/document-editor-context.js.map +1 -1
  220. package/dist/components/cms/document-staging.js +1 -1
  221. package/dist/components/cms/document-staging.js.map +1 -1
  222. package/dist/components/cms/document-url-path.js +2 -2
  223. package/dist/components/cms/document-url-path.js.map +1 -1
  224. package/dist/components/cms/duplicate-document.d.ts +1 -1
  225. package/dist/components/cms/duplicate-document.js.map +1 -1
  226. package/dist/components/cms/editable-cell.js +3 -3
  227. package/dist/components/cms/editable-cell.js.map +1 -1
  228. package/dist/components/cms/field-collaboration.d.ts +1 -1
  229. package/dist/components/cms/field-collaboration.js +1 -1
  230. package/dist/components/cms/field-collaboration.js.map +1 -1
  231. package/dist/components/cms/field-connection.d.ts +1 -1
  232. package/dist/components/cms/field-connection.js +2 -2
  233. package/dist/components/cms/field-connection.js.map +1 -1
  234. package/dist/components/cms/field-control-lib.js +1 -1
  235. package/dist/components/cms/field-control-lib.js.map +1 -1
  236. package/dist/components/cms/field-control.js +4 -4
  237. package/dist/components/cms/field-control.js.map +1 -1
  238. package/dist/components/cms/field-renderer.d.ts +1 -1
  239. package/dist/components/cms/field-renderer.js +2 -2
  240. package/dist/components/cms/field-renderer.js.map +1 -1
  241. package/dist/components/cms/filter-builder.d.ts +1 -1
  242. package/dist/components/cms/filter-builder.js +3 -3
  243. package/dist/components/cms/filter-builder.js.map +1 -1
  244. package/dist/components/cms/gate-validation.js +1 -1
  245. package/dist/components/cms/gate-validation.js.map +1 -1
  246. package/dist/components/cms/import-export.js +2 -2
  247. package/dist/components/cms/import-export.js.map +1 -1
  248. package/dist/components/cms/issue-copy.js +1 -1
  249. package/dist/components/cms/issue-copy.js.map +1 -1
  250. package/dist/components/cms/issues-pane.js +3 -3
  251. package/dist/components/cms/issues-pane.js.map +1 -1
  252. package/dist/components/cms/object-array-field.js +2 -2
  253. package/dist/components/cms/object-array-field.js.map +1 -1
  254. package/dist/components/cms/prop-fields.js +2 -2
  255. package/dist/components/cms/prop-fields.js.map +1 -1
  256. package/dist/components/cms/reference-confirm.d.ts +1 -1
  257. package/dist/components/cms/reference-confirm.js +2 -2
  258. package/dist/components/cms/reference-confirm.js.map +1 -1
  259. package/dist/components/cms/relation-picker.js +2 -2
  260. package/dist/components/cms/relation-picker.js.map +1 -1
  261. package/dist/components/cms/rich-text/block-style.js +1 -1
  262. package/dist/components/cms/rich-text/block-style.js.map +1 -1
  263. package/dist/components/cms/rich-text/component-embed.js +4 -4
  264. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  265. package/dist/components/cms/rich-text/editor.js +3 -3
  266. package/dist/components/cms/rich-text/editor.js.map +1 -1
  267. package/dist/components/cms/rich-text/embed-picker.js +3 -3
  268. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  269. package/dist/components/cms/rich-text/embed-props.js +3 -3
  270. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  271. package/dist/components/cms/rich-text/extensions.js +1 -1
  272. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  273. package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
  274. package/dist/components/cms/rich-text/font-size-control.js +2 -2
  275. package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
  276. package/dist/components/cms/rich-text/lazy.js +1 -1
  277. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  278. package/dist/components/cms/rich-text/link-control.js +2 -2
  279. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  280. package/dist/components/cms/rich-text/slash-command.js +3 -3
  281. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  282. package/dist/components/cms/rich-text/toolbar.js +1 -1
  283. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  284. package/dist/components/cms/rich-text/unknown-node.js +2 -2
  285. package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
  286. package/dist/components/cms/rich-text-preview.js +2 -2
  287. package/dist/components/cms/rich-text-preview.js.map +1 -1
  288. package/dist/components/cms/row-actions.d.ts +1 -1
  289. package/dist/components/cms/row-actions.js.map +1 -1
  290. package/dist/components/cms/scalar-value-editor.d.ts +1 -1
  291. package/dist/components/cms/scalar-value-editor.js +2 -2
  292. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  293. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  294. package/dist/components/cms/use-discard-controller.js +1 -1
  295. package/dist/components/cms/use-discard-controller.js.map +1 -1
  296. package/dist/components/cms/use-document-draft.d.ts +69 -5
  297. package/dist/components/cms/use-document-draft.js +118 -34
  298. package/dist/components/cms/use-document-draft.js.map +1 -1
  299. package/dist/components/cms/use-document-versions.js +1 -1
  300. package/dist/components/cms/use-document-versions.js.map +1 -1
  301. package/dist/components/cms/use-field-focus.d.ts +1 -1
  302. package/dist/components/cms/use-field-focus.js +2 -2
  303. package/dist/components/cms/use-field-focus.js.map +1 -1
  304. package/dist/components/cms/use-live-url.d.ts +7 -2
  305. package/dist/components/cms/use-live-url.js +15 -12
  306. package/dist/components/cms/use-live-url.js.map +1 -1
  307. package/dist/components/cms/use-publish-controller.d.ts +1 -1
  308. package/dist/components/cms/use-publish-controller.js +1 -1
  309. package/dist/components/cms/use-publish-controller.js.map +1 -1
  310. package/dist/components/cms/use-published-perspective.d.ts +1 -1
  311. package/dist/components/cms/use-published-perspective.js.map +1 -1
  312. package/dist/components/cms/use-restore-version.d.ts +1 -1
  313. package/dist/components/cms/use-restore-version.js +1 -1
  314. package/dist/components/cms/use-restore-version.js.map +1 -1
  315. package/dist/components/cms/version-compare-lazy.js +1 -1
  316. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  317. package/dist/components/cms/version-compare.js +1 -1
  318. package/dist/components/cms/version-compare.js.map +1 -1
  319. package/dist/components/cms/versions-pane.js +3 -3
  320. package/dist/components/cms/versions-pane.js.map +1 -1
  321. package/dist/components/cms/view-bar.js +3 -3
  322. package/dist/components/cms/view-bar.js.map +1 -1
  323. package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
  324. package/dist/components/collection-view/cms-collection-view-source.js +11 -7
  325. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  326. package/dist/components/collection-view/collection-view-context.d.ts +1 -1
  327. package/dist/components/collection-view/collection-view-context.js +1 -1
  328. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  329. package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
  330. package/dist/components/collection-view/collection-view-surface.js +18 -0
  331. package/dist/components/collection-view/collection-view-surface.js.map +1 -0
  332. package/dist/components/components/component-detail.js +10 -10
  333. package/dist/components/components/component-detail.js.map +1 -1
  334. package/dist/components/components/component-preview.d.ts +2 -2
  335. package/dist/components/components/component-preview.js +5 -5
  336. package/dist/components/components/component-preview.js.map +1 -1
  337. package/dist/components/components/component-status.d.ts +1 -1
  338. package/dist/components/components/component-status.js +1 -1
  339. package/dist/components/components/component-status.js.map +1 -1
  340. package/dist/components/components/components-index.js +2 -2
  341. package/dist/components/components/components-index.js.map +1 -1
  342. package/dist/components/components/field-inspector.js +1 -1
  343. package/dist/components/components/field-inspector.js.map +1 -1
  344. package/dist/components/components/registry-browser.js +3 -3
  345. package/dist/components/components/registry-browser.js.map +1 -1
  346. package/dist/components/components/taxonomy.d.ts +1 -1
  347. package/dist/components/components/taxonomy.js.map +1 -1
  348. package/dist/components/components/transfer-dialog.js +3 -3
  349. package/dist/components/components/transfer-dialog.js.map +1 -1
  350. package/dist/components/current-user-menu.js +1 -1
  351. package/dist/components/current-user-menu.js.map +1 -1
  352. package/dist/components/delivery-health-banner.js +2 -2
  353. package/dist/components/delivery-health-banner.js.map +1 -1
  354. package/dist/components/deployment-setup-gate.js +13 -4
  355. package/dist/components/deployment-setup-gate.js.map +1 -1
  356. package/dist/components/dev-login-badge.js +2 -2
  357. package/dist/components/dev-login-badge.js.map +1 -1
  358. package/dist/components/guards.js +1 -1
  359. package/dist/components/guards.js.map +1 -1
  360. package/dist/components/inspector/document-panes.d.ts +54 -0
  361. package/dist/components/inspector/document-panes.js +41 -0
  362. package/dist/components/inspector/document-panes.js.map +1 -0
  363. package/dist/components/inspector/inspector-sidebar.js +1 -1
  364. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  365. package/dist/components/nav-issues-registry.d.ts +53 -0
  366. package/dist/components/nav-issues-registry.js +69 -0
  367. package/dist/components/nav-issues-registry.js.map +1 -0
  368. package/dist/components/nav-issues.js +4 -4
  369. package/dist/components/nav-issues.js.map +1 -1
  370. package/dist/components/operation-error.d.ts +1 -1
  371. package/dist/components/operation-error.js +4 -4
  372. package/dist/components/operation-error.js.map +1 -1
  373. package/dist/components/playground-badge.js +2 -2
  374. package/dist/components/playground-badge.js.map +1 -1
  375. package/dist/components/project-list.js +3 -3
  376. package/dist/components/project-list.js.map +1 -1
  377. package/dist/components/project-session.d.ts +2 -2
  378. package/dist/components/project-session.js +5 -5
  379. package/dist/components/project-session.js.map +1 -1
  380. package/dist/components/project-switcher.js +2 -2
  381. package/dist/components/project-switcher.js.map +1 -1
  382. package/dist/components/rail-store-status.js +1 -1
  383. package/dist/components/rail-store-status.js.map +1 -1
  384. package/dist/components/rail-view-source.d.ts +6 -5
  385. package/dist/components/rail-view-source.js +4 -3
  386. package/dist/components/rail-view-source.js.map +1 -1
  387. package/dist/components/records/record-lens.d.ts +51 -0
  388. package/dist/components/records/record-lens.js +151 -0
  389. package/dist/components/records/record-lens.js.map +1 -0
  390. package/dist/components/records/record-rail.d.ts +86 -0
  391. package/dist/components/records/record-rail.js +241 -0
  392. package/dist/components/records/record-rail.js.map +1 -0
  393. package/dist/components/redirects/redirects-schema.js +1 -1
  394. package/dist/components/redirects/redirects-schema.js.map +1 -1
  395. package/dist/components/references/references-panel.js +2 -2
  396. package/dist/components/references/references-panel.js.map +1 -1
  397. package/dist/components/section-nav.js +27 -11
  398. package/dist/components/section-nav.js.map +1 -1
  399. package/dist/components/section-state.js +2 -2
  400. package/dist/components/section-state.js.map +1 -1
  401. package/dist/components/section-surface.js +2 -2
  402. package/dist/components/section-surface.js.map +1 -1
  403. package/dist/components/session-expired-overlay.js +3 -3
  404. package/dist/components/session-expired-overlay.js.map +1 -1
  405. package/dist/components/settings-form.js +1 -1
  406. package/dist/components/settings-form.js.map +1 -1
  407. package/dist/components/sign-in-needed-badge.js +1 -1
  408. package/dist/components/sign-in-needed-badge.js.map +1 -1
  409. package/dist/components/sign-in-view.d.ts +2 -2
  410. package/dist/components/sign-in-view.js +5 -5
  411. package/dist/components/sign-in-view.js.map +1 -1
  412. package/dist/components/studio-context.js +3 -3
  413. package/dist/components/studio-context.js.map +1 -1
  414. package/dist/components/studio-project-screen.js +30 -3
  415. package/dist/components/studio-project-screen.js.map +1 -1
  416. package/dist/components/studio-shell.js +6 -5
  417. package/dist/components/studio-shell.js.map +1 -1
  418. package/dist/components/theme-context.d.ts +1 -1
  419. package/dist/components/theme-context.js +2 -2
  420. package/dist/components/theme-context.js.map +1 -1
  421. package/dist/components/use-hydration-phase.d.ts +1 -1
  422. package/dist/components/use-hydration-phase.js +1 -1
  423. package/dist/components/use-hydration-phase.js.map +1 -1
  424. package/dist/components/users/invite-user-bar.d.ts +27 -0
  425. package/dist/components/users/invite-user-bar.js +25 -9
  426. package/dist/components/users/invite-user-bar.js.map +1 -1
  427. package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
  428. package/dist/components/users/role-policy-drift-issues.js +71 -0
  429. package/dist/components/users/role-policy-drift-issues.js.map +1 -0
  430. package/dist/components/users/users-collection-view-source.d.ts +1 -1
  431. package/dist/components/users/users-collection-view-source.js +4 -2
  432. package/dist/components/users/users-collection-view-source.js.map +1 -1
  433. package/dist/components/users/users-module.js +1 -1
  434. package/dist/components/users/users-module.js.map +1 -1
  435. package/dist/components/users/users-schema.d.ts +10 -1
  436. package/dist/components/users/users-schema.js +45 -25
  437. package/dist/components/users/users-schema.js.map +1 -1
  438. package/dist/components/users/users-store.js +3 -3
  439. package/dist/components/users/users-store.js.map +1 -1
  440. package/dist/components/workspace-projects.d.ts +1 -1
  441. package/dist/components/workspace-projects.js +1 -1
  442. package/dist/components/workspace-projects.js.map +1 -1
  443. package/dist/lib/ai/index.d.ts +1 -1
  444. package/dist/lib/ai/index.js +1 -1
  445. package/dist/lib/ai/index.js.map +1 -1
  446. package/dist/lib/ai/node-handler.d.ts +1 -1
  447. package/dist/lib/ai/node-handler.js.map +1 -1
  448. package/dist/lib/ai/translate-document.js +1 -1
  449. package/dist/lib/ai/translate-document.js.map +1 -1
  450. package/dist/lib/assets/upload.d.ts +25 -7
  451. package/dist/lib/assets/upload.js +30 -21
  452. package/dist/lib/assets/upload.js.map +1 -1
  453. package/dist/lib/clipboard/paste-issues.js +1 -1
  454. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  455. package/dist/lib/clipboard/paste-target.js +2 -2
  456. package/dist/lib/clipboard/paste-target.js.map +1 -1
  457. package/dist/lib/cms/published-documents.d.ts +24 -6
  458. package/dist/lib/cms/published-documents.js +39 -6
  459. package/dist/lib/cms/published-documents.js.map +1 -1
  460. package/dist/lib/cms-views/view-model.d.ts +12 -99
  461. package/dist/lib/cms-views/view-model.js +22 -103
  462. package/dist/lib/cms-views/view-model.js.map +1 -1
  463. package/dist/lib/collection-view/collection-view-source.d.ts +13 -3
  464. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  465. package/dist/lib/data/content-session-refresh.d.ts +1 -1
  466. package/dist/lib/data/content-session-refresh.js.map +1 -1
  467. package/dist/lib/data/playground-unload-guard.d.ts +1 -1
  468. package/dist/lib/data/playground-unload-guard.js +1 -1
  469. package/dist/lib/data/playground-unload-guard.js.map +1 -1
  470. package/dist/lib/engine-version.d.ts +1 -1
  471. package/dist/lib/engine-version.js +1 -1
  472. package/dist/lib/engine-version.js.map +1 -1
  473. package/dist/lib/i18n/i18n.js +1 -1
  474. package/dist/lib/i18n/i18n.js.map +1 -1
  475. package/dist/lib/i18n/messages.de.js +11 -3
  476. package/dist/lib/i18n/messages.de.js.map +1 -1
  477. package/dist/lib/i18n/messages.en.d.ts +9 -1
  478. package/dist/lib/i18n/messages.en.js +16 -4
  479. package/dist/lib/i18n/messages.en.js.map +1 -1
  480. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  481. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  482. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  483. package/dist/lib/operations/project-runtime-config.d.ts +9 -0
  484. package/dist/lib/operations/project-runtime-config.js +13 -0
  485. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  486. package/dist/lib/operations/studio-operations.d.ts +9 -1
  487. package/dist/lib/operations/studio-operations.js +6 -1
  488. package/dist/lib/operations/studio-operations.js.map +1 -1
  489. package/dist/lib/persistence/convex-adapter.d.ts +16 -3
  490. package/dist/lib/persistence/convex-adapter.js +78 -47
  491. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  492. package/dist/lib/persistence/deployment-readiness.js +14 -11
  493. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  494. package/dist/lib/persistence/index.js +14 -1
  495. package/dist/lib/persistence/index.js.map +1 -1
  496. package/dist/lib/persona/labels.js +2 -1
  497. package/dist/lib/persona/labels.js.map +1 -1
  498. package/dist/lib/query/index.d.ts +2 -2
  499. package/dist/lib/query/index.js +1 -1
  500. package/dist/lib/query/index.js.map +1 -1
  501. package/dist/lib/query/operations-query.d.ts +27 -1
  502. package/dist/lib/query/operations-query.js +13 -1
  503. package/dist/lib/query/operations-query.js.map +1 -1
  504. package/dist/lib/references/reference-model.js +1 -1
  505. package/dist/lib/references/reference-model.js.map +1 -1
  506. package/dist/lib/references/references-store.js +1 -1
  507. package/dist/lib/references/references-store.js.map +1 -1
  508. package/dist/lib/registry/index.js +2 -2
  509. package/dist/lib/registry/index.js.map +1 -1
  510. package/dist/lib/registry/preview.d.ts +10 -0
  511. package/dist/lib/registry/preview.js +13 -4
  512. package/dist/lib/registry/preview.js.map +1 -1
  513. package/dist/lib/routes/route-issues.js +6 -3
  514. package/dist/lib/routes/route-issues.js.map +1 -1
  515. package/dist/lib/workspace/index.d.ts +6 -0
  516. package/dist/lib/workspace/index.js +8 -0
  517. package/dist/lib/workspace/index.js.map +1 -1
  518. package/dist/lib/workspace/workspace-source.js +6 -7
  519. package/dist/lib/workspace/workspace-source.js.map +1 -1
  520. package/dist/mount.d.ts +28 -0
  521. package/dist/mount.js +27 -0
  522. package/dist/mount.js.map +1 -0
  523. package/dist/previews/entry-node.d.ts +14 -0
  524. package/dist/previews/entry-node.js +22 -0
  525. package/dist/previews/entry-node.js.map +1 -0
  526. package/dist/previews/harness.js +2 -16
  527. package/dist/previews/harness.js.map +1 -1
  528. package/dist/router/root.js +10 -10
  529. package/dist/router/root.js.map +1 -1
  530. package/dist/ui/badge.d.ts +1 -1
  531. package/dist/ui/button.d.ts +2 -2
  532. package/package.json +15 -11
  533. package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
  534. package/dist/components/redirects/redirects-collection-view-source.js +0 -146
  535. package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
  536. package/dist/components/redirects/redirects-module.d.ts +0 -21
  537. package/dist/components/redirects/redirects-module.js +0 -40
  538. package/dist/components/redirects/redirects-module.js.map +0 -1
  539. package/dist/components/redirects/redirects-store.d.ts +0 -68
  540. package/dist/components/redirects/redirects-store.js +0 -132
  541. package/dist/components/redirects/redirects-store.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import { makeFunctionReference } from 'convex/server'
4
+ import schema from './schema'
5
+
6
+ // convex-test fires `runAfter(0)` on a REAL timer; fake timers keep any scheduled
7
+ // revalidation queued (rows written, the `webhooks:deliver` fetch never fired).
8
+ beforeEach(() => {
9
+ vi.useFakeTimers()
10
+ })
11
+ afterEach(() => {
12
+ vi.useRealTimers()
13
+ })
14
+
15
+ /**
16
+ * EC-616 — the redirect move from the `redirects` table into `records` (kind `redirect`)
17
+ * is a STORED-DATA move (Konditorei and Naturkinder hold real redirects), done the EC-587
18
+ * way: writes to the new home, a marked tolerant read of the old one, an idempotent
19
+ * per-deployment migration. These run the REAL functions on convex-test over legacy rows
20
+ * inserted as a pre-EC-616 deployment holds them, and pin "no gap": a deployment with the
21
+ * new functions pushed and the migration NOT yet run serves exactly what it served before.
22
+ */
23
+
24
+ const modules = {
25
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
26
+ './_generated/api.ts': async () => ({}),
27
+ }
28
+
29
+ const listRedirects = makeFunctionReference<'query'>('redirects:list')
30
+ const getRedirect = makeFunctionReference<'query'>('redirects:get')
31
+ const saveRedirect = makeFunctionReference<'mutation'>('redirects:save')
32
+ const removeRedirect = makeFunctionReference<'mutation'>('redirects:remove')
33
+ const migrateLegacy = makeFunctionReference<'mutation'>('redirects:migrateLegacy')
34
+ const snapshot = makeFunctionReference<'query'>('delivery:snapshot')
35
+
36
+ const STAMP = '2026-08-01T00:00:00.000Z'
37
+ const LATER = '2026-10-26T00:00:00.000Z'
38
+
39
+ /** Rows as a pre-EC-616 deployment stores them in the legacy `redirects` table. */
40
+ const LEGACY = [
41
+ { redirectId: 'r_spring', from: '/spring', to: '/offers', permanent: true, enabled: true },
42
+ { redirectId: 'r_paused', from: '/paused', to: '/x', permanent: false, enabled: false, meta: { note: 'later' } },
43
+ { redirectId: 'r_blog', from: '/blog-old', to: '/blog', permanent: true, enabled: true },
44
+ // Stored before EC-577 refused it: a meta key naming a column. The current schema
45
+ // refuses this row as a create, so the migration must REPORT it, never drop it.
46
+ { redirectId: 'r_shadow', from: '/shadow', to: '/real', permanent: true, enabled: true, meta: { to: '/fake' } },
47
+ ] as const
48
+
49
+ async function legacyDeployment(configuredRevalidation = false) {
50
+ const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env
51
+ if (configuredRevalidation) {
52
+ env['CANVAS_REVALIDATE_ENDPOINT'] = 'https://host.invalid/api/revalidate'
53
+ env['CANVAS_REVALIDATE_SECRET'] = 'ec616-secret'
54
+ }
55
+ const t = convexTest(schema, modules)
56
+ await t.run(async (ctx) => {
57
+ await ctx.db.insert('members', { userId: 'editor', role: 'editor', createdAt: STAMP, updatedAt: STAMP })
58
+ for (const row of LEGACY) await ctx.db.insert('redirects', { ...row, createdAt: STAMP, updatedAt: STAMP })
59
+ })
60
+ const asEditor = t.withIdentity({ subject: 'editor', issuer: 'https://convex.test' })
61
+ return { t, asEditor }
62
+ }
63
+
64
+ afterEach(() => {
65
+ const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env
66
+ delete env['CANVAS_REVALIDATE_ENDPOINT']
67
+ delete env['CANVAS_REVALIDATE_SECRET']
68
+ })
69
+
70
+ /** What the pre-EC-616 functions returned for the legacy rows: each row flat, ordered by `from`. */
71
+ const SERVED_BEFORE = [...LEGACY]
72
+ .sort((a, b) => (a.from < b.from ? -1 : 1))
73
+ .map((row) => ({ ...row, createdAt: STAMP, updatedAt: STAMP }))
74
+
75
+ /** What the pre-EC-616 snapshot served: the enabled rows as routing records. */
76
+ const LIVE_BEFORE = LEGACY.filter((row) => row.enabled).map((row) => ({
77
+ id: row.redirectId,
78
+ from: row.from,
79
+ to: row.to,
80
+ permanent: row.permanent,
81
+ }))
82
+
83
+ const byId = (a: { id: string }, b: { id: string }) => (a.id < b.id ? -1 : 1)
84
+
85
+ async function counts(t: Awaited<ReturnType<typeof legacyDeployment>>['t']) {
86
+ return t.run(async (ctx) => ({
87
+ legacy: (await ctx.db.query('redirects').collect()).length,
88
+ records: (await ctx.db.query('records').collect()).filter((row) => row.kind === 'redirect').length,
89
+ }))
90
+ }
91
+
92
+ describe('the redirect move: no gap before the migration runs (EC-616)', () => {
93
+ it('list, get and the delivery snapshot serve the legacy rows exactly as before', async () => {
94
+ const { asEditor } = await legacyDeployment()
95
+ expect(await asEditor.query(listRedirects, {})).toEqual(SERVED_BEFORE)
96
+ expect(await asEditor.query(getRedirect, { redirectId: 'r_paused' })).toEqual(
97
+ SERVED_BEFORE.find((row) => row.redirectId === 'r_paused'),
98
+ )
99
+ const served = (await asEditor.query(snapshot, { projectId: 'demo' })) as { redirects: { id: string }[] }
100
+ expect([...served.redirects].sort(byId)).toEqual([...LIVE_BEFORE].sort(byId))
101
+ })
102
+
103
+ it('a save of a legacy-only redirect lifts it: an edit, not a create; the legacy row goes', async () => {
104
+ const { t, asEditor } = await legacyDeployment()
105
+ // An edit that leaves `enabled`/`permanent` absent keeps the STORED (legacy) values.
106
+ const saved = await asEditor.mutation(saveRedirect, {
107
+ redirectId: 'r_paused',
108
+ to: '/y',
109
+ createdAt: LATER,
110
+ updatedAt: LATER,
111
+ })
112
+ expect(saved).toEqual({
113
+ redirectId: 'r_paused',
114
+ from: '/paused',
115
+ to: '/y',
116
+ permanent: false,
117
+ enabled: false,
118
+ meta: { note: 'later' },
119
+ createdAt: STAMP,
120
+ updatedAt: LATER,
121
+ })
122
+ expect(await counts(t)).toEqual({ legacy: 3, records: 1 })
123
+ expect((await asEditor.query(listRedirects, {})) as unknown[]).toHaveLength(4)
124
+ })
125
+
126
+ it('a remove of a legacy-only redirect deletes it (it never resurfaces)', async () => {
127
+ const { t, asEditor } = await legacyDeployment()
128
+ await asEditor.mutation(removeRedirect, { redirectId: 'r_blog' })
129
+ expect(await asEditor.query(getRedirect, { redirectId: 'r_blog' })).toBeNull()
130
+ expect(await counts(t)).toEqual({ legacy: 3, records: 0 })
131
+ })
132
+ })
133
+
134
+ describe('redirects:migrateLegacy (EC-616)', () => {
135
+ it('moves every valid legacy row, reports the refused one, and serves the same set after', async () => {
136
+ const { t, asEditor } = await legacyDeployment()
137
+ const first = await t.mutation(migrateLegacy, {})
138
+ expect(first).toEqual({
139
+ scanned: 4,
140
+ moved: 3,
141
+ shadowed: 0,
142
+ refused: [{ redirectId: 'r_shadow', message: expect.stringMatching(/meta may not use a column name/) }],
143
+ movedIds: ['r_blog', 'r_paused', 'r_spring'],
144
+ })
145
+ expect(await counts(t)).toEqual({ legacy: 1, records: 3 })
146
+ // The served set is unchanged by the move — the refused row still serves (tolerant read).
147
+ expect(await asEditor.query(listRedirects, {})).toEqual(SERVED_BEFORE)
148
+ const served = (await asEditor.query(snapshot, { projectId: 'demo' })) as { redirects: { id: string }[] }
149
+ expect([...served.redirects].sort(byId)).toEqual([...LIVE_BEFORE].sort(byId))
150
+ })
151
+
152
+ it('is idempotent: a second run moves nothing', async () => {
153
+ const { t } = await legacyDeployment()
154
+ await t.mutation(migrateLegacy, {})
155
+ expect(await t.mutation(migrateLegacy, {})).toMatchObject({ scanned: 1, moved: 0, shadowed: 0 })
156
+ expect(await counts(t)).toEqual({ legacy: 1, records: 3 })
157
+ })
158
+
159
+ it('drops a legacy row a record already shadows (a save lifted it meanwhile)', async () => {
160
+ const { t, asEditor } = await legacyDeployment()
161
+ // Simulate the race the transaction cannot have: a record and its legacy row both present.
162
+ await t.run(async (ctx) => {
163
+ await ctx.db.insert('redirects', {
164
+ redirectId: 'r_twice',
165
+ from: '/twice',
166
+ to: '/a',
167
+ permanent: true,
168
+ enabled: true,
169
+ createdAt: STAMP,
170
+ updatedAt: STAMP,
171
+ })
172
+ })
173
+ await asEditor.mutation(saveRedirect, { redirectId: 'r_twice', from: '/twice', to: '/b', createdAt: LATER, updatedAt: LATER })
174
+ await t.run(async (ctx) => {
175
+ await ctx.db.insert('redirects', {
176
+ redirectId: 'r_twice',
177
+ from: '/twice',
178
+ to: '/a',
179
+ permanent: true,
180
+ enabled: true,
181
+ createdAt: STAMP,
182
+ updatedAt: STAMP,
183
+ })
184
+ })
185
+ // The record wins every read while both exist…
186
+ expect(await asEditor.query(getRedirect, { redirectId: 'r_twice' })).toMatchObject({ to: '/b' })
187
+ // …and the migration drops the shadowed legacy row instead of overwriting the record.
188
+ expect(await t.mutation(migrateLegacy, {})).toMatchObject({ shadowed: 1 })
189
+ expect(await asEditor.query(getRedirect, { redirectId: 'r_twice' })).toMatchObject({ to: '/b' })
190
+ })
191
+
192
+ it('schedules no revalidation: the served set is identical before and after', async () => {
193
+ const { t } = await legacyDeployment(true)
194
+ await t.mutation(migrateLegacy, {})
195
+ const scheduled = await t.run(async (ctx) => ctx.db.system.query('_scheduled_functions').collect())
196
+ expect(scheduled).toEqual([])
197
+ })
198
+
199
+ it('control: a redirect SAVE on the same configured deployment does schedule one', async () => {
200
+ const { t, asEditor } = await legacyDeployment(true)
201
+ await asEditor.mutation(saveRedirect, { redirectId: 'r_new', from: '/n', to: '/m', createdAt: LATER, updatedAt: LATER })
202
+ const scheduled = await t.run(async (ctx) => ctx.db.system.query('_scheduled_functions').collect())
203
+ expect(scheduled.map((row) => (row.args as [{ event: unknown }])[0].event)).toEqual([{ kind: 'redirects' }])
204
+ })
205
+ })
@@ -135,8 +135,12 @@ describe('EC-510 — the write chokepoint revalidates on every redirect write',
135
135
  // is the gate working rather than the instrument being blind.
136
136
  expect(await scheduledRevalidations(t)).toEqual([])
137
137
  // The write itself still happened — silence is at the webhook, not the save.
138
+ // EC-616: a redirect is stored as a `records` row of kind `redirect` now.
138
139
  expect(
139
- await t.run(async (ctx) => (await ctx.db.query('redirects').collect()).length),
140
+ await t.run(
141
+ async (ctx) =>
142
+ (await ctx.db.query('records').collect()).filter((row) => row.kind === 'redirect').length,
143
+ ),
140
144
  ).toBe(1)
141
145
  })
142
146
  })
@@ -1,92 +1,133 @@
1
- import { ConvexError, v } from 'convex/values'
2
- import type { DocumentByName, WithoutSystemFields } from 'convex/server'
3
- import { redirectRowSchema, rejectedWriteMessage, resolveRecordWrite } from '@elytracms/core/persistence'
4
- import type { DataModel } from './functions'
5
- import { mutation, query } from './functions'
6
- import { requireCapability } from './guard'
7
-
8
- type RedirectDoc = DocumentByName<DataModel, 'redirects'>
1
+ import { internalMutationGeneric } from 'convex/server'
2
+ import type { MutationBuilder } from 'convex/server'
3
+ import {
4
+ PersistenceValidationError,
5
+ recordColumnsOf,
6
+ recordSlots,
7
+ redirectRecordKind,
8
+ resolveRecordQuery,
9
+ resolveRecordSave,
10
+ } from '@elytracms/core/persistence'
11
+ import type { RedirectEntry } from '@elytracms/core/persistence'
12
+ import type { DataModel, MutationCtx, QueryCtx } from './functions'
13
+ import { defineRecordFunctions, findRecordDoc, listRecordWire } from './record-functions'
14
+ import type { LegacyRecord, LegacyRecordSource } from './record-functions'
15
+ import { recordEntryFromWire } from '@elytracms/core/persistence'
9
16
 
10
17
  /**
11
- * Editorial redirects persistence (EC-303). Redirects are data an editor owns —
12
- * saved+enabled entries go live dynamically (no deploy) via the delivery
13
- * snapshot. Reachable only through the app's `ConvexRedirectRepository` adapter.
14
- * Content-gated (a viewer reads, an editor writes), NOT admin-only — redirects
15
- * are content, not administration.
18
+ * Editorial redirects persistence (EC-303) — since EC-616 the redirect RECORD KIND over
19
+ * the generic record store (`redirectRecordKind`, one `records` row per redirect). The
20
+ * functions keep their names and wire (`redirects:save/get/list/remove`, `redirectId` +
21
+ * the columns flat), so the studio's adapter and every consumer's
22
+ * `export * from '@elytracms/studio/convex/redirects'` line are unchanged. Content-gated
23
+ * (a viewer reads, an editor writes), NOT admin-only — redirects are content.
24
+ *
25
+ * ## The move from the `redirects` table (stored data — KON and NK hold real redirects)
26
+ *
27
+ * The EC-587 precedent, with no gap:
28
+ *
29
+ * - **writes go to `records`** (kind `redirect`); nothing writes the legacy table again;
30
+ * - **reads union the legacy rows no record shadows** — the TOLERANT READ below, marked:
31
+ * a deployment with these functions pushed and the migration NOT yet run serves the
32
+ * same redirects it served before (`list`, `get`, the delivery snapshot);
33
+ * - a save of a legacy-only redirect lifts it (record from its columns, legacy row
34
+ * deleted, one transaction); a remove deletes both;
35
+ * - **`redirects:migrateLegacy`** moves every legacy row in one idempotent internal
36
+ * mutation and reports counts — run per deployment (both consumers + the prod harness:
37
+ * owner-run); the legacy table and this tolerant read are dropped together once it is
38
+ * reported run everywhere (a gated item, like EC-587's alt column).
16
39
  */
17
40
 
18
- /**
19
- * Create or modify a redirect (EC-577). The ONE write rule (`resolveRecordWrite`,
20
- * shared with the in-memory adapter) runs here, inside the transaction: a column
21
- * absent from the args keeps its stored value — `permanent`/`enabled` included,
22
- * whose defaults (the record schema's `.default(true)`) apply only when this save
23
- * creates the redirect — `null` clears the optional `meta` bag, a value sets. ONE
24
- * `db.patch` or ONE `db.insert`, so the EC-510 revalidation trigger fires once.
25
- */
26
- export const save = mutation({
27
- args: {
28
- redirectId: v.string(),
29
- from: v.optional(v.string()),
30
- to: v.optional(v.string()),
31
- permanent: v.optional(v.boolean()),
32
- enabled: v.optional(v.boolean()),
33
- // Schema-driven additive-field bag (EC-311 Tier 2): present replaces the bag,
34
- // `null` clears it, absent keeps it.
35
- meta: v.optional(v.any()),
36
- createdAt: v.string(),
37
- updatedAt: v.string(),
38
- },
39
- handler: async (ctx, { redirectId, createdAt, updatedAt, ...write }) => {
40
- await requireCapability(ctx, 'content.write')
41
- const existing = await ctx.db
42
- .query('redirects')
43
- .withIndex('by_redirect', (q) => q.eq('redirectId', redirectId))
44
- .first()
45
- const resolved = resolveRecordWrite(redirectRowSchema, existing ?? undefined, write)
46
- if (!resolved.ok) {
47
- throw new ConvexError({
48
- code: 'validation',
49
- message: rejectedWriteMessage('redirect', redirectId, resolved.issues),
50
- issues: resolved.issues,
51
- })
52
- }
53
- if (existing) {
54
- await ctx.db.patch(existing._id, { ...(resolved.patch as Partial<RedirectDoc>), updatedAt })
55
- return ctx.db.get(existing._id)
56
- }
57
- const row: WithoutSystemFields<RedirectDoc> = { ...resolved.row, redirectId, createdAt, updatedAt }
58
- const id = await ctx.db.insert('redirects', row)
59
- return ctx.db.get(id)
60
- },
61
- })
41
+ type LegacyRedirectDoc = DataModel['redirects']['document']
62
42
 
63
- export const get = query({
64
- args: { redirectId: v.string() },
65
- handler: async (ctx, args) => {
66
- await requireCapability(ctx, 'content.read')
67
- return ctx.db
43
+ /** TOLERANT READ (marked) — the legacy `redirects` table as the kind's legacy source. */
44
+ const legacyRedirects: LegacyRecordSource = {
45
+ find: async (ctx, recordId) => {
46
+ const row = await ctx.db
68
47
  .query('redirects')
69
- .withIndex('by_redirect', (q) => q.eq('redirectId', args.redirectId))
48
+ .withIndex('by_redirect', (q) => q.eq('redirectId', recordId))
70
49
  .first()
50
+ return row ? legacyRecord(row) : null
71
51
  },
72
- })
52
+ list: async (ctx) => (await ctx.db.query('redirects').withIndex('by_redirect').collect()).map(legacyRecord),
53
+ remove: async (ctx, record) => {
54
+ await ctx.db.delete(record.handle as LegacyRedirectDoc['_id'])
55
+ },
56
+ }
57
+
58
+ function legacyRecord(row: LegacyRedirectDoc): LegacyRecord {
59
+ return {
60
+ recordId: row.redirectId,
61
+ columns: recordColumnsOf(redirectRecordKind, row),
62
+ createdAt: row.createdAt,
63
+ updatedAt: row.updatedAt,
64
+ handle: row._id,
65
+ }
66
+ }
73
67
 
74
- export const list = query({
68
+ export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind, { legacy: legacyRedirects })
69
+
70
+ /**
71
+ * Every stored redirect of this deployment (records + the tolerant legacy read), ordered
72
+ * by `from` — the ONE server-side reader; the delivery snapshot projects it through
73
+ * `readLiveRedirects` (EC-590 / EC-627 / EC-628: the one live-redirect rule).
74
+ */
75
+ export async function loadRedirectEntries(ctx: QueryCtx, projectId: string): Promise<RedirectEntry[]> {
76
+ const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind), legacyRedirects)
77
+ return rows.map((row) => recordEntryFromWire(redirectRecordKind, row, projectId))
78
+ }
79
+
80
+ /**
81
+ * EC-616 — move every legacy `redirects` row into `records` (kind `redirect`), the
82
+ * redirect move's migration. Per row: a record of the same id already exists (a save
83
+ * lifted it, or an earlier run) → the legacy row is dropped as shadowed; otherwise the
84
+ * row's columns are written as a record CREATE through the one rule (`resolveRecordSave`
85
+ * — a legacy row the current schema refuses is REPORTED and left where it is, still
86
+ * served by the tolerant read, never dropped silently) and the legacy row deleted.
87
+ * Idempotent: a second run scans nothing.
88
+ *
89
+ * Internal: an admin runs it from the CLI, once per deployment —
90
+ * `npx convex run redirects:migrateLegacy` (no args). Built on the RAW internal-mutation
91
+ * builder, deliberately outside the write-chokepoint triggers: the served redirect set is
92
+ * identical before and after by construction (the tolerant read), so the move needs no
93
+ * revalidation — through the triggers it would schedule two sweeps per row (an insert
94
+ * into `records`, a delete from `redirects`). One transaction: Convex bounds it at 8192
95
+ * written rows, far above any editorial redirect set (KON's import holds 192).
96
+ */
97
+ export const migrateLegacy = (internalMutationGeneric as MutationBuilder<DataModel, 'internal'>)({
75
98
  args: {},
76
99
  handler: async (ctx) => {
77
- await requireCapability(ctx, 'content.read')
78
- return ctx.db.query('redirects').withIndex('by_redirect').collect()
79
- },
80
- })
81
-
82
- export const remove = mutation({
83
- args: { redirectId: v.string() },
84
- handler: async (ctx, args) => {
85
- await requireCapability(ctx, 'content.write')
86
- const existing = await ctx.db
87
- .query('redirects')
88
- .withIndex('by_redirect', (q) => q.eq('redirectId', args.redirectId))
89
- .first()
90
- if (existing) await ctx.db.delete(existing._id)
100
+ const legacy = await ctx.db.query('redirects').withIndex('by_redirect').collect()
101
+ const moved: string[] = []
102
+ const shadowed: string[] = []
103
+ const refused: Array<{ redirectId: string; message: string }> = []
104
+ for (const row of legacy) {
105
+ const record = legacyRecord(row)
106
+ if (await findRecordDoc(ctx as unknown as MutationCtx, redirectRecordKind, record.recordId)) {
107
+ await ctx.db.delete(row._id)
108
+ shadowed.push(record.recordId)
109
+ continue
110
+ }
111
+ let columns: Record<string, unknown>
112
+ try {
113
+ columns = resolveRecordSave(redirectRecordKind, record.recordId, undefined, record.columns)
114
+ } catch (error) {
115
+ if (!(error instanceof PersistenceValidationError)) throw error
116
+ refused.push({ redirectId: record.recordId, message: error.message })
117
+ continue
118
+ }
119
+ const slots = recordSlots(redirectRecordKind, columns, record.createdAt)
120
+ await ctx.db.insert('records', {
121
+ kind: redirectRecordKind.kind,
122
+ recordId: record.recordId,
123
+ data: columns,
124
+ at: slots.at,
125
+ createdAt: record.createdAt,
126
+ updatedAt: record.updatedAt,
127
+ })
128
+ await ctx.db.delete(row._id)
129
+ moved.push(record.recordId)
130
+ }
131
+ return { scanned: legacy.length, moved: moved.length, shadowed: shadowed.length, refused, movedIds: moved }
91
132
  },
92
133
  })