convex-cms 0.0.1 → 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 (267) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +99 -0
  3. package/admin-dist/nitro.json +15 -0
  4. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  5. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  6. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  7. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  8. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  9. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  10. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  11. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  12. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  13. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  14. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  15. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  16. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  17. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  18. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  19. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  20. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  21. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  22. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  23. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  24. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  25. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  26. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  27. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  28. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  29. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  30. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  31. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  32. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  33. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  34. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  35. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  36. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  37. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  38. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  39. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  40. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  41. package/admin-dist/public/favicon.ico +0 -0
  42. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  45. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  46. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  83. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  84. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  88. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  89. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  90. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  91. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  92. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  93. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  94. package/admin-dist/server/_libs/clsx.mjs +16 -0
  95. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  96. package/admin-dist/server/_libs/convex.mjs +4841 -0
  97. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  98. package/admin-dist/server/_libs/croner.mjs +1 -0
  99. package/admin-dist/server/_libs/crossws.mjs +1 -0
  100. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  101. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  102. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  103. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  104. package/admin-dist/server/_libs/h3.mjs +401 -0
  105. package/admin-dist/server/_libs/hookable.mjs +1 -0
  106. package/admin-dist/server/_libs/isbot.mjs +20 -0
  107. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  108. package/admin-dist/server/_libs/ohash.mjs +1 -0
  109. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  110. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  111. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  112. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  113. package/admin-dist/server/_libs/rou3.mjs +8 -0
  114. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  115. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  116. package/admin-dist/server/_libs/srvx.mjs +719 -0
  117. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  118. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  119. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  120. package/admin-dist/server/_libs/tslib.mjs +39 -0
  121. package/admin-dist/server/_libs/ufo.mjs +54 -0
  122. package/admin-dist/server/_libs/unctx.mjs +1 -0
  123. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  124. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  125. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  126. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  127. package/admin-dist/server/_libs/zod.mjs +4223 -0
  128. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  129. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  130. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  131. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  132. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  133. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  134. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  135. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  136. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  137. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  138. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  139. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  140. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  141. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  142. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  143. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  144. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  145. package/admin-dist/server/_ssr/index.mjs +1275 -0
  146. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  147. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  148. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  149. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  150. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  151. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  152. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  153. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  154. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  155. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  156. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  157. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  158. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  159. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  160. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  161. package/admin-dist/server/favicon.ico +0 -0
  162. package/admin-dist/server/index.mjs +627 -0
  163. package/dist/cli/index.js +0 -0
  164. package/dist/client/admin-config.d.ts +0 -1
  165. package/dist/client/admin-config.d.ts.map +1 -1
  166. package/dist/client/admin-config.js +0 -1
  167. package/dist/client/admin-config.js.map +1 -1
  168. package/dist/client/adminApi.d.ts.map +1 -1
  169. package/dist/client/agentTools.d.ts +1237 -135
  170. package/dist/client/agentTools.d.ts.map +1 -1
  171. package/dist/client/agentTools.js +33 -9
  172. package/dist/client/agentTools.js.map +1 -1
  173. package/dist/client/index.d.ts +1 -1
  174. package/dist/client/index.d.ts.map +1 -1
  175. package/dist/client/index.js.map +1 -1
  176. package/dist/component/_generated/component.d.ts +9 -0
  177. package/dist/component/_generated/component.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.d.ts +35 -0
  179. package/dist/component/mediaAssets.d.ts.map +1 -1
  180. package/dist/component/mediaAssets.js +81 -0
  181. package/dist/component/mediaAssets.js.map +1 -1
  182. package/dist/test.d.ts.map +1 -1
  183. package/dist/test.js +2 -1
  184. package/dist/test.js.map +1 -1
  185. package/package.json +24 -9
  186. package/dist/component/auditLog.d.ts +0 -410
  187. package/dist/component/auditLog.d.ts.map +0 -1
  188. package/dist/component/auditLog.js +0 -607
  189. package/dist/component/auditLog.js.map +0 -1
  190. package/dist/component/types.d.ts +0 -4
  191. package/dist/component/types.d.ts.map +0 -1
  192. package/dist/component/types.js +0 -2
  193. package/dist/component/types.js.map +0 -1
  194. package/src/cli/commands/admin.ts +0 -104
  195. package/src/cli/index.ts +0 -21
  196. package/src/cli/utils/detectConvexUrl.ts +0 -54
  197. package/src/cli/utils/openBrowser.ts +0 -16
  198. package/src/client/admin-config.ts +0 -138
  199. package/src/client/adminApi.ts +0 -942
  200. package/src/client/agentTools.ts +0 -1311
  201. package/src/client/argTypes.ts +0 -316
  202. package/src/client/field-types.ts +0 -187
  203. package/src/client/index.ts +0 -1301
  204. package/src/client/queryBuilder.ts +0 -1100
  205. package/src/client/schema/codegen.ts +0 -500
  206. package/src/client/schema/defineContentType.ts +0 -501
  207. package/src/client/schema/index.ts +0 -169
  208. package/src/client/schema/schemaDrift.ts +0 -574
  209. package/src/client/schema/typedClient.ts +0 -688
  210. package/src/client/schema/types.ts +0 -666
  211. package/src/client/types.ts +0 -723
  212. package/src/client/workflows.ts +0 -141
  213. package/src/client/wrapper.ts +0 -4304
  214. package/src/component/_generated/api.ts +0 -140
  215. package/src/component/_generated/component.ts +0 -5029
  216. package/src/component/_generated/dataModel.ts +0 -60
  217. package/src/component/_generated/server.ts +0 -156
  218. package/src/component/authorization.ts +0 -647
  219. package/src/component/authorizationHooks.ts +0 -668
  220. package/src/component/bulkOperations.ts +0 -687
  221. package/src/component/contentEntries.ts +0 -1976
  222. package/src/component/contentEntryMutations.ts +0 -1223
  223. package/src/component/contentEntryValidation.ts +0 -707
  224. package/src/component/contentLock.ts +0 -550
  225. package/src/component/contentTypeMigration.ts +0 -1064
  226. package/src/component/contentTypeMutations.ts +0 -969
  227. package/src/component/contentTypes.ts +0 -346
  228. package/src/component/convex.config.ts +0 -44
  229. package/src/component/documentTypes.ts +0 -240
  230. package/src/component/eventEmitter.ts +0 -485
  231. package/src/component/exportImport.ts +0 -1169
  232. package/src/component/index.ts +0 -491
  233. package/src/component/lib/deepReferenceResolver.ts +0 -999
  234. package/src/component/lib/errors.ts +0 -816
  235. package/src/component/lib/index.ts +0 -145
  236. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  237. package/src/component/lib/metadataExtractor.ts +0 -792
  238. package/src/component/lib/mutationAuth.ts +0 -199
  239. package/src/component/lib/queries.ts +0 -79
  240. package/src/component/lib/ragContentChunker.ts +0 -1371
  241. package/src/component/lib/referenceResolver.ts +0 -430
  242. package/src/component/lib/slugGenerator.ts +0 -262
  243. package/src/component/lib/slugUniqueness.ts +0 -333
  244. package/src/component/lib/softDelete.ts +0 -44
  245. package/src/component/localeFallbackChain.ts +0 -673
  246. package/src/component/localeFields.ts +0 -896
  247. package/src/component/mediaAssetMutations.ts +0 -725
  248. package/src/component/mediaAssets.ts +0 -932
  249. package/src/component/mediaFolderMutations.ts +0 -1046
  250. package/src/component/mediaUploadMutations.ts +0 -224
  251. package/src/component/mediaVariantMutations.ts +0 -900
  252. package/src/component/mediaVariants.ts +0 -793
  253. package/src/component/ragContentIndexer.ts +0 -1067
  254. package/src/component/rateLimitHooks.ts +0 -572
  255. package/src/component/roles.ts +0 -1360
  256. package/src/component/scheduledPublish.ts +0 -358
  257. package/src/component/schema.ts +0 -617
  258. package/src/component/taxonomies.ts +0 -949
  259. package/src/component/taxonomyMutations.ts +0 -1210
  260. package/src/component/trash.ts +0 -724
  261. package/src/component/userContext.ts +0 -898
  262. package/src/component/validation.ts +0 -1388
  263. package/src/component/validators.ts +0 -949
  264. package/src/component/versionMutations.ts +0 -392
  265. package/src/component/webhookTrigger.ts +0 -1922
  266. package/src/react/index.ts +0 -898
  267. package/src/test.ts +0 -1580
