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,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
- }