convex-cms 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/admin-dist/nitro.json +15 -0
  2. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  3. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  4. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  5. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  6. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  7. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  8. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  9. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  10. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  11. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  12. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  13. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  14. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  15. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  16. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  17. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  18. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  19. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  20. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  21. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  22. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  23. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  24. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  25. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  26. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  27. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  28. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  29. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  30. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  31. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  32. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  33. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  34. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  35. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  36. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  37. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  38. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  39. package/admin-dist/public/favicon.ico +0 -0
  40. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  41. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  42. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  45. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  46. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  83. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  84. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  88. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  89. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  90. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  91. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  92. package/admin-dist/server/_libs/clsx.mjs +16 -0
  93. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  94. package/admin-dist/server/_libs/convex.mjs +4841 -0
  95. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  96. package/admin-dist/server/_libs/croner.mjs +1 -0
  97. package/admin-dist/server/_libs/crossws.mjs +1 -0
  98. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  99. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  100. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  101. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  102. package/admin-dist/server/_libs/h3.mjs +401 -0
  103. package/admin-dist/server/_libs/hookable.mjs +1 -0
  104. package/admin-dist/server/_libs/isbot.mjs +20 -0
  105. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  106. package/admin-dist/server/_libs/ohash.mjs +1 -0
  107. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  108. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  109. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  110. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  111. package/admin-dist/server/_libs/rou3.mjs +8 -0
  112. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  113. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  114. package/admin-dist/server/_libs/srvx.mjs +719 -0
  115. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  116. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  117. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  118. package/admin-dist/server/_libs/tslib.mjs +39 -0
  119. package/admin-dist/server/_libs/ufo.mjs +54 -0
  120. package/admin-dist/server/_libs/unctx.mjs +1 -0
  121. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  122. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  123. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  124. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  125. package/admin-dist/server/_libs/zod.mjs +4223 -0
  126. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  127. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  128. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  129. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  130. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  131. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  132. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  133. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  134. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  135. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  136. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  137. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  138. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  139. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  140. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  141. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  142. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  143. package/admin-dist/server/_ssr/index.mjs +1275 -0
  144. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  145. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  146. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  147. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  148. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  149. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  150. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  151. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  152. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  153. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  154. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  155. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  156. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  157. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  158. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  159. package/admin-dist/server/favicon.ico +0 -0
  160. package/admin-dist/server/index.mjs +627 -0
  161. package/dist/cli/index.js +0 -0
  162. package/dist/client/admin-config.d.ts +0 -1
  163. package/dist/client/admin-config.d.ts.map +1 -1
  164. package/dist/client/admin-config.js +0 -1
  165. package/dist/client/admin-config.js.map +1 -1
  166. package/dist/client/adminApi.d.ts.map +1 -1
  167. package/dist/client/agentTools.d.ts +1237 -135
  168. package/dist/client/agentTools.d.ts.map +1 -1
  169. package/dist/client/agentTools.js +33 -9
  170. package/dist/client/agentTools.js.map +1 -1
  171. package/dist/client/index.d.ts +1 -1
  172. package/dist/client/index.d.ts.map +1 -1
  173. package/dist/client/index.js.map +1 -1
  174. package/dist/component/_generated/component.d.ts +9 -0
  175. package/dist/component/_generated/component.d.ts.map +1 -1
  176. package/dist/component/mediaAssets.d.ts +35 -0
  177. package/dist/component/mediaAssets.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.js +81 -0
  179. package/dist/component/mediaAssets.js.map +1 -1
  180. package/dist/test.d.ts.map +1 -1
  181. package/dist/test.js +2 -1
  182. package/dist/test.js.map +1 -1
  183. package/package.json +9 -5
  184. package/dist/component/auditLog.d.ts +0 -410
  185. package/dist/component/auditLog.d.ts.map +0 -1
  186. package/dist/component/auditLog.js +0 -607
  187. package/dist/component/auditLog.js.map +0 -1
  188. package/dist/component/types.d.ts +0 -4
  189. package/dist/component/types.d.ts.map +0 -1
  190. package/dist/component/types.js +0 -2
  191. package/dist/component/types.js.map +0 -1
  192. package/src/cli/commands/admin.ts +0 -104
  193. package/src/cli/index.ts +0 -21
  194. package/src/cli/utils/detectConvexUrl.ts +0 -54
  195. package/src/cli/utils/openBrowser.ts +0 -16
  196. package/src/client/admin-config.ts +0 -138
  197. package/src/client/adminApi.ts +0 -942
  198. package/src/client/agentTools.ts +0 -1311
  199. package/src/client/argTypes.ts +0 -316
  200. package/src/client/field-types.ts +0 -187
  201. package/src/client/index.ts +0 -1301
  202. package/src/client/queryBuilder.ts +0 -1100
  203. package/src/client/schema/codegen.ts +0 -500
  204. package/src/client/schema/defineContentType.ts +0 -501
  205. package/src/client/schema/index.ts +0 -169
  206. package/src/client/schema/schemaDrift.ts +0 -574
  207. package/src/client/schema/typedClient.ts +0 -688
  208. package/src/client/schema/types.ts +0 -666
  209. package/src/client/types.ts +0 -723
  210. package/src/client/workflows.ts +0 -141
  211. package/src/client/wrapper.ts +0 -4304
  212. package/src/component/_generated/api.ts +0 -140
  213. package/src/component/_generated/component.ts +0 -5029
  214. package/src/component/_generated/dataModel.ts +0 -60
  215. package/src/component/_generated/server.ts +0 -156
  216. package/src/component/authorization.ts +0 -647
  217. package/src/component/authorizationHooks.ts +0 -668
  218. package/src/component/bulkOperations.ts +0 -687
  219. package/src/component/contentEntries.ts +0 -1976
  220. package/src/component/contentEntryMutations.ts +0 -1223
  221. package/src/component/contentEntryValidation.ts +0 -707
  222. package/src/component/contentLock.ts +0 -550
  223. package/src/component/contentTypeMigration.ts +0 -1064
  224. package/src/component/contentTypeMutations.ts +0 -969
  225. package/src/component/contentTypes.ts +0 -346
  226. package/src/component/convex.config.ts +0 -44
  227. package/src/component/documentTypes.ts +0 -240
  228. package/src/component/eventEmitter.ts +0 -485
  229. package/src/component/exportImport.ts +0 -1169
  230. package/src/component/index.ts +0 -491
  231. package/src/component/lib/deepReferenceResolver.ts +0 -999
  232. package/src/component/lib/errors.ts +0 -816
  233. package/src/component/lib/index.ts +0 -145
  234. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  235. package/src/component/lib/metadataExtractor.ts +0 -792
  236. package/src/component/lib/mutationAuth.ts +0 -199
  237. package/src/component/lib/queries.ts +0 -79
  238. package/src/component/lib/ragContentChunker.ts +0 -1371
  239. package/src/component/lib/referenceResolver.ts +0 -430
  240. package/src/component/lib/slugGenerator.ts +0 -262
  241. package/src/component/lib/slugUniqueness.ts +0 -333
  242. package/src/component/lib/softDelete.ts +0 -44
  243. package/src/component/localeFallbackChain.ts +0 -673
  244. package/src/component/localeFields.ts +0 -896
  245. package/src/component/mediaAssetMutations.ts +0 -725
  246. package/src/component/mediaAssets.ts +0 -932
  247. package/src/component/mediaFolderMutations.ts +0 -1046
  248. package/src/component/mediaUploadMutations.ts +0 -224
  249. package/src/component/mediaVariantMutations.ts +0 -900
  250. package/src/component/mediaVariants.ts +0 -793
  251. package/src/component/ragContentIndexer.ts +0 -1067
  252. package/src/component/rateLimitHooks.ts +0 -572
  253. package/src/component/roles.ts +0 -1360
  254. package/src/component/scheduledPublish.ts +0 -358
  255. package/src/component/schema.ts +0 -617
  256. package/src/component/taxonomies.ts +0 -949
  257. package/src/component/taxonomyMutations.ts +0 -1210
  258. package/src/component/trash.ts +0 -724
  259. package/src/component/userContext.ts +0 -898
  260. package/src/component/validation.ts +0 -1388
  261. package/src/component/validators.ts +0 -949
  262. package/src/component/versionMutations.ts +0 -392
  263. package/src/component/webhookTrigger.ts +0 -1922
  264. package/src/react/index.ts +0 -898
  265. package/src/test.ts +0 -1580
