convex-cms 0.0.2 → 0.0.3

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 (265) hide show
  1. package/admin-dist/nitro.json +15 -0
  2. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  3. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  4. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  5. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  6. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  7. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  8. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  9. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  10. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  11. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  12. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  13. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  14. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  15. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  16. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  17. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  18. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  19. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  20. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  21. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  22. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  23. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  24. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  25. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  26. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  27. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  28. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  29. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  30. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  31. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  32. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  33. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  34. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  35. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  36. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  37. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  38. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  39. package/admin-dist/public/favicon.ico +0 -0
  40. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  41. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  42. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  45. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  46. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  83. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  84. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  88. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  89. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  90. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  91. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  92. package/admin-dist/server/_libs/clsx.mjs +16 -0
  93. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  94. package/admin-dist/server/_libs/convex.mjs +4841 -0
  95. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  96. package/admin-dist/server/_libs/croner.mjs +1 -0
  97. package/admin-dist/server/_libs/crossws.mjs +1 -0
  98. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  99. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  100. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  101. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  102. package/admin-dist/server/_libs/h3.mjs +401 -0
  103. package/admin-dist/server/_libs/hookable.mjs +1 -0
  104. package/admin-dist/server/_libs/isbot.mjs +20 -0
  105. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  106. package/admin-dist/server/_libs/ohash.mjs +1 -0
  107. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  108. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  109. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  110. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  111. package/admin-dist/server/_libs/rou3.mjs +8 -0
  112. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  113. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  114. package/admin-dist/server/_libs/srvx.mjs +719 -0
  115. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  116. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  117. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  118. package/admin-dist/server/_libs/tslib.mjs +39 -0
  119. package/admin-dist/server/_libs/ufo.mjs +54 -0
  120. package/admin-dist/server/_libs/unctx.mjs +1 -0
  121. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  122. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  123. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  124. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  125. package/admin-dist/server/_libs/zod.mjs +4223 -0
  126. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  127. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  128. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  129. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  130. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  131. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  132. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  133. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  134. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  135. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  136. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  137. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  138. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  139. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  140. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  141. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  142. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  143. package/admin-dist/server/_ssr/index.mjs +1275 -0
  144. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  145. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  146. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  147. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  148. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  149. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  150. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  151. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  152. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  153. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  154. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  155. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  156. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  157. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  158. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  159. package/admin-dist/server/favicon.ico +0 -0
  160. package/admin-dist/server/index.mjs +627 -0
  161. package/dist/cli/index.js +0 -0
  162. package/dist/client/admin-config.d.ts +0 -1
  163. package/dist/client/admin-config.d.ts.map +1 -1
  164. package/dist/client/admin-config.js +0 -1
  165. package/dist/client/admin-config.js.map +1 -1
  166. package/dist/client/adminApi.d.ts.map +1 -1
  167. package/dist/client/agentTools.d.ts +1237 -135
  168. package/dist/client/agentTools.d.ts.map +1 -1
  169. package/dist/client/agentTools.js +33 -9
  170. package/dist/client/agentTools.js.map +1 -1
  171. package/dist/client/index.d.ts +1 -1
  172. package/dist/client/index.d.ts.map +1 -1
  173. package/dist/client/index.js.map +1 -1
  174. package/dist/component/_generated/component.d.ts +9 -0
  175. package/dist/component/_generated/component.d.ts.map +1 -1
  176. package/dist/component/mediaAssets.d.ts +35 -0
  177. package/dist/component/mediaAssets.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.js +81 -0
  179. package/dist/component/mediaAssets.js.map +1 -1
  180. package/dist/test.d.ts.map +1 -1
  181. package/dist/test.js +2 -1
  182. package/dist/test.js.map +1 -1
  183. package/package.json +9 -5
  184. package/dist/component/auditLog.d.ts +0 -410
  185. package/dist/component/auditLog.d.ts.map +0 -1
  186. package/dist/component/auditLog.js +0 -607
  187. package/dist/component/auditLog.js.map +0 -1
  188. package/dist/component/types.d.ts +0 -4
  189. package/dist/component/types.d.ts.map +0 -1
  190. package/dist/component/types.js +0 -2
  191. package/dist/component/types.js.map +0 -1
  192. package/src/cli/commands/admin.ts +0 -104
  193. package/src/cli/index.ts +0 -21
  194. package/src/cli/utils/detectConvexUrl.ts +0 -54
  195. package/src/cli/utils/openBrowser.ts +0 -16
  196. package/src/client/admin-config.ts +0 -138
  197. package/src/client/adminApi.ts +0 -942
  198. package/src/client/agentTools.ts +0 -1311
  199. package/src/client/argTypes.ts +0 -316
  200. package/src/client/field-types.ts +0 -187
  201. package/src/client/index.ts +0 -1301
  202. package/src/client/queryBuilder.ts +0 -1100
  203. package/src/client/schema/codegen.ts +0 -500
  204. package/src/client/schema/defineContentType.ts +0 -501
  205. package/src/client/schema/index.ts +0 -169
  206. package/src/client/schema/schemaDrift.ts +0 -574
  207. package/src/client/schema/typedClient.ts +0 -688
  208. package/src/client/schema/types.ts +0 -666
  209. package/src/client/types.ts +0 -723
  210. package/src/client/workflows.ts +0 -141
  211. package/src/client/wrapper.ts +0 -4304
  212. package/src/component/_generated/api.ts +0 -140
  213. package/src/component/_generated/component.ts +0 -5029
  214. package/src/component/_generated/dataModel.ts +0 -60
  215. package/src/component/_generated/server.ts +0 -156
  216. package/src/component/authorization.ts +0 -647
  217. package/src/component/authorizationHooks.ts +0 -668
  218. package/src/component/bulkOperations.ts +0 -687
  219. package/src/component/contentEntries.ts +0 -1976
  220. package/src/component/contentEntryMutations.ts +0 -1223
  221. package/src/component/contentEntryValidation.ts +0 -707
  222. package/src/component/contentLock.ts +0 -550
  223. package/src/component/contentTypeMigration.ts +0 -1064
  224. package/src/component/contentTypeMutations.ts +0 -969
  225. package/src/component/contentTypes.ts +0 -346
  226. package/src/component/convex.config.ts +0 -44
  227. package/src/component/documentTypes.ts +0 -240
  228. package/src/component/eventEmitter.ts +0 -485
  229. package/src/component/exportImport.ts +0 -1169
  230. package/src/component/index.ts +0 -491
  231. package/src/component/lib/deepReferenceResolver.ts +0 -999
  232. package/src/component/lib/errors.ts +0 -816
  233. package/src/component/lib/index.ts +0 -145
  234. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  235. package/src/component/lib/metadataExtractor.ts +0 -792
  236. package/src/component/lib/mutationAuth.ts +0 -199
  237. package/src/component/lib/queries.ts +0 -79
  238. package/src/component/lib/ragContentChunker.ts +0 -1371
  239. package/src/component/lib/referenceResolver.ts +0 -430
  240. package/src/component/lib/slugGenerator.ts +0 -262
  241. package/src/component/lib/slugUniqueness.ts +0 -333
  242. package/src/component/lib/softDelete.ts +0 -44
  243. package/src/component/localeFallbackChain.ts +0 -673
  244. package/src/component/localeFields.ts +0 -896
  245. package/src/component/mediaAssetMutations.ts +0 -725
  246. package/src/component/mediaAssets.ts +0 -932
  247. package/src/component/mediaFolderMutations.ts +0 -1046
  248. package/src/component/mediaUploadMutations.ts +0 -224
  249. package/src/component/mediaVariantMutations.ts +0 -900
  250. package/src/component/mediaVariants.ts +0 -793
  251. package/src/component/ragContentIndexer.ts +0 -1067
  252. package/src/component/rateLimitHooks.ts +0 -572
  253. package/src/component/roles.ts +0 -1360
  254. package/src/component/scheduledPublish.ts +0 -358
  255. package/src/component/schema.ts +0 -617
  256. package/src/component/taxonomies.ts +0 -949
  257. package/src/component/taxonomyMutations.ts +0 -1210
  258. package/src/component/trash.ts +0 -724
  259. package/src/component/userContext.ts +0 -898
  260. package/src/component/validation.ts +0 -1388
  261. package/src/component/validators.ts +0 -949
  262. package/src/component/versionMutations.ts +0 -392
  263. package/src/component/webhookTrigger.ts +0 -1922
  264. package/src/react/index.ts +0 -898
  265. package/src/test.ts +0 -1580
