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.
- package/admin-dist/nitro.json +15 -0
- package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
- package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
- package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
- package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
- package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
- package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
- package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
- package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
- package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
- package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
- package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
- package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
- package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
- package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
- package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
- package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
- package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
- package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
- package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
- package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
- package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
- package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
- package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
- package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
- package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
- package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
- package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
- package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
- package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
- package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
- package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
- package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
- package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
- package/admin-dist/public/favicon.ico +0 -0
- package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
- package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
- package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
- package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
- package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
- package/admin-dist/server/_libs/clsx.mjs +16 -0
- package/admin-dist/server/_libs/cmdk.mjs +315 -0
- package/admin-dist/server/_libs/convex.mjs +4841 -0
- package/admin-dist/server/_libs/cookie-es.mjs +58 -0
- package/admin-dist/server/_libs/croner.mjs +1 -0
- package/admin-dist/server/_libs/crossws.mjs +1 -0
- package/admin-dist/server/_libs/date-fns.mjs +1716 -0
- package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
- package/admin-dist/server/_libs/get-nonce.mjs +9 -0
- package/admin-dist/server/_libs/h3-v2.mjs +277 -0
- package/admin-dist/server/_libs/h3.mjs +401 -0
- package/admin-dist/server/_libs/hookable.mjs +1 -0
- package/admin-dist/server/_libs/isbot.mjs +20 -0
- package/admin-dist/server/_libs/lucide-react.mjs +850 -0
- package/admin-dist/server/_libs/ohash.mjs +1 -0
- package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
- package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
- package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
- package/admin-dist/server/_libs/rou3.mjs +8 -0
- package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
- package/admin-dist/server/_libs/seroval.mjs +1765 -0
- package/admin-dist/server/_libs/srvx.mjs +719 -0
- package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
- package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
- package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
- package/admin-dist/server/_libs/tslib.mjs +39 -0
- package/admin-dist/server/_libs/ufo.mjs +54 -0
- package/admin-dist/server/_libs/unctx.mjs +1 -0
- package/admin-dist/server/_libs/unstorage.mjs +1 -0
- package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
- package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
- package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
- package/admin-dist/server/_libs/zod.mjs +4223 -0
- package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
- package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
- package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
- package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
- package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
- package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
- package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
- package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
- package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
- package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
- package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
- package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
- package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
- package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
- package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
- package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
- package/admin-dist/server/_ssr/index.mjs +1275 -0
- package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
- package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
- package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
- package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
- package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
- package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
- package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
- package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
- package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
- package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
- package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
- package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
- package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
- package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
- package/admin-dist/server/favicon.ico +0 -0
- package/admin-dist/server/index.mjs +627 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/admin-config.d.ts +0 -1
- package/dist/client/admin-config.d.ts.map +1 -1
- package/dist/client/admin-config.js +0 -1
- package/dist/client/admin-config.js.map +1 -1
- package/dist/client/adminApi.d.ts.map +1 -1
- package/dist/client/agentTools.d.ts +1237 -135
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/agentTools.js +33 -9
- package/dist/client/agentTools.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +9 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/mediaAssets.d.ts +35 -0
- package/dist/component/mediaAssets.d.ts.map +1 -1
- package/dist/component/mediaAssets.js +81 -0
- package/dist/component/mediaAssets.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -1
- package/dist/test.js.map +1 -1
- package/package.json +9 -5
- package/dist/component/auditLog.d.ts +0 -410
- package/dist/component/auditLog.d.ts.map +0 -1
- package/dist/component/auditLog.js +0 -607
- package/dist/component/auditLog.js.map +0 -1
- package/dist/component/types.d.ts +0 -4
- package/dist/component/types.d.ts.map +0 -1
- package/dist/component/types.js +0 -2
- package/dist/component/types.js.map +0 -1
- package/src/cli/commands/admin.ts +0 -104
- package/src/cli/index.ts +0 -21
- package/src/cli/utils/detectConvexUrl.ts +0 -54
- package/src/cli/utils/openBrowser.ts +0 -16
- package/src/client/admin-config.ts +0 -138
- package/src/client/adminApi.ts +0 -942
- package/src/client/agentTools.ts +0 -1311
- package/src/client/argTypes.ts +0 -316
- package/src/client/field-types.ts +0 -187
- package/src/client/index.ts +0 -1301
- package/src/client/queryBuilder.ts +0 -1100
- package/src/client/schema/codegen.ts +0 -500
- package/src/client/schema/defineContentType.ts +0 -501
- package/src/client/schema/index.ts +0 -169
- package/src/client/schema/schemaDrift.ts +0 -574
- package/src/client/schema/typedClient.ts +0 -688
- package/src/client/schema/types.ts +0 -666
- package/src/client/types.ts +0 -723
- package/src/client/workflows.ts +0 -141
- package/src/client/wrapper.ts +0 -4304
- package/src/component/_generated/api.ts +0 -140
- package/src/component/_generated/component.ts +0 -5029
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/authorization.ts +0 -647
- package/src/component/authorizationHooks.ts +0 -668
- package/src/component/bulkOperations.ts +0 -687
- package/src/component/contentEntries.ts +0 -1976
- package/src/component/contentEntryMutations.ts +0 -1223
- package/src/component/contentEntryValidation.ts +0 -707
- package/src/component/contentLock.ts +0 -550
- package/src/component/contentTypeMigration.ts +0 -1064
- package/src/component/contentTypeMutations.ts +0 -969
- package/src/component/contentTypes.ts +0 -346
- package/src/component/convex.config.ts +0 -44
- package/src/component/documentTypes.ts +0 -240
- package/src/component/eventEmitter.ts +0 -485
- package/src/component/exportImport.ts +0 -1169
- package/src/component/index.ts +0 -491
- package/src/component/lib/deepReferenceResolver.ts +0 -999
- package/src/component/lib/errors.ts +0 -816
- package/src/component/lib/index.ts +0 -145
- package/src/component/lib/mediaReferenceResolver.ts +0 -495
- package/src/component/lib/metadataExtractor.ts +0 -792
- package/src/component/lib/mutationAuth.ts +0 -199
- package/src/component/lib/queries.ts +0 -79
- package/src/component/lib/ragContentChunker.ts +0 -1371
- package/src/component/lib/referenceResolver.ts +0 -430
- package/src/component/lib/slugGenerator.ts +0 -262
- package/src/component/lib/slugUniqueness.ts +0 -333
- package/src/component/lib/softDelete.ts +0 -44
- package/src/component/localeFallbackChain.ts +0 -673
- package/src/component/localeFields.ts +0 -896
- package/src/component/mediaAssetMutations.ts +0 -725
- package/src/component/mediaAssets.ts +0 -932
- package/src/component/mediaFolderMutations.ts +0 -1046
- package/src/component/mediaUploadMutations.ts +0 -224
- package/src/component/mediaVariantMutations.ts +0 -900
- package/src/component/mediaVariants.ts +0 -793
- package/src/component/ragContentIndexer.ts +0 -1067
- package/src/component/rateLimitHooks.ts +0 -572
- package/src/component/roles.ts +0 -1360
- package/src/component/scheduledPublish.ts +0 -358
- package/src/component/schema.ts +0 -617
- package/src/component/taxonomies.ts +0 -949
- package/src/component/taxonomyMutations.ts +0 -1210
- package/src/component/trash.ts +0 -724
- package/src/component/userContext.ts +0 -898
- package/src/component/validation.ts +0 -1388
- package/src/component/validators.ts +0 -949
- package/src/component/versionMutations.ts +0 -392
- package/src/component/webhookTrigger.ts +0 -1922
- package/src/react/index.ts +0 -898
- 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
|
-
});
|