@@ -1,724 +0,0 @@
1
- /**
2
- * Trash Management Functions
3
- *
4
- * Provides functionality for managing soft-deleted content entries in a "trash" system:
5
- * - List deleted entries with pagination and filtering
6
- * - Configure retention period for automatic cleanup
7
- * - Manually empty trash (permanent deletion)
8
- * - Automatic scheduled cleanup of old deleted items
9
- *
10
- * Soft Delete Workflow:
11
- * 1. User deletes an entry -> `deletedAt` timestamp is set (soft delete)
12
- * 2. Entry is hidden from normal queries but visible in trash
13
- * 3. User can restore the entry using `restoreEntry` mutation
14
- * 4. After retention period, entry is permanently deleted (hard delete)
15
- *
16
- * Configuration:
17
- * - `retentionDays`: How long items stay in trash (default: 30 days)
18
- * - `autoCleanupEnabled`: Whether to run automatic cleanup (default: true)
19
- * - Setting `retentionDays` to 0 disables automatic cleanup
20
- */
21
-
22
- import { v } from "convex/values";
23
- import { isDeleted } from "./lib/softDelete.js";
24
- import { stream } from "convex-helpers/server/stream";
25
- import { query, mutation, internalMutation } from "./_generated/server.js";
26
- import { internal } from "./_generated/api.js";
27
- import {
28
- // contentEntryDoc,
29
- trashConfigDoc,
30
- updateTrashConfigArgs,
31
- listTrashArgs,
32
- emptyTrashArgs,
33
- emptyTrashResult,
34
- trashItemDoc,
35
- DEFAULT_TRASH_RETENTION_DAYS,
36
- } from "./validators.js";
37
- import schema from "./schema.js";
38
-
39
- // =============================================================================
40
- // Constants
41
- // =============================================================================
42
-
43
- /** Default pagination size for trash listing */
44
- const DEFAULT_NUM_ITEMS = 50;
45
-
46
- /** Maximum items per page */
47
- const MAX_NUM_ITEMS = 250;
48
-
49
- /** Milliseconds in a day */
50
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
51
-
52
- // =============================================================================
53
- // Trash Configuration
54
- // =============================================================================
55
-
56
- /**
57
- * Query to get the current trash configuration.
58
- *
59
- * If no configuration exists, returns default values.
60
- * This is useful for displaying settings in the admin UI.
61
- *
62
- * @returns Current trash configuration
63
- *
64
- * @example
65
- * ```typescript
66
- * const config = await ctx.runQuery(api.trash.getTrashConfig, {});
67
- * console.log(`Retention: ${config.retentionDays} days`);
68
- * console.log(`Auto-cleanup: ${config.autoCleanupEnabled}`);
69
- * ```
70
- */
71
- export const getTrashConfig = query({
72
- args: {},
73
- returns: v.object({
74
- retentionDays: v.number(),
75
- autoCleanupEnabled: v.boolean(),
76
- lastCleanupAt: v.optional(v.number()),
77
- lastCleanupCount: v.optional(v.number()),
78
- }),
79
- handler: async (ctx) => {
80
- // Get the singleton config record
81
- const config = await ctx.db.query("trashConfig").first();
82
-
83
- if (config) {
84
- return {
85
- retentionDays: config.retentionDays,
86
- autoCleanupEnabled: config.autoCleanupEnabled,
87
- lastCleanupAt: config.lastCleanupAt,
88
- lastCleanupCount: config.lastCleanupCount,
89
- };
90
- }
91
-
92
- // Return defaults if no config exists
93
- return {
94
- retentionDays: DEFAULT_TRASH_RETENTION_DAYS,
95
- autoCleanupEnabled: true,
96
- lastCleanupAt: undefined,
97
- lastCleanupCount: undefined,
98
- };
99
- },
100
- });
101
-
102
- /**
103
- * Mutation to update trash configuration settings.
104
- *
105
- * Creates the config record if it doesn't exist.
106
- * Use this to customize retention period or disable auto-cleanup.
107
- *
108
- * @param retentionDays - Days to keep items in trash (0 to disable auto-cleanup)
109
- * @param autoCleanupEnabled - Whether to enable automatic cleanup
110
- * @param updatedBy - User ID for audit trail
111
- *
112
- * @returns Updated configuration
113
- *
114
- * @example
115
- * ```typescript
116
- * // Set 7-day retention period
117
- * await ctx.runMutation(api.trash.updateTrashConfig, {
118
- * retentionDays: 7,
119
- * updatedBy: currentUserId,
120
- * });
121
- *
122
- * // Disable auto-cleanup (keep items forever until manually deleted)
123
- * await ctx.runMutation(api.trash.updateTrashConfig, {
124
- * autoCleanupEnabled: false,
125
- * updatedBy: currentUserId,
126
- * });
127
- * ```
128
- */
129
- export const updateTrashConfig = mutation({
130
- args: updateTrashConfigArgs.fields,
131
- returns: trashConfigDoc,
132
- handler: async (ctx, args) => {
133
- const { retentionDays, autoCleanupEnabled, updatedBy } = args;
134
-
135
- // Validate retention days
136
- if (retentionDays !== undefined) {
137
- if (retentionDays < 0 || retentionDays > 365) {
138
- throw new Error("Retention days must be between 0 and 365");
139
- }
140
- }
141
-
142
- // Get existing config
143
- const existingConfig = await ctx.db.query("trashConfig").first();
144
-
145
- if (existingConfig) {
146
- // Update existing config
147
- const updates: Record<string, unknown> = { updatedBy };
148
- if (retentionDays !== undefined) updates.retentionDays = retentionDays;
149
- if (autoCleanupEnabled !== undefined)
150
- updates.autoCleanupEnabled = autoCleanupEnabled;
151
-
152
- await ctx.db.patch(existingConfig._id, updates);
153
- const updated = await ctx.db.get(existingConfig._id);
154
- return updated!;
155
- } else {
156
- // Create new config with defaults for unspecified fields
157
- const configId = await ctx.db.insert("trashConfig", {
158
- retentionDays: retentionDays ?? DEFAULT_TRASH_RETENTION_DAYS,
159
- autoCleanupEnabled: autoCleanupEnabled ?? true,
160
- updatedBy,
161
- });
162
- const newConfig = await ctx.db.get(configId);
163
- return newConfig!;
164
- }
165
- },
166
- });
167
-
168
- // =============================================================================
169
- // List Trash Query
170
- // =============================================================================
171
-
172
- /**
173
- * Paginated response for trash listing.
174
- */
175
- const paginatedTrashResponse = v.object({
176
- /** Array of deleted content entries with metadata */
177
- page: v.array(trashItemDoc),
178
- /** Cursor for fetching the next page */
179
- continueCursor: v.union(v.string(), v.null()),
180
- /** Whether this is the last page */
181
- isDone: v.boolean(),
182
- /** Total count of items in trash (approximate) */
183
- totalCount: v.optional(v.number()),
184
- });
185
-
186
- /**
187
- * Query to list soft-deleted content entries (trash).
188
- *
189
- * Returns entries that have been soft-deleted, sorted by deletion time
190
- * (most recently deleted first). Each entry includes metadata about
191
- * when it was deleted and when it will expire.
192
- *
193
- * @param contentTypeId - Filter by content type ID
194
- * @param contentTypeName - Filter by content type name
195
- * @param search - Search within deleted items
196
- * @param paginationOpts - Standard pagination options
197
- *
198
- * @returns Paginated list of deleted entries
199
- *
200
- * @example
201
- * ```typescript
202
- * // List all trash items
203
- * const { page, continueCursor, isDone } = await ctx.runQuery(
204
- * api.trash.listTrash,
205
- * { paginationOpts: { numItems: 20 } }
206
- * );
207
- *
208
- * // Filter by content type
209
- * const deletedPosts = await ctx.runQuery(api.trash.listTrash, {
210
- * contentTypeName: "blog_post",
211
- * paginationOpts: { numItems: 20 },
212
- * });
213
- *
214
- * // Each item includes deletion metadata
215
- * for (const item of page) {
216
- * console.log(`${item.slug} - deleted ${item.deletedDaysAgo} days ago`);
217
- * if (item.expiresAt) {
218
- * console.log(` Expires: ${new Date(item.expiresAt).toISOString()}`);
219
- * }
220
- * }
221
- * ```
222
- */
223
- export const listTrash = query({
224
- args: listTrashArgs.fields,
225
- returns: paginatedTrashResponse,
226
- handler: async (ctx, args) => {
227
- const { contentTypeId, contentTypeName, search, paginationOpts } = args;
228
-
229
- // Clamp pagination
230
- const numItems = Math.min(
231
- Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
232
- MAX_NUM_ITEMS,
233
- );
234
-
235
- const clampedPaginationOpts = {
236
- ...paginationOpts,
237
- numItems,
238
- };
239
-
240
- // Resolve content type ID from name if provided
241
- let resolvedContentTypeId = contentTypeId;
242
- if (!resolvedContentTypeId && contentTypeName) {
243
- const contentType = await ctx.db
244
- .query("contentTypes")
245
- .withIndex("by_name", (q) => q.eq("name", contentTypeName))
246
- .first();
247
- if (contentType) {
248
- resolvedContentTypeId = contentType._id;
249
- }
250
- }
251
-
252
- // Get trash config for retention period
253
- const config = await ctx.db.query("trashConfig").first();
254
- const retentionDays = config?.retentionDays ?? DEFAULT_TRASH_RETENTION_DAYS;
255
- const now = Date.now();
256
-
257
- // Create content type cache for display names
258
- const contentTypeCache = new Map<string, string>();
259
-
260
- // Build the query using the by_deleted index
261
- const streamDb = stream(ctx.db, schema);
262
-
263
- // Query deleted entries (where deletedAt is defined)
264
- // We use filterWith to handle the complex filtering
265
- const baseQuery = streamDb.query("contentEntries");
266
-
267
- const filteredQuery = baseQuery.order("desc").filterWith(async (entry) => {
268
- // Must be deleted
269
- if (!isDeleted(entry)) {
270
- return false;
271
- }
272
-
273
- // Filter by content type if specified
274
- if (
275
- resolvedContentTypeId &&
276
- entry.contentTypeId !== resolvedContentTypeId
277
- ) {
278
- return false;
279
- }
280
-
281
- // Simple text search if provided
282
- if (search && search.trim().length > 0) {
283
- const searchLower = search.toLowerCase();
284
- const slugMatch = entry.slug?.toLowerCase().includes(searchLower);
285
- const searchTextMatch = entry.searchText
286
- ?.toLowerCase()
287
- .includes(searchLower);
288
- if (!slugMatch && !searchTextMatch) {
289
- return false;
290
- }
291
- }
292
-
293
- return true;
294
- });
295
-
296
- // Execute pagination
297
- const result = await filteredQuery.paginate({
298
- ...clampedPaginationOpts,
299
- maximumRowsRead: numItems * 10,
300
- });
301
-
302
- // Enrich results with deletion metadata
303
- const enrichedPage = await Promise.all(
304
- result.page.map(async (entry) => {
305
- const deletedAt = entry.deletedAt!;
306
- const deletedDaysAgo = Math.floor((now - deletedAt) / MS_PER_DAY);
307
-
308
- // Calculate expiration time based on retention
309
- let expiresAt: number | undefined;
310
- if (retentionDays > 0) {
311
- expiresAt = deletedAt + retentionDays * MS_PER_DAY;
312
- }
313
-
314
- // Get content type display name
315
- let contentTypeName = contentTypeCache.get(
316
- entry.contentTypeId.toString(),
317
- );
318
- if (!contentTypeName) {
319
- const ct = await ctx.db.get(entry.contentTypeId);
320
- contentTypeName = ct?.displayName ?? ct?.name;
321
- if (contentTypeName) {
322
- contentTypeCache.set(
323
- entry.contentTypeId.toString(),
324
- contentTypeName,
325
- );
326
- }
327
- }
328
-
329
- return {
330
- ...entry,
331
- deletedDaysAgo,
332
- expiresAt,
333
- contentTypeName,
334
- };
335
- }),
336
- );
337
-
338
- return {
339
- page: enrichedPage,
340
- continueCursor: result.continueCursor,
341
- isDone: result.isDone,
342
- };
343
- },
344
- });
345
-
346
- /**
347
- * Query to get trash statistics.
348
- *
349
- * Returns counts and metadata about items in trash.
350
- * Useful for displaying trash status in the admin UI.
351
- *
352
- * @returns Trash statistics
353
- */
354
- export const getTrashStats = query({
355
- args: {},
356
- returns: v.object({
357
- /** Total number of items in trash */
358
- totalCount: v.number(),
359
- /** Number of items that have expired (past retention period) */
360
- expiredCount: v.number(),
361
- /** Oldest item deletion date */
362
- oldestDeletedAt: v.optional(v.number()),
363
- /** Most recent item deletion date */
364
- newestDeletedAt: v.optional(v.number()),
365
- /** Current retention period in days */
366
- retentionDays: v.number(),
367
- }),
368
- handler: async (ctx) => {
369
- // Get trash config
370
- const config = await ctx.db.query("trashConfig").first();
371
- const retentionDays = config?.retentionDays ?? DEFAULT_TRASH_RETENTION_DAYS;
372
- const now = Date.now();
373
- const expirationThreshold = now - retentionDays * MS_PER_DAY;
374
-
375
- // Query all deleted entries
376
- const deletedEntries = await ctx.db
377
- .query("contentEntries")
378
- .filter((q) => q.neq(q.field("deletedAt"), undefined))
379
- .collect();
380
-
381
- let totalCount = 0;
382
- let expiredCount = 0;
383
- let oldestDeletedAt: number | undefined;
384
- let newestDeletedAt: number | undefined;
385
-
386
- for (const entry of deletedEntries) {
387
- if (!isDeleted(entry)) continue;
388
- const deletedAt = entry.deletedAt!;
389
-
390
- totalCount++;
391
-
392
- if (retentionDays > 0 && deletedAt < expirationThreshold) {
393
- expiredCount++;
394
- }
395
-
396
- if (oldestDeletedAt === undefined || deletedAt < oldestDeletedAt) {
397
- oldestDeletedAt = deletedAt;
398
- }
399
- if (newestDeletedAt === undefined || deletedAt > newestDeletedAt) {
400
- newestDeletedAt = deletedAt;
401
- }
402
- }
403
-
404
- return {
405
- totalCount,
406
- expiredCount,
407
- oldestDeletedAt,
408
- newestDeletedAt,
409
- retentionDays,
410
- };
411
- },
412
- });
413
-
414
- // =============================================================================
415
- // Empty Trash Mutation
416
- // =============================================================================
417
-
418
- /**
419
- * Mutation to permanently delete items from trash.
420
- *
421
- * This performs a hard delete, removing entries and all their version history.
422
- * This action cannot be undone.
423
- *
424
- * Options:
425
- * - Delete all trash items
426
- * - Delete only items older than a specified number of days
427
- * - Delete only items of a specific content type
428
- *
429
- * @param olderThanDays - Only delete items deleted more than this many days ago
430
- * @param contentTypeId - Only delete items of this content type
431
- * @param deletedBy - User performing the operation (for logging)
432
- *
433
- * @returns Count of deleted items and any errors
434
- *
435
- * @example
436
- * ```typescript
437
- * // Empty all trash
438
- * const result = await ctx.runMutation(api.trash.emptyTrash, {
439
- * deletedBy: currentUserId,
440
- * });
441
- * console.log(`Permanently deleted ${result.deletedCount} items`);
442
- *
443
- * // Delete only items older than 7 days
444
- * await ctx.runMutation(api.trash.emptyTrash, {
445
- * olderThanDays: 7,
446
- * deletedBy: currentUserId,
447
- * });
448
- *
449
- * // Delete only deleted blog posts
450
- * await ctx.runMutation(api.trash.emptyTrash, {
451
- * contentTypeId: blogTypeId,
452
- * deletedBy: currentUserId,
453
- * });
454
- * ```
455
- */
456
- export const emptyTrash = mutation({
457
- args: emptyTrashArgs.fields,
458
- returns: emptyTrashResult,
459
- handler: async (ctx, args) => {
460
- const { olderThanDays, contentTypeId } = args;
461
-
462
- const now = Date.now();
463
- let cutoffTime: number | undefined;
464
-
465
- if (olderThanDays !== undefined) {
466
- cutoffTime = now - olderThanDays * MS_PER_DAY;
467
- }
468
-
469
- // Query all deleted entries
470
- const deletedEntries = await ctx.db
471
- .query("contentEntries")
472
- .filter((q) => q.neq(q.field("deletedAt"), undefined))
473
- .collect();
474
-
475
- let deletedCount = 0;
476
- let deletedVersionsCount = 0;
477
- const errors: Array<{
478
- id: typeof deletedEntries[0]["_id"];
479
- error: string;
480
- }> = [];
481
-
482
- for (const entry of deletedEntries) {
483
- // Skip if not actually deleted
484
- if (!isDeleted(entry)) continue;
485
- const deletedAt = entry.deletedAt!;
486
-
487
- // Apply filters
488
- if (cutoffTime !== undefined && deletedAt > cutoffTime) {
489
- continue; // Not old enough
490
- }
491
- if (
492
- contentTypeId !== undefined &&
493
- entry.contentTypeId !== contentTypeId
494
- ) {
495
- continue; // Wrong content type
496
- }
497
-
498
- try {
499
- // Delete all versions for this entry
500
- const versions = await ctx.db
501
- .query("contentVersions")
502
- .withIndex("by_entry", (q) => q.eq("entryId", entry._id))
503
- .collect();
504
-
505
- for (const version of versions) {
506
- await ctx.db.delete(version._id);
507
- deletedVersionsCount++;
508
- }
509
-
510
- // Delete the entry itself
511
- await ctx.db.delete(entry._id);
512
- deletedCount++;
513
- } catch (error) {
514
- errors.push({
515
- id: entry._id,
516
- error: error instanceof Error ? error.message : "Unknown error",
517
- });
518
- }
519
- }
520
-
521
- return {
522
- deletedCount,
523
- deletedVersionsCount,
524
- errors,
525
- };
526
- },
527
- });
528
-
529
- // =============================================================================
530
- // Scheduled Cleanup Functions
531
- // =============================================================================
532
-
533
- /**
534
- * Internal mutation for scheduled trash cleanup.
535
- *
536
- * This is called by the scheduler to automatically delete items that have
537
- * exceeded the retention period. It runs periodically (e.g., daily) and
538
- * processes items in batches to respect Convex transaction limits.
539
- *
540
- * The function:
541
- * 1. Checks if auto-cleanup is enabled
542
- * 2. Finds all items past the retention period
543
- * 3. Permanently deletes them and their versions
544
- * 4. Updates the config with cleanup stats
545
- * 5. Reschedules itself for the next run
546
- */
547
- export const executeTrashCleanup = internalMutation({
548
- args: {},
549
- handler: async (ctx) => {
550
- // Get trash configuration
551
- const config = await ctx.db.query("trashConfig").first();
552
-
553
- // If no config exists, create one with defaults
554
- if (!config) {
555
- await ctx.db.insert("trashConfig", {
556
- retentionDays: DEFAULT_TRASH_RETENTION_DAYS,
557
- autoCleanupEnabled: true,
558
- });
559
- return;
560
- }
561
-
562
- // Check if auto-cleanup is enabled
563
- if (!config.autoCleanupEnabled) {
564
- console.log("Trash auto-cleanup is disabled, skipping");
565
- return;
566
- }
567
-
568
- // Check if retention is set (0 = no auto-cleanup)
569
- if (config.retentionDays === 0) {
570
- console.log("Trash retention is 0 days (disabled), skipping cleanup");
571
- return;
572
- }
573
-
574
- const now = Date.now();
575
- const cutoffTime = now - config.retentionDays * MS_PER_DAY;
576
-
577
- // Find expired items
578
- const expiredEntries = await ctx.db
579
- .query("contentEntries")
580
- .filter((q) => q.neq(q.field("deletedAt"), undefined))
581
- .collect();
582
-
583
- let deletedCount = 0;
584
-
585
- for (const entry of expiredEntries) {
586
- // Skip if not actually deleted or not expired
587
- if (!isDeleted(entry)) continue;
588
- if (entry.deletedAt! > cutoffTime) continue;
589
-
590
- try {
591
- // Delete all versions
592
- const versions = await ctx.db
593
- .query("contentVersions")
594
- .withIndex("by_entry", (q) => q.eq("entryId", entry._id))
595
- .collect();
596
-
597
- for (const version of versions) {
598
- await ctx.db.delete(version._id);
599
- }
600
-
601
- // Delete the entry
602
- await ctx.db.delete(entry._id);
603
- deletedCount++;
604
- } catch (error) {
605
- console.error(`Failed to delete expired entry ${entry._id}:`, error);
606
- }
607
- }
608
-
609
- // Update config with cleanup stats
610
- await ctx.db.patch(config._id, {
611
- lastCleanupAt: now,
612
- lastCleanupCount: deletedCount,
613
- });
614
-
615
- console.log(
616
- `Trash cleanup completed: ${deletedCount} items permanently deleted`,
617
- );
618
- },
619
- });
620
-
621
- /**
622
- * Mutation to manually trigger trash cleanup.
623
- *
624
- * Use this to run cleanup on-demand instead of waiting for the scheduled job.
625
- * Useful for testing or when you need immediate cleanup.
626
- *
627
- * @param updatedBy - User triggering the cleanup
628
- *
629
- * @returns Cleanup result statistics
630
- */
631
- export const runTrashCleanup = mutation({
632
- args: {
633
- updatedBy: v.optional(v.string()),
634
- },
635
- returns: v.object({
636
- deletedCount: v.number(),
637
- message: v.string(),
638
- }),
639
- handler: async (ctx, args) => {
640
- // Get trash configuration
641
- const config = await ctx.db.query("trashConfig").first();
642
- const retentionDays = config?.retentionDays ?? DEFAULT_TRASH_RETENTION_DAYS;
643
-
644
- if (retentionDays === 0) {
645
- return {
646
- deletedCount: 0,
647
- message: "Retention is set to 0 days (disabled). No items deleted.",
648
- };
649
- }
650
-
651
- const now = Date.now();
652
- const cutoffTime = now - retentionDays * MS_PER_DAY;
653
-
654
- // Find expired items
655
- const expiredEntries = await ctx.db
656
- .query("contentEntries")
657
- .filter((q) => q.neq(q.field("deletedAt"), undefined))
658
- .collect();
659
-
660
- let deletedCount = 0;
661
-
662
- for (const entry of expiredEntries) {
663
- if (!isDeleted(entry)) continue;
664
- if (entry.deletedAt! > cutoffTime) continue;
665
-
666
- // Delete versions
667
- const versions = await ctx.db
668
- .query("contentVersions")
669
- .withIndex("by_entry", (q) => q.eq("entryId", entry._id))
670
- .collect();
671
-
672
- for (const version of versions) {
673
- await ctx.db.delete(version._id);
674
- }
675
-
676
- // Delete entry
677
- await ctx.db.delete(entry._id);
678
- deletedCount++;
679
- }
680
-
681
- // Update config stats if it exists
682
- if (config) {
683
- await ctx.db.patch(config._id, {
684
- lastCleanupAt: now,
685
- lastCleanupCount: deletedCount,
686
- updatedBy: args.updatedBy,
687
- });
688
- }
689
-
690
- return {
691
- deletedCount,
692
- message: `Successfully deleted ${deletedCount} items older than ${retentionDays} days.`,
693
- };
694
- },
695
- });
696
-
697
- /**
698
- * Mutation to schedule periodic trash cleanup.
699
- *
700
- * Call this once during setup to enable automatic trash cleanup.
701
- * The function schedules itself to run daily.
702
- *
703
- * @param intervalMs - Cleanup interval in milliseconds (default: 24 hours)
704
- */
705
- export const scheduleTrashCleanup = mutation({
706
- args: {
707
- /** Interval between cleanups in milliseconds. Default is 24 hours. */
708
- intervalMs: v.optional(v.number()),
709
- },
710
- handler: async (ctx, args) => {
711
- const intervalMs = args.intervalMs ?? 24 * 60 * 60 * 1000; // Default: 24 hours
712
-
713
- // Schedule the cleanup to run
714
- await ctx.scheduler.runAfter(
715
- intervalMs,
716
- internal.trash.executeTrashCleanup,
717
- {},
718
- );
719
-
720
- console.log(
721
- `Trash cleanup scheduled to run in ${intervalMs / 1000 / 60} minutes`,
722
- );
723
- },
724
- });