@@ -1,1064 +0,0 @@
1
- /**
2
- * Content Type Migration Utility
3
- *
4
- * Provides utilities to safely migrate content when content type schemas change.
5
- * Handles field additions, removals, renames, and type changes with data
6
- * transformation functions.
7
- *
8
- * Key features:
9
- * - Dry-run mode to preview changes before committing
10
- * - Custom transformation functions for type conversions
11
- * - Field renaming support
12
- * - Default value assignment for new fields
13
- * - Version snapshot creation for rollback capability
14
- * - Batch processing to respect Convex transaction limits
15
- *
16
- * @example
17
- * ```typescript
18
- * // Migrate content when changing a field from text to number
19
- * const result = await ctx.runMutation(api.contentTypeMigration.migrateContentType, {
20
- * contentTypeId: typeId,
21
- * migrations: [
22
- * {
23
- * type: "TRANSFORM_FIELD",
24
- * fieldName: "price",
25
- * transformation: "TEXT_TO_NUMBER",
26
- * },
27
- * {
28
- * type: "RENAME_FIELD",
29
- * oldFieldName: "desc",
30
- * newFieldName: "description",
31
- * },
32
- * {
33
- * type: "ADD_FIELD",
34
- * fieldName: "featured",
35
- * defaultValue: false,
36
- * },
37
- * ],
38
- * dryRun: true, // Preview changes first
39
- * migratedBy: currentUserId,
40
- * });
41
- * ```
42
- */
43
-
44
- import { v } from "convex/values";
45
- import { isDeleted } from "./lib/softDelete.js";
46
- import { mutation, query } from "./_generated/server.js";
47
- import { Id } from "./_generated/dataModel.js";
48
-
49
- // =============================================================================
50
- // Types and Interfaces
51
- // =============================================================================
52
-
53
- /**
54
- * Supported migration operation types.
55
- */
56
- export type MigrationOperationType =
57
- | "ADD_FIELD"
58
- | "REMOVE_FIELD"
59
- | "RENAME_FIELD"
60
- | "TRANSFORM_FIELD"
61
- | "SET_DEFAULT";
62
-
63
- /**
64
- * Built-in transformation types for common type conversions.
65
- */
66
- export type BuiltInTransformation =
67
- | "TEXT_TO_NUMBER"
68
- | "NUMBER_TO_TEXT"
69
- | "TEXT_TO_BOOLEAN"
70
- | "BOOLEAN_TO_TEXT"
71
- | "TEXT_TO_DATE"
72
- | "DATE_TO_TEXT"
73
- | "TEXT_TO_JSON"
74
- | "JSON_TO_TEXT"
75
- | "SINGLE_TO_ARRAY"
76
- | "ARRAY_TO_SINGLE"
77
- | "SELECT_VALUE_REMAP";
78
-
79
- /**
80
- * A migration operation to be applied to content entries.
81
- */
82
- export interface MigrationOperation {
83
- /** Type of migration operation */
84
- type: MigrationOperationType;
85
- /** Field name for ADD_FIELD, REMOVE_FIELD, TRANSFORM_FIELD, SET_DEFAULT */
86
- fieldName?: string;
87
- /** Old field name for RENAME_FIELD */
88
- oldFieldName?: string;
89
- /** New field name for RENAME_FIELD */
90
- newFieldName?: string;
91
- /** Default value for ADD_FIELD or SET_DEFAULT */
92
- defaultValue?: unknown;
93
- /** Built-in transformation type */
94
- transformation?: BuiltInTransformation;
95
- /** Custom transformation function as a string (for advanced use) */
96
- customTransformation?: string;
97
- /** Value mapping for SELECT_VALUE_REMAP transformation */
98
- valueMap?: Record<string, unknown>;
99
- /** Whether to preserve null/undefined values (don't apply default) */
100
- preserveEmpty?: boolean;
101
- }
102
-
103
- /**
104
- * Result of migrating a single entry.
105
- */
106
- export interface EntryMigrationResult {
107
- /** Entry ID */
108
- entryId: Id<"contentEntries">;
109
- /** Entry slug for identification */
110
- slug: string;
111
- /** Whether migration succeeded */
112
- success: boolean;
113
- /** Error message if failed */
114
- error?: string;
115
- /** Changes made to this entry (for dry run preview) */
116
- changes?: FieldChange[];
117
- }
118
-
119
- /**
120
- * Describes a change to a single field.
121
- */
122
- export interface FieldChange {
123
- /** Field name that was changed */
124
- fieldName: string;
125
- /** Operation that caused the change */
126
- operation: MigrationOperationType;
127
- /** Previous value (for preview) */
128
- oldValue?: unknown;
129
- /** New value (for preview) */
130
- newValue?: unknown;
131
- }
132
-
133
- /**
134
- * Result of the migration operation.
135
- */
136
- export interface MigrationResult {
137
- /** Whether this was a dry run */
138
- dryRun: boolean;
139
- /** Total entries processed */
140
- totalEntries: number;
141
- /** Entries successfully migrated */
142
- successCount: number;
143
- /** Entries that failed migration */
144
- failureCount: number;
145
- /** Entries skipped (no changes needed) */
146
- skippedCount: number;
147
- /** Detailed results per entry */
148
- results: EntryMigrationResult[];
149
- /** Version snapshots created (IDs) */
150
- versionSnapshotsCreated: number;
151
- }
152
-
153
- // =============================================================================
154
- // Transformation Functions
155
- // =============================================================================
156
-
157
- /**
158
- * Convert a text value to a number.
159
- */
160
- function textToNumber(value: unknown): unknown {
161
- if (value === null || value === undefined || value === "") {
162
- return null;
163
- }
164
- if (typeof value === "number") {
165
- return value;
166
- }
167
- if (typeof value === "string") {
168
- const trimmed = value.trim();
169
- // Handle common formats
170
- const cleaned = trimmed.replace(/[,$]/g, "");
171
- const parsed = parseFloat(cleaned);
172
- return isNaN(parsed) ? null : parsed;
173
- }
174
- return null;
175
- }
176
-
177
- /**
178
- * Convert a number value to text.
179
- */
180
- function numberToText(value: unknown): unknown {
181
- if (value === null || value === undefined) {
182
- return null;
183
- }
184
- if (typeof value === "string") {
185
- return value;
186
- }
187
- if (typeof value === "number") {
188
- return String(value);
189
- }
190
- return null;
191
- }
192
-
193
- /**
194
- * Convert a text value to boolean.
195
- */
196
- function textToBoolean(value: unknown): unknown {
197
- if (value === null || value === undefined) {
198
- return null;
199
- }
200
- if (typeof value === "boolean") {
201
- return value;
202
- }
203
- if (typeof value === "string") {
204
- const lower = value.toLowerCase().trim();
205
- if (["true", "yes", "1", "on", "enabled"].includes(lower)) {
206
- return true;
207
- }
208
- if (["false", "no", "0", "off", "disabled", ""].includes(lower)) {
209
- return false;
210
- }
211
- return null;
212
- }
213
- if (typeof value === "number") {
214
- return value !== 0;
215
- }
216
- return null;
217
- }
218
-
219
- /**
220
- * Convert a boolean value to text.
221
- */
222
- function booleanToText(value: unknown): unknown {
223
- if (value === null || value === undefined) {
224
- return null;
225
- }
226
- if (typeof value === "string") {
227
- return value;
228
- }
229
- if (typeof value === "boolean") {
230
- return value ? "true" : "false";
231
- }
232
- return null;
233
- }
234
-
235
- /**
236
- * Convert a text value to a date timestamp.
237
- */
238
- function textToDate(value: unknown): unknown {
239
- if (value === null || value === undefined || value === "") {
240
- return null;
241
- }
242
- if (typeof value === "number") {
243
- return value;
244
- }
245
- if (typeof value === "string") {
246
- const timestamp = Date.parse(value);
247
- return isNaN(timestamp) ? null : timestamp;
248
- }
249
- return null;
250
- }
251
-
252
- /**
253
- * Convert a date timestamp to text (ISO format).
254
- */
255
- function dateToText(value: unknown): unknown {
256
- if (value === null || value === undefined) {
257
- return null;
258
- }
259
- if (typeof value === "string") {
260
- return value;
261
- }
262
- if (typeof value === "number") {
263
- return new Date(value).toISOString();
264
- }
265
- return null;
266
- }
267
-
268
- /**
269
- * Convert a text value (JSON string) to parsed JSON.
270
- */
271
- function textToJson(value: unknown): unknown {
272
- if (value === null || value === undefined || value === "") {
273
- return null;
274
- }
275
- if (typeof value === "object") {
276
- return value;
277
- }
278
- if (typeof value === "string") {
279
- try {
280
- return JSON.parse(value);
281
- } catch {
282
- // If it's not valid JSON, return as-is wrapped in object
283
- return { value };
284
- }
285
- }
286
- return { value };
287
- }
288
-
289
- /**
290
- * Convert a JSON value to text (stringified).
291
- */
292
- function jsonToText(value: unknown): unknown {
293
- if (value === null || value === undefined) {
294
- return null;
295
- }
296
- if (typeof value === "string") {
297
- return value;
298
- }
299
- return JSON.stringify(value);
300
- }
301
-
302
- /**
303
- * Convert a single value to an array containing that value.
304
- */
305
- function singleToArray(value: unknown): unknown {
306
- if (value === null || value === undefined) {
307
- return [];
308
- }
309
- if (Array.isArray(value)) {
310
- return value;
311
- }
312
- return [value];
313
- }
314
-
315
- /**
316
- * Convert an array to its first element (or null if empty).
317
- */
318
- function arrayToSingle(value: unknown): unknown {
319
- if (value === null || value === undefined) {
320
- return null;
321
- }
322
- if (Array.isArray(value)) {
323
- return value.length > 0 ? value[0] : null;
324
- }
325
- return value;
326
- }
327
-
328
- /**
329
- * Remap select/multiSelect values using a provided mapping.
330
- */
331
- function selectValueRemap(
332
- value: unknown,
333
- valueMap: Record<string, unknown>,
334
- ): unknown {
335
- if (value === null || value === undefined) {
336
- return null;
337
- }
338
-
339
- if (Array.isArray(value)) {
340
- // Handle multiSelect
341
- return value.map((v) => {
342
- if (typeof v === "string" && v in valueMap) {
343
- return valueMap[v];
344
- }
345
- return v;
346
- });
347
- }
348
-
349
- if (typeof value === "string" && value in valueMap) {
350
- return valueMap[value];
351
- }
352
-
353
- return value;
354
- }
355
-
356
- /**
357
- * Apply a built-in transformation to a value.
358
- */
359
- function applyTransformation(
360
- value: unknown,
361
- transformation: BuiltInTransformation,
362
- valueMap?: Record<string, unknown>,
363
- ): unknown {
364
- switch (transformation) {
365
- case "TEXT_TO_NUMBER":
366
- return textToNumber(value);
367
- case "NUMBER_TO_TEXT":
368
- return numberToText(value);
369
- case "TEXT_TO_BOOLEAN":
370
- return textToBoolean(value);
371
- case "BOOLEAN_TO_TEXT":
372
- return booleanToText(value);
373
- case "TEXT_TO_DATE":
374
- return textToDate(value);
375
- case "DATE_TO_TEXT":
376
- return dateToText(value);
377
- case "TEXT_TO_JSON":
378
- return textToJson(value);
379
- case "JSON_TO_TEXT":
380
- return jsonToText(value);
381
- case "SINGLE_TO_ARRAY":
382
- return singleToArray(value);
383
- case "ARRAY_TO_SINGLE":
384
- return arrayToSingle(value);
385
- case "SELECT_VALUE_REMAP":
386
- return selectValueRemap(value, valueMap ?? {});
387
- default:
388
- return value;
389
- }
390
- }
391
-
392
- // =============================================================================
393
- // Migration Logic
394
- // =============================================================================
395
-
396
- /**
397
- * Apply migration operations to a single entry's data.
398
- *
399
- * @param data - The current entry data
400
- * @param operations - Migration operations to apply
401
- * @param dryRun - If true, only compute changes without modifying
402
- * @returns Object containing the migrated data and list of changes
403
- */
404
- export function applyMigrations(
405
- data: Record<string, unknown>,
406
- operations: MigrationOperation[],
407
- ): { migratedData: Record<string, unknown>; changes: FieldChange[] } {
408
- const migratedData = { ...data };
409
- const changes: FieldChange[] = [];
410
-
411
- for (const op of operations) {
412
- switch (op.type) {
413
- case "ADD_FIELD": {
414
- if (!op.fieldName) continue;
415
- const fieldName = op.fieldName;
416
-
417
- // Only add if field doesn't exist or is empty (unless preserveEmpty)
418
- const currentValue = migratedData[fieldName];
419
- const isEmpty =
420
- currentValue === undefined ||
421
- currentValue === null ||
422
- currentValue === "";
423
-
424
- if (isEmpty && !op.preserveEmpty) {
425
- const newValue = op.defaultValue;
426
- if (newValue !== undefined) {
427
- changes.push({
428
- fieldName,
429
- operation: "ADD_FIELD",
430
- oldValue: currentValue,
431
- newValue,
432
- });
433
- migratedData[fieldName] = newValue;
434
- }
435
- }
436
- break;
437
- }
438
-
439
- case "REMOVE_FIELD": {
440
- if (!op.fieldName) continue;
441
- const fieldName = op.fieldName;
442
-
443
- if (fieldName in migratedData) {
444
- changes.push({
445
- fieldName,
446
- operation: "REMOVE_FIELD",
447
- oldValue: migratedData[fieldName],
448
- newValue: undefined,
449
- });
450
- delete migratedData[fieldName];
451
- }
452
- break;
453
- }
454
-
455
- case "RENAME_FIELD": {
456
- if (!op.oldFieldName || !op.newFieldName) continue;
457
- const { oldFieldName, newFieldName } = op;
458
-
459
- if (oldFieldName in migratedData) {
460
- const value = migratedData[oldFieldName];
461
- changes.push({
462
- fieldName: oldFieldName,
463
- operation: "RENAME_FIELD",
464
- oldValue: value,
465
- newValue: value,
466
- });
467
- delete migratedData[oldFieldName];
468
- migratedData[newFieldName] = value;
469
- }
470
- break;
471
- }
472
-
473
- case "TRANSFORM_FIELD": {
474
- if (!op.fieldName) continue;
475
- const fieldName = op.fieldName;
476
-
477
- if (fieldName in migratedData && op.transformation) {
478
- const oldValue = migratedData[fieldName];
479
- const newValue = applyTransformation(
480
- oldValue,
481
- op.transformation,
482
- op.valueMap,
483
- );
484
-
485
- // Only record change if value actually changed
486
- if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
487
- changes.push({
488
- fieldName,
489
- operation: "TRANSFORM_FIELD",
490
- oldValue,
491
- newValue,
492
- });
493
- migratedData[fieldName] = newValue;
494
- }
495
- }
496
- break;
497
- }
498
-
499
- case "SET_DEFAULT": {
500
- if (!op.fieldName) continue;
501
- const fieldName = op.fieldName;
502
-
503
- const currentValue = migratedData[fieldName];
504
- const isEmpty =
505
- currentValue === undefined ||
506
- currentValue === null ||
507
- currentValue === "";
508
-
509
- if (isEmpty && !op.preserveEmpty && op.defaultValue !== undefined) {
510
- changes.push({
511
- fieldName,
512
- operation: "SET_DEFAULT",
513
- oldValue: currentValue,
514
- newValue: op.defaultValue,
515
- });
516
- migratedData[fieldName] = op.defaultValue;
517
- }
518
- break;
519
- }
520
- }
521
- }
522
-
523
- return { migratedData, changes };
524
- }
525
-
526
- // =============================================================================
527
- // Validators
528
- // =============================================================================
529
-
530
- /**
531
- * Validator for migration operation type.
532
- */
533
- const migrationOperationTypeValidator = v.union(
534
- v.literal("ADD_FIELD"),
535
- v.literal("REMOVE_FIELD"),
536
- v.literal("RENAME_FIELD"),
537
- v.literal("TRANSFORM_FIELD"),
538
- v.literal("SET_DEFAULT"),
539
- );
540
-
541
- /**
542
- * Validator for built-in transformation types.
543
- */
544
- const builtInTransformationValidator = v.union(
545
- v.literal("TEXT_TO_NUMBER"),
546
- v.literal("NUMBER_TO_TEXT"),
547
- v.literal("TEXT_TO_BOOLEAN"),
548
- v.literal("BOOLEAN_TO_TEXT"),
549
- v.literal("TEXT_TO_DATE"),
550
- v.literal("DATE_TO_TEXT"),
551
- v.literal("TEXT_TO_JSON"),
552
- v.literal("JSON_TO_TEXT"),
553
- v.literal("SINGLE_TO_ARRAY"),
554
- v.literal("ARRAY_TO_SINGLE"),
555
- v.literal("SELECT_VALUE_REMAP"),
556
- );
557
-
558
- /**
559
- * Validator for a migration operation.
560
- */
561
- export const migrationOperationValidator = v.object({
562
- type: migrationOperationTypeValidator,
563
- fieldName: v.optional(v.string()),
564
- oldFieldName: v.optional(v.string()),
565
- newFieldName: v.optional(v.string()),
566
- defaultValue: v.optional(v.any()),
567
- transformation: v.optional(builtInTransformationValidator),
568
- customTransformation: v.optional(v.string()),
569
- valueMap: v.optional(v.any()),
570
- preserveEmpty: v.optional(v.boolean()),
571
- });
572
-
573
- /**
574
- * Validator for migrate content type arguments.
575
- */
576
- export const migrateContentTypeArgs = v.object({
577
- /** Content type ID to migrate */
578
- contentTypeId: v.id("contentTypes"),
579
- /** Array of migration operations to apply */
580
- migrations: v.array(migrationOperationValidator),
581
- /** If true, preview changes without applying them */
582
- dryRun: v.optional(v.boolean()),
583
- /** Create version snapshots before migration (default: true) */
584
- createVersionSnapshots: v.optional(v.boolean()),
585
- /** Filter entries by status (default: all statuses) */
586
- statusFilter: v.optional(
587
- v.array(
588
- v.union(
589
- v.literal("draft"),
590
- v.literal("published"),
591
- v.literal("archived"),
592
- v.literal("scheduled"),
593
- ),
594
- ),
595
- ),
596
- /** Only migrate entries with IDs in this list */
597
- entryIds: v.optional(v.array(v.id("contentEntries"))),
598
- /** User performing the migration */
599
- migratedBy: v.optional(v.string()),
600
- /** Description of the migration for version history */
601
- changeDescription: v.optional(v.string()),
602
- });
603
-
604
- /**
605
- * Validator for entry migration result.
606
- */
607
- const fieldChangeValidator = v.object({
608
- fieldName: v.string(),
609
- operation: migrationOperationTypeValidator,
610
- oldValue: v.optional(v.any()),
611
- newValue: v.optional(v.any()),
612
- });
613
-
614
- const entryMigrationResultValidator = v.object({
615
- entryId: v.id("contentEntries"),
616
- slug: v.string(),
617
- success: v.boolean(),
618
- error: v.optional(v.string()),
619
- changes: v.optional(v.array(fieldChangeValidator)),
620
- });
621
-
622
- /**
623
- * Validator for migration result.
624
- */
625
- export const migrationResultValidator = v.object({
626
- dryRun: v.boolean(),
627
- totalEntries: v.number(),
628
- successCount: v.number(),
629
- failureCount: v.number(),
630
- skippedCount: v.number(),
631
- results: v.array(entryMigrationResultValidator),
632
- versionSnapshotsCreated: v.number(),
633
- });
634
-
635
- // =============================================================================
636
- // Mutations
637
- // =============================================================================
638
-
639
- /**
640
- * Mutation to migrate content entries when a content type schema changes.
641
- *
642
- * This mutation applies a series of migration operations to all entries
643
- * of a given content type. It supports:
644
- * - Adding new fields with default values
645
- * - Removing fields
646
- * - Renaming fields
647
- * - Transforming field values (type conversions)
648
- * - Setting default values for empty fields
649
- *
650
- * The mutation can run in dry-run mode to preview changes before applying them.
651
- * By default, it creates version snapshots before modifying entries.
652
- *
653
- * @param contentTypeId - The content type to migrate
654
- * @param migrations - Array of migration operations to apply
655
- * @param dryRun - If true, preview changes without applying (default: false)
656
- * @param createVersionSnapshots - Create version snapshots before migration (default: true)
657
- * @param statusFilter - Only migrate entries with these statuses
658
- * @param entryIds - Only migrate entries with these IDs
659
- * @param migratedBy - User performing the migration
660
- * @param changeDescription - Description for version history
661
- *
662
- * @returns MigrationResult with details of all changes
663
- *
664
- * @example
665
- * ```typescript
666
- * // Preview migration
667
- * const preview = await ctx.runMutation(api.contentTypeMigration.migrateContentType, {
668
- * contentTypeId: blogPostTypeId,
669
- * migrations: [
670
- * { type: "RENAME_FIELD", oldFieldName: "body", newFieldName: "content" },
671
- * { type: "ADD_FIELD", fieldName: "featured", defaultValue: false },
672
- * { type: "TRANSFORM_FIELD", fieldName: "viewCount", transformation: "TEXT_TO_NUMBER" },
673
- * ],
674
- * dryRun: true,
675
- * });
676
- *
677
- * // Apply migration after reviewing preview
678
- * const result = await ctx.runMutation(api.contentTypeMigration.migrateContentType, {
679
- * contentTypeId: blogPostTypeId,
680
- * migrations: [...],
681
- * dryRun: false,
682
- * changeDescription: "Renamed body to content, added featured flag",
683
- * migratedBy: currentUserId,
684
- * });
685
- * ```
686
- */
687
- export const migrateContentType = mutation({
688
- args: migrateContentTypeArgs.fields,
689
- returns: migrationResultValidator,
690
- handler: async (ctx, args): Promise<MigrationResult> => {
691
- const {
692
- contentTypeId,
693
- migrations,
694
- dryRun = false,
695
- createVersionSnapshots = true,
696
- statusFilter,
697
- entryIds,
698
- migratedBy,
699
- changeDescription = "Content type migration",
700
- } = args;
701
-
702
- // Validate content type exists
703
- const contentType = await ctx.db.get(contentTypeId);
704
- if (!contentType) {
705
- throw new Error(`Content type not found: ${contentTypeId}`);
706
- }
707
- if (isDeleted(contentType)) {
708
- throw new Error(`Content type has been deleted: ${contentType.name}`);
709
- }
710
-
711
- // Validate migration operations
712
- for (const op of migrations) {
713
- if (
714
- op.type === "RENAME_FIELD" &&
715
- (!op.oldFieldName || !op.newFieldName)
716
- ) {
717
- throw new Error(
718
- "RENAME_FIELD operation requires both oldFieldName and newFieldName",
719
- );
720
- }
721
- if (
722
- (op.type === "ADD_FIELD" ||
723
- op.type === "REMOVE_FIELD" ||
724
- op.type === "TRANSFORM_FIELD" ||
725
- op.type === "SET_DEFAULT") &&
726
- !op.fieldName
727
- ) {
728
- throw new Error(`${op.type} operation requires fieldName`);
729
- }
730
- if (op.type === "TRANSFORM_FIELD" && !op.transformation) {
731
- throw new Error(
732
- "TRANSFORM_FIELD operation requires transformation type",
733
- );
734
- }
735
- }
736
-
737
- // Build query for entries
738
- const entriesQuery = ctx.db
739
- .query("contentEntries")
740
- .withIndex("by_content_type", (q) => q.eq("contentTypeId", contentTypeId))
741
- .filter((q) => q.eq(q.field("deletedAt"), undefined));
742
-
743
- // Collect all entries
744
- const allEntries = await entriesQuery.collect();
745
-
746
- // Filter by status if specified
747
- let entries = allEntries;
748
- if (statusFilter && statusFilter.length > 0) {
749
- entries = entries.filter((e) =>
750
- statusFilter.includes(
751
- e.status as "draft" | "published" | "archived" | "scheduled",
752
- ),
753
- );
754
- }
755
-
756
- // Filter by entry IDs if specified
757
- if (entryIds && entryIds.length > 0) {
758
- const entryIdSet = new Set(entryIds.map((id) => id.toString()));
759
- entries = entries.filter((e) => entryIdSet.has(e._id.toString()));
760
- }
761
-
762
- // Process entries
763
- const results: EntryMigrationResult[] = [];
764
- let successCount = 0;
765
- let failureCount = 0;
766
- let skippedCount = 0;
767
- let versionSnapshotsCreated = 0;
768
-
769
- for (const entry of entries) {
770
- try {
771
- const entryData = entry.data as Record<string, unknown>;
772
- const { migratedData, changes } = applyMigrations(
773
- entryData,
774
- migrations as MigrationOperation[],
775
- );
776
-
777
- // Skip if no changes
778
- if (changes.length === 0) {
779
- results.push({
780
- entryId: entry._id,
781
- slug: entry.slug,
782
- success: true,
783
- changes: [],
784
- });
785
- skippedCount++;
786
- continue;
787
- }
788
-
789
- if (dryRun) {
790
- // In dry run mode, just report what would change
791
- results.push({
792
- entryId: entry._id,
793
- slug: entry.slug,
794
- success: true,
795
- changes,
796
- });
797
- successCount++;
798
- } else {
799
- // Create version snapshot before migration
800
- if (createVersionSnapshots) {
801
- await ctx.db.insert("contentVersions", {
802
- entryId: entry._id,
803
- versionNumber: entry.version,
804
- data: entry.data,
805
- slug: entry.slug,
806
- status: entry.status,
807
- changeDescription: `Pre-migration snapshot: ${changeDescription}`,
808
- createdBy: migratedBy,
809
- wasPublished: entry.status === "published",
810
- publishedAt:
811
- entry.status === "published"
812
- ? entry.lastPublishedAt
813
- : undefined,
814
- });
815
- versionSnapshotsCreated++;
816
- }
817
-
818
- // Apply migration
819
- await ctx.db.patch(entry._id, {
820
- data: migratedData,
821
- version: entry.version + 1,
822
- updatedBy: migratedBy,
823
- });
824
-
825
- results.push({
826
- entryId: entry._id,
827
- slug: entry.slug,
828
- success: true,
829
- changes,
830
- });
831
- successCount++;
832
- }
833
- } catch (error) {
834
- results.push({
835
- entryId: entry._id,
836
- slug: entry.slug,
837
- success: false,
838
- error: error instanceof Error ? error.message : "Unknown error",
839
- });
840
- failureCount++;
841
- }
842
- }
843
-
844
- return {
845
- dryRun,
846
- totalEntries: entries.length,
847
- successCount,
848
- failureCount,
849
- skippedCount,
850
- results,
851
- versionSnapshotsCreated,
852
- };
853
- },
854
- });
855
-
856
- // =============================================================================
857
- // Queries
858
- // =============================================================================
859
-
860
- /**
861
- * Query to preview migration without modifying data.
862
- *
863
- * This is a convenience wrapper that always runs in dry-run mode.
864
- * Use this to safely preview what changes would be made.
865
- */
866
- export const previewMigration = query({
867
- args: {
868
- contentTypeId: v.id("contentTypes"),
869
- migrations: v.array(migrationOperationValidator),
870
- statusFilter: v.optional(
871
- v.array(
872
- v.union(
873
- v.literal("draft"),
874
- v.literal("published"),
875
- v.literal("archived"),
876
- v.literal("scheduled"),
877
- ),
878
- ),
879
- ),
880
- entryIds: v.optional(v.array(v.id("contentEntries"))),
881
- /** Limit number of entries to preview (default: 10) */
882
- limit: v.optional(v.number()),
883
- },
884
- returns: v.object({
885
- totalEntries: v.number(),
886
- previewedEntries: v.number(),
887
- results: v.array(entryMigrationResultValidator),
888
- summary: v.object({
889
- entriesWithChanges: v.number(),
890
- entriesWithoutChanges: v.number(),
891
- operationCounts: v.any(),
892
- }),
893
- }),
894
- handler: async (ctx, args) => {
895
- const {
896
- contentTypeId,
897
- migrations,
898
- statusFilter,
899
- entryIds,
900
- limit = 10,
901
- } = args;
902
-
903
- // Validate content type exists
904
- const contentType = await ctx.db.get(contentTypeId);
905
- if (!contentType) {
906
- throw new Error(`Content type not found: ${contentTypeId}`);
907
- }
908
-
909
- // Build query for entries
910
- let entries = await ctx.db
911
- .query("contentEntries")
912
- .withIndex("by_content_type", (q) => q.eq("contentTypeId", contentTypeId))
913
- .filter((q) => q.eq(q.field("deletedAt"), undefined))
914
- .collect();
915
-
916
- const totalEntries = entries.length;
917
-
918
- // Filter by status if specified
919
- if (statusFilter && statusFilter.length > 0) {
920
- entries = entries.filter((e) =>
921
- statusFilter.includes(
922
- e.status as "draft" | "published" | "archived" | "scheduled",
923
- ),
924
- );
925
- }
926
-
927
- // Filter by entry IDs if specified
928
- if (entryIds && entryIds.length > 0) {
929
- const entryIdSet = new Set(entryIds.map((id) => id.toString()));
930
- entries = entries.filter((e) => entryIdSet.has(e._id.toString()));
931
- }
932
-
933
- // Limit entries for preview
934
- const previewEntries = entries.slice(0, limit);
935
- const results: EntryMigrationResult[] = [];
936
- const operationCounts: Record<string, number> = {};
937
- let entriesWithChanges = 0;
938
- let entriesWithoutChanges = 0;
939
-
940
- for (const entry of previewEntries) {
941
- const entryData = entry.data as Record<string, unknown>;
942
- const { changes } = applyMigrations(
943
- entryData,
944
- migrations as MigrationOperation[],
945
- );
946
-
947
- if (changes.length > 0) {
948
- entriesWithChanges++;
949
- for (const change of changes) {
950
- operationCounts[change.operation] =
951
- (operationCounts[change.operation] || 0) + 1;
952
- }
953
- } else {
954
- entriesWithoutChanges++;
955
- }
956
-
957
- results.push({
958
- entryId: entry._id,
959
- slug: entry.slug,
960
- success: true,
961
- changes,
962
- });
963
- }
964
-
965
- return {
966
- totalEntries,
967
- previewedEntries: previewEntries.length,
968
- results,
969
- summary: {
970
- entriesWithChanges,
971
- entriesWithoutChanges,
972
- operationCounts,
973
- },
974
- };
975
- },
976
- });
977
-
978
- /**
979
- * Query to get available transformation types and their descriptions.
980
- */
981
- export const getTransformationTypes = query({
982
- args: {},
983
- returns: v.array(
984
- v.object({
985
- type: v.string(),
986
- description: v.string(),
987
- fromType: v.string(),
988
- toType: v.string(),
989
- }),
990
- ),
991
- handler: async () => {
992
- return [
993
- {
994
- type: "TEXT_TO_NUMBER",
995
- description:
996
- "Convert text strings to numbers (handles currency formatting)",
997
- fromType: "text",
998
- toType: "number",
999
- },
1000
- {
1001
- type: "NUMBER_TO_TEXT",
1002
- description: "Convert numbers to text strings",
1003
- fromType: "number",
1004
- toType: "text",
1005
- },
1006
- {
1007
- type: "TEXT_TO_BOOLEAN",
1008
- description:
1009
- "Convert text to boolean (true/false, yes/no, 1/0, on/off, enabled/disabled)",
1010
- fromType: "text",
1011
- toType: "boolean",
1012
- },
1013
- {
1014
- type: "BOOLEAN_TO_TEXT",
1015
- description: 'Convert boolean to "true" or "false" strings',
1016
- fromType: "boolean",
1017
- toType: "text",
1018
- },
1019
- {
1020
- type: "TEXT_TO_DATE",
1021
- description:
1022
- "Convert date strings to timestamps (ISO 8601 and common formats)",
1023
- fromType: "text",
1024
- toType: "date/datetime",
1025
- },
1026
- {
1027
- type: "DATE_TO_TEXT",
1028
- description: "Convert timestamps to ISO 8601 date strings",
1029
- fromType: "date/datetime",
1030
- toType: "text",
1031
- },
1032
- {
1033
- type: "TEXT_TO_JSON",
1034
- description: "Parse JSON strings to objects",
1035
- fromType: "text",
1036
- toType: "json",
1037
- },
1038
- {
1039
- type: "JSON_TO_TEXT",
1040
- description: "Stringify JSON objects to text",
1041
- fromType: "json",
1042
- toType: "text",
1043
- },
1044
- {
1045
- type: "SINGLE_TO_ARRAY",
1046
- description: "Wrap single values in an array (for multiple references)",
1047
- fromType: "any",
1048
- toType: "array",
1049
- },
1050
- {
1051
- type: "ARRAY_TO_SINGLE",
1052
- description: "Extract first element from array (for single references)",
1053
- fromType: "array",
1054
- toType: "any",
1055
- },
1056
- {
1057
- type: "SELECT_VALUE_REMAP",
1058
- description: "Remap select/multiSelect values using a provided mapping",
1059
- fromType: "select/multiSelect",
1060
- toType: "select/multiSelect",
1061
- },
1062
- ];
1063
- },
1064
- });