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,687 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bulk Operations for Content Entries
|
|
3
|
-
*
|
|
4
|
-
* Provides mutations for performing bulk operations on content entries:
|
|
5
|
-
* - bulkPublish: Publish multiple entries at once
|
|
6
|
-
* - bulkUnpublish: Revert multiple entries to draft
|
|
7
|
-
* - bulkDelete: Delete multiple entries (soft or hard)
|
|
8
|
-
* - bulkUpdate: Update multiple entries with the same changes
|
|
9
|
-
*
|
|
10
|
-
* All operations process entries in a single transaction for atomicity,
|
|
11
|
-
* respecting Convex limits (max 16,000 documents written per transaction).
|
|
12
|
-
* The BULK_OPERATION_BATCH_SIZE constant defines the maximum entries per call.
|
|
13
|
-
*
|
|
14
|
-
* For larger datasets, callers should batch IDs into chunks of BULK_OPERATION_BATCH_SIZE
|
|
15
|
-
* and call the appropriate bulk operation for each batch.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { v } from "convex/values";
|
|
19
|
-
import { mutation } from "./_generated/server.js";
|
|
20
|
-
import {
|
|
21
|
-
bulkPublishArgs,
|
|
22
|
-
bulkUnpublishArgs,
|
|
23
|
-
bulkDeleteArgs,
|
|
24
|
-
bulkUpdateArgs,
|
|
25
|
-
bulkOperationResult,
|
|
26
|
-
BULK_OPERATION_BATCH_SIZE,
|
|
27
|
-
} from "./validators.js";
|
|
28
|
-
import {
|
|
29
|
-
validateContentData,
|
|
30
|
-
ContentTypeSchema,
|
|
31
|
-
FieldDefinition,
|
|
32
|
-
} from "./validation.js";
|
|
33
|
-
import { Id } from "./_generated/dataModel.js";
|
|
34
|
-
import { isDeleted } from "./lib/softDelete.js";
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// Types
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
interface BulkOperationItemResult {
|
|
41
|
-
id: Id<"contentEntries">;
|
|
42
|
-
success: boolean;
|
|
43
|
-
error?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface BulkOperationResult {
|
|
47
|
-
total: number;
|
|
48
|
-
succeeded: number;
|
|
49
|
-
failed: number;
|
|
50
|
-
results: BulkOperationItemResult[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// =============================================================================
|
|
54
|
-
// Bulk Publish Mutation
|
|
55
|
-
// =============================================================================
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Mutation to publish multiple content entries in a single transaction.
|
|
59
|
-
*
|
|
60
|
-
* Publishes entries that are in draft or scheduled status. Already published
|
|
61
|
-
* entries are skipped with a success status. Deleted or archived entries
|
|
62
|
-
* will fail with an error message.
|
|
63
|
-
*
|
|
64
|
-
* For each entry published:
|
|
65
|
-
* - Status is set to "published"
|
|
66
|
-
* - firstPublishedAt is set (if first publication)
|
|
67
|
-
* - lastPublishedAt is updated
|
|
68
|
-
* - Version is incremented
|
|
69
|
-
* - A version snapshot is created
|
|
70
|
-
*
|
|
71
|
-
* @param ids - Array of content entry IDs to publish (max BULK_OPERATION_BATCH_SIZE)
|
|
72
|
-
* @param changeDescription - Optional description for version history
|
|
73
|
-
* @param updatedBy - User ID for audit trail
|
|
74
|
-
*
|
|
75
|
-
* @returns BulkOperationResult with success/failure details for each entry
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```typescript
|
|
79
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkPublish, {
|
|
80
|
-
* ids: [entry1._id, entry2._id, entry3._id],
|
|
81
|
-
* changeDescription: "Publishing launch content",
|
|
82
|
-
* updatedBy: currentUserId,
|
|
83
|
-
* });
|
|
84
|
-
* console.log(`Published ${result.succeeded} of ${result.total} entries`);
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
export const bulkPublish = mutation({
|
|
88
|
-
args: bulkPublishArgs.fields,
|
|
89
|
-
returns: bulkOperationResult,
|
|
90
|
-
handler: async (ctx, args): Promise<BulkOperationResult> => {
|
|
91
|
-
const { ids, changeDescription, updatedBy } = args;
|
|
92
|
-
|
|
93
|
-
// Validate batch size
|
|
94
|
-
if (ids.length > BULK_OPERATION_BATCH_SIZE) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Batch size exceeds limit. Maximum ${BULK_OPERATION_BATCH_SIZE} entries per operation, got ${ids.length}.`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (ids.length === 0) {
|
|
101
|
-
return { total: 0, succeeded: 0, failed: 0, results: [] };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const results: BulkOperationItemResult[] = [];
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
|
|
107
|
-
for (const id of ids) {
|
|
108
|
-
try {
|
|
109
|
-
const entry = await ctx.db.get(id);
|
|
110
|
-
|
|
111
|
-
if (!entry) {
|
|
112
|
-
results.push({ id, success: false, error: "Entry not found" });
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (isDeleted(entry)) {
|
|
117
|
-
results.push({ id, success: false, error: "Entry has been deleted" });
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (entry.status === "published") {
|
|
122
|
-
// Already published - treat as success (idempotent)
|
|
123
|
-
results.push({ id, success: true });
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (entry.status === "archived") {
|
|
128
|
-
results.push({
|
|
129
|
-
id,
|
|
130
|
-
success: false,
|
|
131
|
-
error: "Cannot publish archived content. Restore it first.",
|
|
132
|
-
});
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Create version snapshot before publishing
|
|
137
|
-
await ctx.db.insert("contentVersions", {
|
|
138
|
-
entryId: id,
|
|
139
|
-
versionNumber: entry.version,
|
|
140
|
-
data: entry.data,
|
|
141
|
-
slug: entry.slug,
|
|
142
|
-
status: entry.status,
|
|
143
|
-
changeDescription,
|
|
144
|
-
createdBy: updatedBy,
|
|
145
|
-
wasPublished: true,
|
|
146
|
-
publishedAt: now,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Build the update object
|
|
150
|
-
const updates: Record<string, unknown> = {
|
|
151
|
-
status: "published",
|
|
152
|
-
lastPublishedAt: now,
|
|
153
|
-
version: entry.version + 1,
|
|
154
|
-
updatedBy,
|
|
155
|
-
scheduledPublishAt: undefined,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Set firstPublishedAt only on first publication
|
|
159
|
-
if (entry.firstPublishedAt === undefined) {
|
|
160
|
-
updates.firstPublishedAt = now;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
await ctx.db.patch(id, updates);
|
|
164
|
-
results.push({ id, success: true });
|
|
165
|
-
} catch (error) {
|
|
166
|
-
results.push({
|
|
167
|
-
id,
|
|
168
|
-
success: false,
|
|
169
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const succeeded = results.filter((r) => r.success).length;
|
|
175
|
-
return {
|
|
176
|
-
total: ids.length,
|
|
177
|
-
succeeded,
|
|
178
|
-
failed: ids.length - succeeded,
|
|
179
|
-
results,
|
|
180
|
-
};
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// =============================================================================
|
|
185
|
-
// Bulk Unpublish Mutation
|
|
186
|
-
// =============================================================================
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Mutation to unpublish multiple content entries in a single transaction.
|
|
190
|
-
*
|
|
191
|
-
* Reverts published entries to draft status. Non-published entries are
|
|
192
|
-
* skipped with a success status (idempotent behavior). Deleted entries
|
|
193
|
-
* will fail with an error message.
|
|
194
|
-
*
|
|
195
|
-
* For each entry unpublished:
|
|
196
|
-
* - Status is set to "draft"
|
|
197
|
-
* - Version is incremented
|
|
198
|
-
* - Publication timestamps are preserved for history
|
|
199
|
-
*
|
|
200
|
-
* @param ids - Array of content entry IDs to unpublish (max BULK_OPERATION_BATCH_SIZE)
|
|
201
|
-
* @param updatedBy - User ID for audit trail
|
|
202
|
-
*
|
|
203
|
-
* @returns BulkOperationResult with success/failure details for each entry
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```typescript
|
|
207
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkUnpublish, {
|
|
208
|
-
* ids: [entry1._id, entry2._id],
|
|
209
|
-
* updatedBy: currentUserId,
|
|
210
|
-
* });
|
|
211
|
-
* ```
|
|
212
|
-
*/
|
|
213
|
-
export const bulkUnpublish = mutation({
|
|
214
|
-
args: bulkUnpublishArgs.fields,
|
|
215
|
-
returns: bulkOperationResult,
|
|
216
|
-
handler: async (ctx, args): Promise<BulkOperationResult> => {
|
|
217
|
-
const { ids, updatedBy } = args;
|
|
218
|
-
|
|
219
|
-
// Validate batch size
|
|
220
|
-
if (ids.length > BULK_OPERATION_BATCH_SIZE) {
|
|
221
|
-
throw new Error(
|
|
222
|
-
`Batch size exceeds limit. Maximum ${BULK_OPERATION_BATCH_SIZE} entries per operation, got ${ids.length}.`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (ids.length === 0) {
|
|
227
|
-
return { total: 0, succeeded: 0, failed: 0, results: [] };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const results: BulkOperationItemResult[] = [];
|
|
231
|
-
|
|
232
|
-
for (const id of ids) {
|
|
233
|
-
try {
|
|
234
|
-
const entry = await ctx.db.get(id);
|
|
235
|
-
|
|
236
|
-
if (!entry) {
|
|
237
|
-
results.push({ id, success: false, error: "Entry not found" });
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (isDeleted(entry)) {
|
|
242
|
-
results.push({ id, success: false, error: "Entry has been deleted" });
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (entry.status !== "published") {
|
|
247
|
-
// Not published - treat as success (idempotent)
|
|
248
|
-
results.push({ id, success: true });
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
await ctx.db.patch(id, {
|
|
253
|
-
status: "draft",
|
|
254
|
-
version: entry.version + 1,
|
|
255
|
-
updatedBy,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
results.push({ id, success: true });
|
|
259
|
-
} catch (error) {
|
|
260
|
-
results.push({
|
|
261
|
-
id,
|
|
262
|
-
success: false,
|
|
263
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const succeeded = results.filter((r) => r.success).length;
|
|
269
|
-
return {
|
|
270
|
-
total: ids.length,
|
|
271
|
-
succeeded,
|
|
272
|
-
failed: ids.length - succeeded,
|
|
273
|
-
results,
|
|
274
|
-
};
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// =============================================================================
|
|
279
|
-
// Bulk Delete Mutation
|
|
280
|
-
// =============================================================================
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Mutation to delete multiple content entries in a single transaction.
|
|
284
|
-
*
|
|
285
|
-
* By default, performs soft delete by setting deletedAt timestamp.
|
|
286
|
-
* When hardDelete is true, permanently removes entries and all versions.
|
|
287
|
-
*
|
|
288
|
-
* Soft Delete:
|
|
289
|
-
* - Sets deletedAt timestamp
|
|
290
|
-
* - Entries can be restored later
|
|
291
|
-
* - Already deleted entries are skipped
|
|
292
|
-
*
|
|
293
|
-
* Hard Delete:
|
|
294
|
-
* - Permanently removes entry document
|
|
295
|
-
* - Deletes all version snapshots
|
|
296
|
-
* - Cannot be undone
|
|
297
|
-
*
|
|
298
|
-
* @param ids - Array of content entry IDs to delete (max BULK_OPERATION_BATCH_SIZE)
|
|
299
|
-
* @param deletedBy - User ID for audit trail
|
|
300
|
-
* @param hardDelete - If true, permanently delete entries and versions
|
|
301
|
-
*
|
|
302
|
-
* @returns BulkOperationResult with success/failure details for each entry
|
|
303
|
-
*
|
|
304
|
-
* @example
|
|
305
|
-
* ```typescript
|
|
306
|
-
* // Soft delete (default)
|
|
307
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkDelete, {
|
|
308
|
-
* ids: [entry1._id, entry2._id],
|
|
309
|
-
* deletedBy: currentUserId,
|
|
310
|
-
* });
|
|
311
|
-
*
|
|
312
|
-
* // Hard delete
|
|
313
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkDelete, {
|
|
314
|
-
* ids: [entry1._id, entry2._id],
|
|
315
|
-
* deletedBy: currentUserId,
|
|
316
|
-
* hardDelete: true,
|
|
317
|
-
* });
|
|
318
|
-
* ```
|
|
319
|
-
*/
|
|
320
|
-
export const bulkDelete = mutation({
|
|
321
|
-
args: bulkDeleteArgs.fields,
|
|
322
|
-
returns: bulkOperationResult,
|
|
323
|
-
handler: async (ctx, args): Promise<BulkOperationResult> => {
|
|
324
|
-
const { ids, deletedBy, hardDelete = false } = args;
|
|
325
|
-
|
|
326
|
-
// Validate batch size
|
|
327
|
-
if (ids.length > BULK_OPERATION_BATCH_SIZE) {
|
|
328
|
-
throw new Error(
|
|
329
|
-
`Batch size exceeds limit. Maximum ${BULK_OPERATION_BATCH_SIZE} entries per operation, got ${ids.length}.`,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (ids.length === 0) {
|
|
334
|
-
return { total: 0, succeeded: 0, failed: 0, results: [] };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const results: BulkOperationItemResult[] = [];
|
|
338
|
-
const now = Date.now();
|
|
339
|
-
|
|
340
|
-
for (const id of ids) {
|
|
341
|
-
try {
|
|
342
|
-
const entry = await ctx.db.get(id);
|
|
343
|
-
|
|
344
|
-
if (!entry) {
|
|
345
|
-
results.push({ id, success: false, error: "Entry not found" });
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// For soft delete, skip already deleted entries
|
|
350
|
-
if (!hardDelete && isDeleted(entry)) {
|
|
351
|
-
// Already deleted - treat as success (idempotent)
|
|
352
|
-
results.push({ id, success: true });
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (hardDelete) {
|
|
357
|
-
// Hard delete: remove all versions first
|
|
358
|
-
const versions = await ctx.db
|
|
359
|
-
.query("contentVersions")
|
|
360
|
-
.withIndex("by_entry", (q) => q.eq("entryId", id))
|
|
361
|
-
.collect();
|
|
362
|
-
|
|
363
|
-
for (const version of versions) {
|
|
364
|
-
await ctx.db.delete(version._id);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Delete the entry itself
|
|
368
|
-
await ctx.db.delete(id);
|
|
369
|
-
} else {
|
|
370
|
-
// Soft delete: set deletedAt timestamp
|
|
371
|
-
await ctx.db.patch(id, {
|
|
372
|
-
deletedAt: now,
|
|
373
|
-
updatedBy: deletedBy,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
results.push({ id, success: true });
|
|
378
|
-
} catch (error) {
|
|
379
|
-
results.push({
|
|
380
|
-
id,
|
|
381
|
-
success: false,
|
|
382
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const succeeded = results.filter((r) => r.success).length;
|
|
388
|
-
return {
|
|
389
|
-
total: ids.length,
|
|
390
|
-
succeeded,
|
|
391
|
-
failed: ids.length - succeeded,
|
|
392
|
-
results,
|
|
393
|
-
};
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// =============================================================================
|
|
398
|
-
// Bulk Update Mutation
|
|
399
|
-
// =============================================================================
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Mutation to update multiple content entries with the same changes.
|
|
403
|
-
*
|
|
404
|
-
* Applies the same data updates and/or status change to all specified entries.
|
|
405
|
-
* Each entry is validated against its content type schema before updating.
|
|
406
|
-
*
|
|
407
|
-
* Data is merged with existing data for each entry (partial updates).
|
|
408
|
-
* Status can be changed independently of data updates.
|
|
409
|
-
*
|
|
410
|
-
* @param ids - Array of content entry IDs to update (max BULK_OPERATION_BATCH_SIZE)
|
|
411
|
-
* @param data - Data to merge into each entry
|
|
412
|
-
* @param status - New status to apply to all entries
|
|
413
|
-
* @param updatedBy - User ID for audit trail
|
|
414
|
-
*
|
|
415
|
-
* @returns BulkOperationResult with success/failure details for each entry
|
|
416
|
-
*
|
|
417
|
-
* @example
|
|
418
|
-
* ```typescript
|
|
419
|
-
* // Update data for multiple entries
|
|
420
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkUpdate, {
|
|
421
|
-
* ids: [entry1._id, entry2._id, entry3._id],
|
|
422
|
-
* data: { featured: true, category: "news" },
|
|
423
|
-
* updatedBy: currentUserId,
|
|
424
|
-
* });
|
|
425
|
-
*
|
|
426
|
-
* // Change status for multiple entries
|
|
427
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkUpdate, {
|
|
428
|
-
* ids: [entry1._id, entry2._id],
|
|
429
|
-
* status: "archived",
|
|
430
|
-
* updatedBy: currentUserId,
|
|
431
|
-
* });
|
|
432
|
-
* ```
|
|
433
|
-
*/
|
|
434
|
-
export const bulkUpdate = mutation({
|
|
435
|
-
args: bulkUpdateArgs.fields,
|
|
436
|
-
returns: bulkOperationResult,
|
|
437
|
-
handler: async (ctx, args): Promise<BulkOperationResult> => {
|
|
438
|
-
const { ids, data, status, updatedBy } = args;
|
|
439
|
-
|
|
440
|
-
// Validate batch size
|
|
441
|
-
if (ids.length > BULK_OPERATION_BATCH_SIZE) {
|
|
442
|
-
throw new Error(
|
|
443
|
-
`Batch size exceeds limit. Maximum ${BULK_OPERATION_BATCH_SIZE} entries per operation, got ${ids.length}.`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (ids.length === 0) {
|
|
448
|
-
return { total: 0, succeeded: 0, failed: 0, results: [] };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Check that at least one update field is provided
|
|
452
|
-
if (data === undefined && status === undefined) {
|
|
453
|
-
throw new Error(
|
|
454
|
-
"At least one of 'data' or 'status' must be provided for bulk update",
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const results: BulkOperationItemResult[] = [];
|
|
459
|
-
|
|
460
|
-
// Cache content types to avoid repeated lookups
|
|
461
|
-
const contentTypeCache = new Map<
|
|
462
|
-
string,
|
|
463
|
-
{
|
|
464
|
-
name: string;
|
|
465
|
-
displayName: string;
|
|
466
|
-
description?: string;
|
|
467
|
-
fields: FieldDefinition[];
|
|
468
|
-
titleField?: string;
|
|
469
|
-
slugField?: string;
|
|
470
|
-
singleton?: boolean;
|
|
471
|
-
}
|
|
472
|
-
>();
|
|
473
|
-
|
|
474
|
-
for (const id of ids) {
|
|
475
|
-
try {
|
|
476
|
-
const entry = await ctx.db.get(id);
|
|
477
|
-
|
|
478
|
-
if (!entry) {
|
|
479
|
-
results.push({ id, success: false, error: "Entry not found" });
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (isDeleted(entry)) {
|
|
484
|
-
results.push({ id, success: false, error: "Entry has been deleted" });
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Build updates object
|
|
489
|
-
const updates: Record<string, unknown> = {
|
|
490
|
-
updatedBy,
|
|
491
|
-
version: entry.version + 1,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
// Handle data update with validation
|
|
495
|
-
if (data !== undefined) {
|
|
496
|
-
// Get content type (from cache or database)
|
|
497
|
-
const contentTypeId = entry.contentTypeId.toString();
|
|
498
|
-
let contentType = contentTypeCache.get(contentTypeId);
|
|
499
|
-
|
|
500
|
-
if (!contentType) {
|
|
501
|
-
const dbContentType = await ctx.db.get(entry.contentTypeId);
|
|
502
|
-
if (!dbContentType) {
|
|
503
|
-
results.push({
|
|
504
|
-
id,
|
|
505
|
-
success: false,
|
|
506
|
-
error: "Content type not found",
|
|
507
|
-
});
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (isDeleted(dbContentType)) {
|
|
511
|
-
results.push({
|
|
512
|
-
id,
|
|
513
|
-
success: false,
|
|
514
|
-
error: "Content type has been deleted",
|
|
515
|
-
});
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
contentType = {
|
|
520
|
-
name: dbContentType.name,
|
|
521
|
-
displayName: dbContentType.displayName,
|
|
522
|
-
description: dbContentType.description,
|
|
523
|
-
fields: dbContentType.fields as FieldDefinition[],
|
|
524
|
-
titleField: dbContentType.titleField,
|
|
525
|
-
slugField: dbContentType.slugField,
|
|
526
|
-
singleton: dbContentType.singleton,
|
|
527
|
-
};
|
|
528
|
-
contentTypeCache.set(contentTypeId, contentType);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Merge data with existing
|
|
532
|
-
const mergedData = {
|
|
533
|
-
...(entry.data as Record<string, unknown>),
|
|
534
|
-
...(data as Record<string, unknown>),
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
// Validate merged data against schema
|
|
538
|
-
const schema: ContentTypeSchema = {
|
|
539
|
-
name: contentType.name,
|
|
540
|
-
displayName: contentType.displayName,
|
|
541
|
-
description: contentType.description,
|
|
542
|
-
fields: contentType.fields,
|
|
543
|
-
titleField: contentType.titleField,
|
|
544
|
-
slugField: contentType.slugField,
|
|
545
|
-
singleton: contentType.singleton,
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
const validationResult = validateContentData(mergedData, schema);
|
|
549
|
-
if (!validationResult.valid) {
|
|
550
|
-
const errorMessages = validationResult.errors
|
|
551
|
-
.map((e) => `${e.field}: ${e.message}`)
|
|
552
|
-
.join("; ");
|
|
553
|
-
results.push({
|
|
554
|
-
id,
|
|
555
|
-
success: false,
|
|
556
|
-
error: `Validation failed: ${errorMessages}`,
|
|
557
|
-
});
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
updates.data = mergedData;
|
|
562
|
-
|
|
563
|
-
// Regenerate searchText from searchable fields
|
|
564
|
-
let searchText = "";
|
|
565
|
-
for (const field of contentType.fields) {
|
|
566
|
-
if (field.searchable && mergedData[field.name]) {
|
|
567
|
-
const value = mergedData[field.name];
|
|
568
|
-
if (typeof value === "string") {
|
|
569
|
-
searchText += ` ${value}`;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
updates.searchText = searchText.trim() || undefined;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Handle status update
|
|
577
|
-
if (status !== undefined) {
|
|
578
|
-
updates.status = status;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
await ctx.db.patch(id, updates);
|
|
582
|
-
results.push({ id, success: true });
|
|
583
|
-
} catch (error) {
|
|
584
|
-
results.push({
|
|
585
|
-
id,
|
|
586
|
-
success: false,
|
|
587
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const succeeded = results.filter((r) => r.success).length;
|
|
593
|
-
return {
|
|
594
|
-
total: ids.length,
|
|
595
|
-
succeeded,
|
|
596
|
-
failed: ids.length - succeeded,
|
|
597
|
-
results,
|
|
598
|
-
};
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// =============================================================================
|
|
603
|
-
// Bulk Restore Mutation
|
|
604
|
-
// =============================================================================
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Mutation to restore multiple soft-deleted content entries.
|
|
608
|
-
*
|
|
609
|
-
* Removes the deletedAt marker from entries, making them active again.
|
|
610
|
-
* Only works for soft-deleted entries. Non-deleted entries are skipped
|
|
611
|
-
* with a success status (idempotent behavior).
|
|
612
|
-
*
|
|
613
|
-
* @param ids - Array of content entry IDs to restore (max BULK_OPERATION_BATCH_SIZE)
|
|
614
|
-
* @param restoredBy - User ID for audit trail
|
|
615
|
-
*
|
|
616
|
-
* @returns BulkOperationResult with success/failure details for each entry
|
|
617
|
-
*
|
|
618
|
-
* @example
|
|
619
|
-
* ```typescript
|
|
620
|
-
* const result = await ctx.runMutation(api.bulkOperations.bulkRestore, {
|
|
621
|
-
* ids: [deletedEntry1._id, deletedEntry2._id],
|
|
622
|
-
* restoredBy: currentUserId,
|
|
623
|
-
* });
|
|
624
|
-
* ```
|
|
625
|
-
*/
|
|
626
|
-
export const bulkRestore = mutation({
|
|
627
|
-
args: {
|
|
628
|
-
ids: v.array(v.id("contentEntries")),
|
|
629
|
-
restoredBy: v.optional(v.string()),
|
|
630
|
-
},
|
|
631
|
-
returns: bulkOperationResult,
|
|
632
|
-
handler: async (ctx, args): Promise<BulkOperationResult> => {
|
|
633
|
-
const { ids, restoredBy } = args;
|
|
634
|
-
|
|
635
|
-
// Validate batch size
|
|
636
|
-
if (ids.length > BULK_OPERATION_BATCH_SIZE) {
|
|
637
|
-
throw new Error(
|
|
638
|
-
`Batch size exceeds limit. Maximum ${BULK_OPERATION_BATCH_SIZE} entries per operation, got ${ids.length}.`,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (ids.length === 0) {
|
|
643
|
-
return { total: 0, succeeded: 0, failed: 0, results: [] };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const results: BulkOperationItemResult[] = [];
|
|
647
|
-
|
|
648
|
-
for (const id of ids) {
|
|
649
|
-
try {
|
|
650
|
-
const entry = await ctx.db.get(id);
|
|
651
|
-
|
|
652
|
-
if (!entry) {
|
|
653
|
-
results.push({ id, success: false, error: "Entry not found" });
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (!isDeleted(entry)) {
|
|
658
|
-
// Not deleted - treat as success (idempotent)
|
|
659
|
-
results.push({ id, success: true });
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Restore the entry
|
|
664
|
-
await ctx.db.patch(id, {
|
|
665
|
-
deletedAt: undefined,
|
|
666
|
-
updatedBy: restoredBy,
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
results.push({ id, success: true });
|
|
670
|
-
} catch (error) {
|
|
671
|
-
results.push({
|
|
672
|
-
id,
|
|
673
|
-
success: false,
|
|
674
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const succeeded = results.filter((r) => r.success).length;
|
|
680
|
-
return {
|
|
681
|
-
total: ids.length,
|
|
682
|
-
succeeded,
|
|
683
|
-
failed: ids.length - succeeded,
|
|
684
|
-
results,
|
|
685
|
-
};
|
|
686
|
-
},
|
|
687
|
-
});
|