@@ -1,333 +0,0 @@
1
- /**
2
- * Slug Uniqueness Utilities
3
- *
4
- * Provides functions for checking slug uniqueness within content type scope
5
- * and generating unique slugs with incremental suffixes when conflicts exist.
6
- */
7
-
8
- import {
9
- generateSlug,
10
- generateUniqueSlug,
11
- isValidSlug,
12
- } from "./slugGenerator.js";
13
-
14
- /**
15
- * Options for slug uniqueness checking
16
- */
17
- export interface SlugUniquenessOptions {
18
- /** Maximum number of suffix attempts before falling back to timestamp (default: 100) */
19
- maxAttempts?: number;
20
- /** Separator character used in slugs (default: '-') */
21
- separator?: string;
22
- /** ID of the current entry to exclude from uniqueness check (for updates) */
23
- excludeEntryId?: string;
24
- }
25
-
26
- /**
27
- * Result of a slug uniqueness check
28
- */
29
- export interface SlugCheckResult {
30
- /** Whether the slug is unique */
31
- isUnique: boolean;
32
- /** The existing entry ID that has this slug (if not unique) */
33
- existingEntryId?: string;
34
- /** Suggested alternative slug if not unique */
35
- suggestedSlug?: string;
36
- }
37
-
38
- /**
39
- * Entry data structure for uniqueness checking
40
- */
41
- export interface SlugEntry {
42
- /** The entry's unique identifier */
43
- _id: string;
44
- /** The entry's slug */
45
- slug: string;
46
- /** Soft delete timestamp (null/undefined if not deleted) */
47
- deletedAt?: number | null;
48
- }
49
-
50
- /**
51
- * Function type for querying existing entries by slug within a content type
52
- */
53
- export type SlugQueryFn = (slug: string) => Promise<SlugEntry | null>;
54
-
55
- /**
56
- * Function type for querying all entries with a slug prefix within a content type
57
- * Used for finding the next available suffix number
58
- */
59
- export type SlugPrefixQueryFn = (slugPrefix: string) => Promise<SlugEntry[]>;
60
-
61
- /**
62
- * Checks if a slug is unique within a content type scope.
63
- *
64
- * @param slug - The slug to check
65
- * @param queryFn - Function that queries the database for entries with the given slug
66
- * @param options - Configuration options
67
- * @returns Result indicating uniqueness and suggestions
68
- *
69
- * @example
70
- * ```typescript
71
- * // In a Convex mutation
72
- * const queryFn = async (slug: string) => {
73
- * return await ctx.db
74
- * .query("contentEntries")
75
- * .withIndex("by_content_type_and_slug", (q) =>
76
- * q.eq("contentTypeId", contentTypeId).eq("slug", slug)
77
- * )
78
- * .filter((q) => q.eq(q.field("deletedAt"), undefined))
79
- * .first();
80
- * };
81
- *
82
- * const result = await checkSlugUniqueness("my-post", queryFn);
83
- * if (!result.isUnique) {
84
- * console.log(`Slug conflict with entry ${result.existingEntryId}`);
85
- * console.log(`Suggested alternative: ${result.suggestedSlug}`);
86
- * }
87
- * ```
88
- */
89
- export async function checkSlugUniqueness(
90
- slug: string,
91
- queryFn: SlugQueryFn,
92
- options: SlugUniquenessOptions = {},
93
- ): Promise<SlugCheckResult> {
94
- const { excludeEntryId } = options;
95
-
96
- // Validate the slug format
97
- if (!isValidSlug(slug, options.separator)) {
98
- return {
99
- isUnique: false,
100
- suggestedSlug: generateSlug(slug, { separator: options.separator }),
101
- };
102
- }
103
-
104
- // Query for existing entry with this slug
105
- const existingEntry = await queryFn(slug);
106
-
107
- // Check if the slug is available
108
- if (!existingEntry) {
109
- return { isUnique: true };
110
- }
111
-
112
- // If we're updating an entry, exclude it from the check
113
- if (excludeEntryId && existingEntry._id === excludeEntryId) {
114
- return { isUnique: true };
115
- }
116
-
117
- // Slug is taken - generate a suggestion
118
- const isUniqueFn = async (candidateSlug: string): Promise<boolean> => {
119
- const entry = await queryFn(candidateSlug);
120
- if (!entry) return true;
121
- if (excludeEntryId && entry._id === excludeEntryId) return true;
122
- return false;
123
- };
124
-
125
- const suggestedSlug = await generateUniqueSlug(
126
- slug,
127
- isUniqueFn,
128
- options.maxAttempts,
129
- );
130
-
131
- return {
132
- isUnique: false,
133
- existingEntryId: existingEntry._id,
134
- suggestedSlug,
135
- };
136
- }
137
-
138
- /**
139
- * Ensures a slug is unique by generating incremental suffixes if needed.
140
- * This is the main function to use when creating or updating content entries.
141
- *
142
- * @param baseSlug - The desired slug (or title to generate slug from)
143
- * @param queryFn - Function that queries the database for entries with a given slug
144
- * @param options - Configuration options
145
- * @returns A unique slug (either the original or with a numeric suffix)
146
- *
147
- * @example
148
- * ```typescript
149
- * // In a Convex mutation for creating a new entry
150
- * const queryFn = async (slug: string) => {
151
- * return await ctx.db
152
- * .query("contentEntries")
153
- * .withIndex("by_content_type_and_slug", (q) =>
154
- * q.eq("contentTypeId", contentTypeId).eq("slug", slug)
155
- * )
156
- * .filter((q) => q.eq(q.field("deletedAt"), undefined))
157
- * .first();
158
- * };
159
- *
160
- * const uniqueSlug = await ensureUniqueSlug("my-post", queryFn);
161
- * // Returns "my-post" if unique, or "my-post-1", "my-post-2", etc.
162
- * ```
163
- */
164
- export async function ensureUniqueSlug(
165
- baseSlug: string,
166
- queryFn: SlugQueryFn,
167
- options: SlugUniquenessOptions = {},
168
- ): Promise<string> {
169
- const { excludeEntryId, maxAttempts = 100 } = options;
170
-
171
- // Validate and normalize the base slug
172
- let slug = baseSlug;
173
- if (!isValidSlug(slug, options.separator)) {
174
- slug = generateSlug(slug, { separator: options.separator });
175
- }
176
-
177
- // If slug is empty after normalization, use a fallback
178
- if (!slug) {
179
- slug = "untitled";
180
- }
181
-
182
- // Check if the base slug is available
183
- const isUniqueFn = async (candidateSlug: string): Promise<boolean> => {
184
- const entry = await queryFn(candidateSlug);
185
- if (!entry) return true;
186
- if (excludeEntryId && entry._id === excludeEntryId) return true;
187
- return false;
188
- };
189
-
190
- return generateUniqueSlug(slug, isUniqueFn, maxAttempts);
191
- }
192
-
193
- /**
194
- * Finds the next available slug suffix by analyzing existing slugs.
195
- * This is useful when you want to pre-compute the next suffix without
196
- * iterating through each number.
197
- *
198
- * @param baseSlug - The base slug to find the next suffix for
199
- * @param prefixQueryFn - Function that returns all entries with slugs starting with the base
200
- * @param options - Configuration options
201
- * @returns The next available slug with the appropriate suffix
202
- *
203
- * @example
204
- * ```typescript
205
- * // If entries exist with slugs: "post", "post-1", "post-2", "post-5"
206
- * const nextSlug = await findNextAvailableSlug("post", queryPrefixFn);
207
- * // Returns "post-3" (fills the gap)
208
- * ```
209
- */
210
- export async function findNextAvailableSlug(
211
- baseSlug: string,
212
- prefixQueryFn: SlugPrefixQueryFn,
213
- options: SlugUniquenessOptions = {},
214
- ): Promise<string> {
215
- const { excludeEntryId, separator = "-" } = options;
216
-
217
- // Validate the base slug
218
- let slug = baseSlug;
219
- if (!isValidSlug(slug, separator)) {
220
- slug = generateSlug(slug, { separator });
221
- }
222
-
223
- if (!slug) {
224
- slug = "untitled";
225
- }
226
-
227
- // Get all entries with this prefix
228
- const existingEntries = await prefixQueryFn(slug);
229
-
230
- // Filter out the excluded entry and soft-deleted entries
231
- const activeEntries = existingEntries.filter((entry) => {
232
- if (excludeEntryId && entry._id === excludeEntryId) return false;
233
- if (entry.deletedAt) return false;
234
- return true;
235
- });
236
-
237
- // If no entries exist with this slug, the base is available
238
- const hasSlugsToCheck = activeEntries.some((entry) => {
239
- return entry.slug === slug || entry.slug.startsWith(`${slug}${separator}`);
240
- });
241
-
242
- if (!hasSlugsToCheck) {
243
- return slug;
244
- }
245
-
246
- // Check if base slug itself is taken
247
- const baseIsTaken = activeEntries.some((entry) => entry.slug === slug);
248
- if (!baseIsTaken) {
249
- return slug;
250
- }
251
-
252
- // Extract existing suffix numbers
253
- const suffixPattern = new RegExp(
254
- `^${escapeRegex(slug)}${escapeRegex(separator)}(\\d+)$`,
255
- );
256
- const usedNumbers = new Set<number>();
257
-
258
- for (const entry of activeEntries) {
259
- const match = entry.slug.match(suffixPattern);
260
- if (match) {
261
- usedNumbers.add(parseInt(match[1], 10));
262
- }
263
- }
264
-
265
- // Find the smallest available number
266
- let nextNumber = 1;
267
- while (usedNumbers.has(nextNumber)) {
268
- nextNumber++;
269
- }
270
-
271
- return `${slug}${separator}${nextNumber}`;
272
- }
273
-
274
- /**
275
- * Escapes special regex characters in a string
276
- */
277
- function escapeRegex(str: string): string {
278
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
279
- }
280
-
281
- /**
282
- * Validates a slug and returns validation errors if any.
283
- *
284
- * @param slug - The slug to validate
285
- * @param separator - The separator character (default: '-')
286
- * @returns Array of validation error messages (empty if valid)
287
- */
288
- export function validateSlugFormat(
289
- slug: string,
290
- separator: string = "-",
291
- ): string[] {
292
- const errors: string[] = [];
293
-
294
- if (!slug || typeof slug !== "string") {
295
- errors.push("Slug is required");
296
- return errors;
297
- }
298
-
299
- if (slug.length === 0) {
300
- errors.push("Slug cannot be empty");
301
- return errors;
302
- }
303
-
304
- if (slug.length > 100) {
305
- errors.push("Slug must be 100 characters or less");
306
- }
307
-
308
- if (slug !== slug.toLowerCase()) {
309
- errors.push("Slug must be lowercase");
310
- }
311
-
312
- if (slug.startsWith(separator)) {
313
- errors.push(`Slug cannot start with '${separator}'`);
314
- }
315
-
316
- if (slug.endsWith(separator)) {
317
- errors.push(`Slug cannot end with '${separator}'`);
318
- }
319
-
320
- const doubleSeparatorRegex = new RegExp(`${escapeRegex(separator)}{2,}`);
321
- if (doubleSeparatorRegex.test(slug)) {
322
- errors.push(`Slug cannot contain consecutive '${separator}' characters`);
323
- }
324
-
325
- const invalidCharsRegex = new RegExp(`[^a-z0-9${escapeRegex(separator)}]`);
326
- if (invalidCharsRegex.test(slug)) {
327
- errors.push(
328
- "Slug can only contain lowercase letters, numbers, and hyphens",
329
- );
330
- }
331
-
332
- return errors;
333
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Soft-delete utilities for CMS documents.
3
- *
4
- * Provides type-safe helpers for working with documents that use
5
- * the soft-delete pattern (deletedAt timestamp).
6
- */
7
-
8
- export interface SoftDeletable {
9
- deletedAt?: number;
10
- }
11
-
12
- export function isDeleted<T extends SoftDeletable>(doc: T): boolean {
13
- return doc.deletedAt !== undefined;
14
- }
15
-
16
- export function isActive<T extends SoftDeletable>(doc: T): boolean {
17
- return doc.deletedAt === undefined;
18
- }
19
-
20
- export function filterActive<T extends SoftDeletable>(docs: T[]): T[] {
21
- return docs.filter(isActive);
22
- }
23
-
24
- export function filterDeleted<T extends SoftDeletable>(docs: T[]): T[] {
25
- return docs.filter(isDeleted);
26
- }
27
-
28
- export function requireNotDeleted<T extends SoftDeletable>(
29
- doc: T,
30
- errorFactory: () => Error
31
- ): asserts doc is T & { deletedAt: undefined } {
32
- if (isDeleted(doc)) {
33
- throw errorFactory();
34
- }
35
- }
36
-
37
- export function requireDeleted<T extends SoftDeletable>(
38
- doc: T,
39
- errorFactory: () => Error
40
- ): void {
41
- if (!isDeleted(doc)) {
42
- throw errorFactory();
43
- }
44
- }