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
package/src/test.ts DELETED
@@ -1,1580 +0,0 @@
1
- /// <reference types="vite/client" />
2
- /**
3
- * Test helpers for the Convex CMS component.
4
- *
5
- * This module provides utilities for testing applications that use the Convex CMS component:
6
- * - Component registration helpers for convex-test
7
- * - Mock data factories for content types, entries, and media assets
8
- * - Assertion utilities for validating CMS-specific structures
9
- *
10
- * @example
11
- * ```typescript
12
- * import { convexTest } from "convex-test";
13
- * import { describe, it, expect } from "vitest";
14
- * import {
15
- * register,
16
- * contentTypeFactory,
17
- * contentEntryFactory,
18
- * assertContentType,
19
- * } from "@convex-cms/core/test";
20
- * import schema from "./schema";
21
- *
22
- * const modules = import.meta.glob("./**\/*.ts");
23
- *
24
- * describe("my CMS tests", () => {
25
- * it("creates content types correctly", async () => {
26
- * const t = convexTest(schema, modules);
27
- * register(t, "convexCms");
28
- *
29
- * // Create test data using factories
30
- * const blogPostType = contentTypeFactory.blogPost();
31
- *
32
- * // Insert into test database
33
- * const typeId = await t.run(async (ctx) => {
34
- * return await ctx.db.insert("contentTypes", blogPostType);
35
- * });
36
- *
37
- * // Assert the structure is correct
38
- * const result = await t.run(async (ctx) => {
39
- * return await ctx.db.get(typeId);
40
- * });
41
- * assertContentType(result);
42
- * });
43
- * });
44
- * ```
45
- *
46
- * @module
47
- */
48
-
49
- import type { TestConvex } from "convex-test";
50
- import type { GenericSchema, SchemaDefinition } from "convex/server";
51
- import type { GenericId } from "convex/values";
52
- import schema from "./component/schema.js";
53
- import type {
54
- FieldType,
55
- ContentStatus,
56
- // MediaType,
57
- } from "./component/validators.js";
58
-
59
- // Generic ID type alias for convenience
60
- type Id<TableName extends string> = GenericId<TableName>;
61
-
62
- // Import all component modules for testing
63
- const modules = import.meta.glob("./component/**/*.ts");
64
-
65
- // =============================================================================
66
- // Type Definitions
67
- // =============================================================================
68
-
69
- /**
70
- * Simplified field definition for test factories.
71
- * This is a flattened interface that covers all field types for convenience.
72
- * When used in ContentTypeData, it must be cast to the schema's discriminated union type.
73
- */
74
- export interface TestFieldDefinition {
75
- name: string;
76
- label: string;
77
- type: FieldType;
78
- required: boolean;
79
- searchable?: boolean;
80
- localized?: boolean;
81
- description?: string;
82
- defaultValue?: unknown;
83
- options?: FieldOptions;
84
- }
85
-
86
- /**
87
- * Field-specific options.
88
- */
89
- export interface FieldOptions {
90
- // Text fields
91
- minLength?: number;
92
- maxLength?: number;
93
- pattern?: string;
94
-
95
- // Number fields
96
- min?: number;
97
- max?: number;
98
- step?: number;
99
- precision?: number;
100
-
101
- // Reference fields
102
- allowedContentTypes?: string[];
103
- multiple?: boolean;
104
- minItems?: number;
105
-
106
- // Media fields
107
- allowedMimeTypes?: string[];
108
- maxFileSize?: number;
109
-
110
- // Select fields
111
- options?: { value: string; label: string }[];
112
-
113
- // Rich text fields
114
- allowedBlocks?: string[];
115
- allowedMarks?: string[];
116
- }
117
-
118
- // =============================================================================
119
- // Data Types for Test Factories (without system fields)
120
- // =============================================================================
121
-
122
- /**
123
- * These types represent the data you INSERT into the database,
124
- * derived from internal document types by omitting system fields.
125
- * This ensures test factories stay in sync with schema definitions.
126
- */
127
- import type {
128
- ContentTypeInternal,
129
- ContentEntryInternal,
130
- MediaAssetInternal,
131
- MediaFolderInternal,
132
- } from "./component/documentTypes.js";
133
-
134
- /**
135
- * Content type data structure (without system fields).
136
- * For testing, some fields that are required at runtime have defaults.
137
- * Uses the simplified TestFieldDefinition for convenience in test factories.
138
- */
139
- export type ContentTypeData = Omit<
140
- ContentTypeInternal,
141
- "_id" | "_creationTime" | "fields" | "createdBy"
142
- > & {
143
- createdBy?: string;
144
- fields: TestFieldDefinition[];
145
- };
146
-
147
- /**
148
- * Content entry data structure (without system fields).
149
- * Derived from ContentEntryInternal for type safety.
150
- */
151
- export type ContentEntryData = Omit<
152
- ContentEntryInternal,
153
- "_id" | "_creationTime"
154
- >;
155
-
156
- /**
157
- * Media asset data structure (without system fields).
158
- * Uses 'name' for filename (matching unified schema).
159
- */
160
- export type MediaAssetData = Omit<MediaAssetInternal, "_id" | "_creationTime">;
161
-
162
- /**
163
- * Media folder data structure (without system fields).
164
- * Derived from MediaFolderInternal for type safety.
165
- */
166
- export type MediaFolderData = Omit<
167
- MediaFolderInternal,
168
- "_id" | "_creationTime"
169
- >;
170
-
171
- // =============================================================================
172
- // Component Registration
173
- // =============================================================================
174
-
175
- /**
176
- * Register the Convex CMS component with a convex-test instance.
177
- *
178
- * @param t - The test convex instance from calling `convexTest()`
179
- * @param name - The name of the component as registered in convex.config.ts
180
- * Defaults to "convexCms"
181
- *
182
- * @example
183
- * ```typescript
184
- * import { convexTest } from "convex-test";
185
- * import { register } from "@convex-cms/core/test";
186
- * import schema from "./schema";
187
- *
188
- * const modules = import.meta.glob("./**\/*.ts");
189
- *
190
- * test("my test", async () => {
191
- * const t = convexTest(schema, modules);
192
- * register(t, "convexCms");
193
- * // Your tests here
194
- * });
195
- * ```
196
- */
197
- export function register(
198
- t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
199
- name: string = "convexCms",
200
- ) {
201
- t.registerComponent(name, schema, modules);
202
- }
203
-
204
- // =============================================================================
205
- // Field Factories
206
- // =============================================================================
207
-
208
- /**
209
- * Factory functions for creating common field definitions.
210
- * These produce valid field structures for use in content type definitions.
211
- */
212
- export const fieldFactory = {
213
- /**
214
- * Create a text field definition.
215
- */
216
- text(
217
- name: string,
218
- label: string,
219
- options: {
220
- required?: boolean;
221
- searchable?: boolean;
222
- localized?: boolean;
223
- description?: string;
224
- defaultValue?: string;
225
- minLength?: number;
226
- maxLength?: number;
227
- pattern?: string;
228
- } = {},
229
- ): TestFieldDefinition {
230
- return {
231
- name,
232
- label,
233
- type: "text",
234
- required: options.required ?? false,
235
- searchable: options.searchable,
236
- localized: options.localized,
237
- description: options.description,
238
- defaultValue: options.defaultValue,
239
- options:
240
- options.minLength !== undefined ||
241
- options.maxLength !== undefined ||
242
- options.pattern !== undefined
243
- ? {
244
- minLength: options.minLength,
245
- maxLength: options.maxLength,
246
- pattern: options.pattern,
247
- }
248
- : undefined,
249
- };
250
- },
251
-
252
- /**
253
- * Create a rich text field definition.
254
- */
255
- richText(
256
- name: string,
257
- label: string,
258
- options: {
259
- required?: boolean;
260
- searchable?: boolean;
261
- localized?: boolean;
262
- description?: string;
263
- allowedBlocks?: string[];
264
- allowedMarks?: string[];
265
- } = {},
266
- ): TestFieldDefinition {
267
- return {
268
- name,
269
- label,
270
- type: "richText",
271
- required: options.required ?? false,
272
- searchable: options.searchable,
273
- localized: options.localized,
274
- description: options.description,
275
- options:
276
- options.allowedBlocks !== undefined ||
277
- options.allowedMarks !== undefined
278
- ? {
279
- allowedBlocks: options.allowedBlocks,
280
- allowedMarks: options.allowedMarks,
281
- }
282
- : undefined,
283
- };
284
- },
285
-
286
- /**
287
- * Create a number field definition.
288
- */
289
- number(
290
- name: string,
291
- label: string,
292
- options: {
293
- required?: boolean;
294
- description?: string;
295
- defaultValue?: number;
296
- min?: number;
297
- max?: number;
298
- step?: number;
299
- precision?: number;
300
- } = {},
301
- ): TestFieldDefinition {
302
- return {
303
- name,
304
- label,
305
- type: "number",
306
- required: options.required ?? false,
307
- description: options.description,
308
- defaultValue: options.defaultValue,
309
- options:
310
- options.min !== undefined ||
311
- options.max !== undefined ||
312
- options.step !== undefined ||
313
- options.precision !== undefined
314
- ? {
315
- min: options.min,
316
- max: options.max,
317
- step: options.step,
318
- precision: options.precision,
319
- }
320
- : undefined,
321
- };
322
- },
323
-
324
- /**
325
- * Create a boolean field definition.
326
- */
327
- boolean(
328
- name: string,
329
- label: string,
330
- options: {
331
- required?: boolean;
332
- description?: string;
333
- defaultValue?: boolean;
334
- } = {},
335
- ): TestFieldDefinition {
336
- return {
337
- name,
338
- label,
339
- type: "boolean",
340
- required: options.required ?? false,
341
- description: options.description,
342
- defaultValue: options.defaultValue,
343
- };
344
- },
345
-
346
- /**
347
- * Create a date field definition.
348
- */
349
- date(
350
- name: string,
351
- label: string,
352
- options: {
353
- required?: boolean;
354
- description?: string;
355
- } = {},
356
- ): TestFieldDefinition {
357
- return {
358
- name,
359
- label,
360
- type: "date",
361
- required: options.required ?? false,
362
- description: options.description,
363
- };
364
- },
365
-
366
- /**
367
- * Create a datetime field definition.
368
- */
369
- datetime(
370
- name: string,
371
- label: string,
372
- options: {
373
- required?: boolean;
374
- description?: string;
375
- } = {},
376
- ): TestFieldDefinition {
377
- return {
378
- name,
379
- label,
380
- type: "datetime",
381
- required: options.required ?? false,
382
- description: options.description,
383
- };
384
- },
385
-
386
- /**
387
- * Create a reference field definition.
388
- */
389
- reference(
390
- name: string,
391
- label: string,
392
- options: {
393
- required?: boolean;
394
- description?: string;
395
- allowedContentTypes?: string[];
396
- multiple?: boolean;
397
- minItems?: number;
398
- } = {},
399
- ): TestFieldDefinition {
400
- return {
401
- name,
402
- label,
403
- type: "reference",
404
- required: options.required ?? false,
405
- description: options.description,
406
- options: {
407
- allowedContentTypes: options.allowedContentTypes,
408
- multiple: options.multiple,
409
- minItems: options.minItems,
410
- },
411
- };
412
- },
413
-
414
- /**
415
- * Create a media field definition.
416
- */
417
- media(
418
- name: string,
419
- label: string,
420
- options: {
421
- required?: boolean;
422
- description?: string;
423
- allowedMimeTypes?: string[];
424
- maxFileSize?: number;
425
- multiple?: boolean;
426
- } = {},
427
- ): TestFieldDefinition {
428
- return {
429
- name,
430
- label,
431
- type: "media",
432
- required: options.required ?? false,
433
- description: options.description,
434
- options: {
435
- allowedMimeTypes: options.allowedMimeTypes,
436
- maxFileSize: options.maxFileSize,
437
- multiple: options.multiple,
438
- },
439
- };
440
- },
441
-
442
- /**
443
- * Create a JSON field definition.
444
- */
445
- json(
446
- name: string,
447
- label: string,
448
- options: {
449
- required?: boolean;
450
- description?: string;
451
- defaultValue?: unknown;
452
- } = {},
453
- ): TestFieldDefinition {
454
- return {
455
- name,
456
- label,
457
- type: "json",
458
- required: options.required ?? false,
459
- description: options.description,
460
- defaultValue: options.defaultValue,
461
- };
462
- },
463
-
464
- /**
465
- * Create a select field definition.
466
- */
467
- select(
468
- name: string,
469
- label: string,
470
- selectOptions: { value: string; label: string }[],
471
- options: {
472
- required?: boolean;
473
- description?: string;
474
- defaultValue?: string;
475
- } = {},
476
- ): TestFieldDefinition {
477
- return {
478
- name,
479
- label,
480
- type: "select",
481
- required: options.required ?? false,
482
- description: options.description,
483
- defaultValue: options.defaultValue,
484
- options: {
485
- options: selectOptions,
486
- },
487
- };
488
- },
489
-
490
- /**
491
- * Create a multi-select field definition.
492
- */
493
- multiSelect(
494
- name: string,
495
- label: string,
496
- selectOptions: { value: string; label: string }[],
497
- options: {
498
- required?: boolean;
499
- description?: string;
500
- defaultValue?: string[];
501
- } = {},
502
- ): TestFieldDefinition {
503
- return {
504
- name,
505
- label,
506
- type: "multiSelect",
507
- required: options.required ?? false,
508
- description: options.description,
509
- defaultValue: options.defaultValue,
510
- options: {
511
- options: selectOptions,
512
- },
513
- };
514
- },
515
- };
516
-
517
- // =============================================================================
518
- // Content Type Factories
519
- // =============================================================================
520
-
521
- /**
522
- * Factory functions for creating test content type data.
523
- * All factories return data suitable for inserting into the contentTypes table.
524
- */
525
- export const contentTypeFactory = {
526
- /**
527
- * Create a minimal valid content type.
528
- */
529
- minimal(name: string = "test_type"): ContentTypeData {
530
- return {
531
- name,
532
- displayName: name
533
- .split("_")
534
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
535
- .join(" "),
536
- createdBy: "test-user",
537
- fields: [],
538
- isActive: true,
539
- };
540
- },
541
-
542
- /**
543
- * Create a blog post content type with common fields.
544
- */
545
- blogPost(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
546
- return {
547
- name: "blog_post",
548
- displayName: "Blog Post",
549
- description: "A blog post content type for testing",
550
- createdBy: "test-user",
551
- fields: [
552
- fieldFactory.text("title", "Title", {
553
- required: true,
554
- searchable: true,
555
- maxLength: 200,
556
- }),
557
- fieldFactory.text("slug", "Slug", { required: true }),
558
- fieldFactory.richText("content", "Content", {
559
- required: true,
560
- searchable: true,
561
- localized: true,
562
- }),
563
- fieldFactory.text("excerpt", "Excerpt", {
564
- searchable: true,
565
- maxLength: 500,
566
- }),
567
- fieldFactory.media("featuredImage", "Featured Image", {
568
- allowedMimeTypes: ["image/*"],
569
- }),
570
- fieldFactory.reference("author", "Author", {
571
- allowedContentTypes: ["author"],
572
- }),
573
- fieldFactory.multiSelect(
574
- "tags",
575
- "Tags",
576
- [
577
- { value: "tech", label: "Technology" },
578
- { value: "news", label: "News" },
579
- { value: "tutorial", label: "Tutorial" },
580
- ],
581
- {},
582
- ),
583
- fieldFactory.datetime("publishedAt", "Published At"),
584
- ],
585
- icon: "📝",
586
- slugField: "title",
587
- titleField: "title",
588
- isActive: true,
589
- ...overrides,
590
- };
591
- },
592
-
593
- /**
594
- * Create a product content type for e-commerce testing.
595
- */
596
- product(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
597
- return {
598
- name: "product",
599
- displayName: "Product",
600
- description: "A product content type for testing",
601
- createdBy: "test-user",
602
- fields: [
603
- fieldFactory.text("name", "Product Name", {
604
- required: true,
605
- searchable: true,
606
- }),
607
- fieldFactory.text("sku", "SKU", { required: true }),
608
- fieldFactory.richText("description", "Description", {
609
- searchable: true,
610
- }),
611
- fieldFactory.number("price", "Price", {
612
- required: true,
613
- min: 0,
614
- precision: 2,
615
- }),
616
- fieldFactory.number("stock", "Stock Quantity", { min: 0 }),
617
- fieldFactory.media("images", "Product Images", {
618
- allowedMimeTypes: ["image/*"],
619
- multiple: true,
620
- }),
621
- fieldFactory.reference("category", "Category", {
622
- allowedContentTypes: ["category"],
623
- }),
624
- fieldFactory.select(
625
- "status",
626
- "Status",
627
- [
628
- { value: "active", label: "Active" },
629
- { value: "discontinued", label: "Discontinued" },
630
- { value: "out_of_stock", label: "Out of Stock" },
631
- ],
632
- { required: true, defaultValue: "active" },
633
- ),
634
- fieldFactory.json("metadata", "Metadata"),
635
- ],
636
- slugField: "name",
637
- titleField: "name",
638
- isActive: true,
639
- ...overrides,
640
- };
641
- },
642
-
643
- /**
644
- * Create an author content type.
645
- */
646
- author(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
647
- return {
648
- name: "author",
649
- displayName: "Author",
650
- description: "An author content type for testing",
651
- createdBy: "test-user",
652
- fields: [
653
- fieldFactory.text("name", "Name", { required: true, searchable: true }),
654
- fieldFactory.text("email", "Email"),
655
- fieldFactory.richText("bio", "Biography"),
656
- fieldFactory.media("avatar", "Avatar", {
657
- allowedMimeTypes: ["image/*"],
658
- }),
659
- fieldFactory.json("socialLinks", "Social Links"),
660
- ],
661
- slugField: "name",
662
- titleField: "name",
663
- isActive: true,
664
- ...overrides,
665
- };
666
- },
667
-
668
- /**
669
- * Create a category content type.
670
- */
671
- category(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
672
- return {
673
- name: "category",
674
- displayName: "Category",
675
- description: "A category content type for testing",
676
- createdBy: "test-user",
677
- fields: [
678
- fieldFactory.text("name", "Name", { required: true, searchable: true }),
679
- fieldFactory.text("description", "Description"),
680
- fieldFactory.media("icon", "Icon", { allowedMimeTypes: ["image/*"] }),
681
- fieldFactory.reference("parent", "Parent Category", {
682
- allowedContentTypes: ["category"],
683
- }),
684
- fieldFactory.number("sortOrder", "Sort Order", { defaultValue: 0 }),
685
- ],
686
- slugField: "name",
687
- titleField: "name",
688
- isActive: true,
689
- ...overrides,
690
- };
691
- },
692
-
693
- /**
694
- * Create a page content type (singleton-compatible).
695
- */
696
- page(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
697
- return {
698
- name: "page",
699
- displayName: "Page",
700
- description: "A page content type for testing",
701
- createdBy: "test-user",
702
- fields: [
703
- fieldFactory.text("title", "Title", {
704
- required: true,
705
- searchable: true,
706
- }),
707
- fieldFactory.richText("content", "Content", {
708
- required: true,
709
- localized: true,
710
- }),
711
- fieldFactory.text("metaTitle", "Meta Title"),
712
- fieldFactory.text("metaDescription", "Meta Description", {
713
- maxLength: 160,
714
- }),
715
- fieldFactory.media("ogImage", "Open Graph Image", {
716
- allowedMimeTypes: ["image/*"],
717
- }),
718
- ],
719
- slugField: "title",
720
- titleField: "title",
721
- isActive: true,
722
- ...overrides,
723
- };
724
- },
725
-
726
- /**
727
- * Create a singleton content type (e.g., for site settings).
728
- */
729
- siteSettings(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
730
- return {
731
- name: "site_settings",
732
- displayName: "Site Settings",
733
- description: "Global site settings (singleton)",
734
- createdBy: "test-user",
735
- fields: [
736
- fieldFactory.text("siteName", "Site Name", { required: true }),
737
- fieldFactory.text("tagline", "Tagline"),
738
- fieldFactory.media("logo", "Logo", { allowedMimeTypes: ["image/*"] }),
739
- fieldFactory.media("favicon", "Favicon", {
740
- allowedMimeTypes: ["image/*"],
741
- }),
742
- fieldFactory.json("socialLinks", "Social Links"),
743
- fieldFactory.json("analytics", "Analytics Config"),
744
- ],
745
- singleton: true,
746
- titleField: "siteName",
747
- isActive: true,
748
- ...overrides,
749
- };
750
- },
751
-
752
- /**
753
- * Create a content type with all supported field types.
754
- * Useful for comprehensive testing.
755
- */
756
- allFieldTypes(overrides: Partial<ContentTypeData> = {}): ContentTypeData {
757
- return {
758
- name: "all_fields",
759
- displayName: "All Field Types",
760
- description: "A content type with every supported field type",
761
- createdBy: "test-user",
762
- fields: [
763
- fieldFactory.text("textField", "Text Field", { required: true }),
764
- fieldFactory.richText("richTextField", "Rich Text Field"),
765
- fieldFactory.number("numberField", "Number Field"),
766
- fieldFactory.boolean("booleanField", "Boolean Field"),
767
- fieldFactory.date("dateField", "Date Field"),
768
- fieldFactory.datetime("datetimeField", "Datetime Field"),
769
- fieldFactory.reference("referenceField", "Reference Field"),
770
- fieldFactory.media("mediaField", "Media Field"),
771
- fieldFactory.json("jsonField", "JSON Field"),
772
- fieldFactory.select("selectField", "Select Field", [
773
- { value: "option1", label: "Option 1" },
774
- { value: "option2", label: "Option 2" },
775
- ]),
776
- fieldFactory.multiSelect("multiSelectField", "Multi Select Field", [
777
- { value: "choice1", label: "Choice 1" },
778
- { value: "choice2", label: "Choice 2" },
779
- { value: "choice3", label: "Choice 3" },
780
- ]),
781
- ],
782
- slugField: "textField",
783
- titleField: "textField",
784
- isActive: true,
785
- ...overrides,
786
- };
787
- },
788
-
789
- /**
790
- * Create a custom content type with specified fields.
791
- */
792
- custom(
793
- name: string,
794
- displayName: string,
795
- fields: TestFieldDefinition[],
796
- overrides: Partial<ContentTypeData> = {},
797
- ): ContentTypeData {
798
- return {
799
- name,
800
- displayName,
801
- createdBy: "test-user",
802
- fields,
803
- isActive: true,
804
- ...overrides,
805
- };
806
- },
807
- };
808
-
809
- // =============================================================================
810
- // Content Entry Factories
811
- // =============================================================================
812
-
813
- let entryCounter = 0;
814
-
815
- /**
816
- * Factory functions for creating test content entry data.
817
- * All factories return data suitable for inserting into the contentEntries table.
818
- *
819
- * Note: You'll need to provide a valid contentTypeId when inserting into the database.
820
- */
821
- export const contentEntryFactory = {
822
- /**
823
- * Reset the internal counter used for generating unique slugs.
824
- * Call this in beforeEach() if you need predictable slug values.
825
- */
826
- resetCounter(): void {
827
- entryCounter = 0;
828
- },
829
-
830
- /**
831
- * Create a minimal valid content entry.
832
- */
833
- minimal(
834
- contentTypeId: Id<"contentTypes">,
835
- overrides: Partial<ContentEntryData> = {},
836
- ): ContentEntryData {
837
- entryCounter++;
838
- return {
839
- contentTypeId,
840
- slug: `test-entry-${entryCounter}`,
841
- status: "draft",
842
- data: {},
843
- version: 1,
844
- ...overrides,
845
- };
846
- },
847
-
848
- /**
849
- * Create a draft content entry.
850
- */
851
- draft(
852
- contentTypeId: Id<"contentTypes">,
853
- data: Record<string, unknown>,
854
- overrides: Partial<ContentEntryData> = {},
855
- ): ContentEntryData {
856
- entryCounter++;
857
- return {
858
- contentTypeId,
859
- slug: `draft-${entryCounter}`,
860
- status: "draft",
861
- data,
862
- version: 1,
863
- ...overrides,
864
- };
865
- },
866
-
867
- /**
868
- * Create a published content entry.
869
- */
870
- published(
871
- contentTypeId: Id<"contentTypes">,
872
- data: Record<string, unknown>,
873
- overrides: Partial<ContentEntryData> = {},
874
- ): ContentEntryData {
875
- entryCounter++;
876
- const now = Date.now();
877
- return {
878
- contentTypeId,
879
- slug: `published-${entryCounter}`,
880
- status: "published",
881
- data,
882
- version: 1,
883
- firstPublishedAt: now,
884
- lastPublishedAt: now,
885
- ...overrides,
886
- };
887
- },
888
-
889
- /**
890
- * Create an archived content entry.
891
- */
892
- archived(
893
- contentTypeId: Id<"contentTypes">,
894
- data: Record<string, unknown>,
895
- overrides: Partial<ContentEntryData> = {},
896
- ): ContentEntryData {
897
- entryCounter++;
898
- return {
899
- contentTypeId,
900
- slug: `archived-${entryCounter}`,
901
- status: "archived",
902
- data,
903
- version: 1,
904
- ...overrides,
905
- };
906
- },
907
-
908
- /**
909
- * Create a scheduled content entry.
910
- */
911
- scheduled(
912
- contentTypeId: Id<"contentTypes">,
913
- data: Record<string, unknown>,
914
- scheduledPublishAt: number,
915
- overrides: Partial<ContentEntryData> = {},
916
- ): ContentEntryData {
917
- entryCounter++;
918
- return {
919
- contentTypeId,
920
- slug: `scheduled-${entryCounter}`,
921
- status: "scheduled",
922
- data,
923
- version: 1,
924
- scheduledPublishAt,
925
- ...overrides,
926
- };
927
- },
928
-
929
- /**
930
- * Create a soft-deleted content entry.
931
- */
932
- deleted(
933
- contentTypeId: Id<"contentTypes">,
934
- data: Record<string, unknown>,
935
- overrides: Partial<ContentEntryData> = {},
936
- ): ContentEntryData {
937
- entryCounter++;
938
- return {
939
- contentTypeId,
940
- slug: `deleted-${entryCounter}`,
941
- status: "draft",
942
- data,
943
- version: 1,
944
- deletedAt: Date.now(),
945
- ...overrides,
946
- };
947
- },
948
-
949
- /**
950
- * Create a localized content entry variant.
951
- */
952
- localized(
953
- contentTypeId: Id<"contentTypes">,
954
- primaryEntryId: Id<"contentEntries">,
955
- locale: string,
956
- data: Record<string, unknown>,
957
- overrides: Partial<ContentEntryData> = {},
958
- ): ContentEntryData {
959
- entryCounter++;
960
- return {
961
- contentTypeId,
962
- slug: `localized-${locale}-${entryCounter}`,
963
- status: "draft",
964
- data,
965
- locale,
966
- primaryEntryId,
967
- version: 1,
968
- ...overrides,
969
- };
970
- },
971
-
972
- /**
973
- * Create a blog post entry with typical data.
974
- */
975
- blogPost(
976
- contentTypeId: Id<"contentTypes">,
977
- overrides: Partial<ContentEntryData> = {},
978
- ): ContentEntryData {
979
- entryCounter++;
980
- return {
981
- contentTypeId,
982
- slug: `blog-post-${entryCounter}`,
983
- status: "draft",
984
- data: {
985
- title: `Test Blog Post ${entryCounter}`,
986
- content: `<p>This is the content of test blog post ${entryCounter}.</p>`,
987
- excerpt: `A brief excerpt for blog post ${entryCounter}.`,
988
- tags: ["tech", "tutorial"],
989
- },
990
- version: 1,
991
- searchText: `Test Blog Post ${entryCounter} content excerpt`,
992
- ...overrides,
993
- };
994
- },
995
-
996
- /**
997
- * Create a product entry with typical data.
998
- */
999
- product(
1000
- contentTypeId: Id<"contentTypes">,
1001
- overrides: Partial<ContentEntryData> = {},
1002
- ): ContentEntryData {
1003
- entryCounter++;
1004
- const price = Math.floor(Math.random() * 10000) / 100 + 9.99;
1005
- return {
1006
- contentTypeId,
1007
- slug: `product-${entryCounter}`,
1008
- status: "draft",
1009
- data: {
1010
- name: `Test Product ${entryCounter}`,
1011
- sku: `SKU-${entryCounter.toString().padStart(6, "0")}`,
1012
- description: `<p>Description for test product ${entryCounter}.</p>`,
1013
- price,
1014
- stock: Math.floor(Math.random() * 100),
1015
- status: "active",
1016
- },
1017
- version: 1,
1018
- searchText: `Test Product ${entryCounter}`,
1019
- ...overrides,
1020
- };
1021
- },
1022
-
1023
- /**
1024
- * Create multiple entries at once.
1025
- */
1026
- batch(
1027
- contentTypeId: Id<"contentTypes">,
1028
- count: number,
1029
- factory: (
1030
- contentTypeId: Id<"contentTypes">,
1031
- index: number,
1032
- ) => Partial<ContentEntryData> = () => ({}),
1033
- ): ContentEntryData[] {
1034
- return Array.from({ length: count }, (_, index) => {
1035
- const custom = factory(contentTypeId, index);
1036
- return contentEntryFactory.minimal(contentTypeId, custom);
1037
- });
1038
- },
1039
- };
1040
-
1041
- // =============================================================================
1042
- // Media Asset Factories
1043
- // =============================================================================
1044
-
1045
- let mediaCounter = 0;
1046
-
1047
- /**
1048
- * Factory functions for creating test media asset data.
1049
- * All factories return data suitable for inserting into the mediaAssets table.
1050
- *
1051
- * Note: You'll need to provide a valid storageId when inserting into the database.
1052
- */
1053
- export const mediaAssetFactory = {
1054
- /**
1055
- * Reset the internal counter used for generating unique filenames.
1056
- */
1057
- resetCounter(): void {
1058
- mediaCounter = 0;
1059
- },
1060
-
1061
- /**
1062
- * Create a minimal valid media asset.
1063
- */
1064
- minimal(
1065
- storageId: Id<"_storage">,
1066
- overrides: Partial<MediaAssetData> = {},
1067
- ): MediaAssetData {
1068
- mediaCounter++;
1069
- const name = `file-${mediaCounter}.bin`;
1070
- return {
1071
- kind: "asset",
1072
- storageId,
1073
- name,
1074
- path: `/${name}`,
1075
- mimeType: "application/octet-stream",
1076
- size: 1024,
1077
- ...overrides,
1078
- };
1079
- },
1080
-
1081
- /**
1082
- * Create an image asset.
1083
- */
1084
- image(
1085
- storageId: Id<"_storage">,
1086
- overrides: Partial<MediaAssetData> = {},
1087
- ): MediaAssetData {
1088
- mediaCounter++;
1089
- const name = `image-${mediaCounter}.jpg`;
1090
- return {
1091
- kind: "asset",
1092
- storageId,
1093
- name,
1094
- path: `/${name}`,
1095
- mimeType: "image/jpeg",
1096
- size: 102400,
1097
- width: 1920,
1098
- height: 1080,
1099
- title: `Test Image ${mediaCounter}`,
1100
- altText: `Alt text for test image ${mediaCounter}`,
1101
- searchText: `Test Image ${mediaCounter}`,
1102
- ...overrides,
1103
- };
1104
- },
1105
-
1106
- /**
1107
- * Create a PNG image asset.
1108
- */
1109
- png(
1110
- storageId: Id<"_storage">,
1111
- overrides: Partial<MediaAssetData> = {},
1112
- ): MediaAssetData {
1113
- mediaCounter++;
1114
- const name = `image-${mediaCounter}.png`;
1115
- return {
1116
- kind: "asset",
1117
- storageId,
1118
- name,
1119
- path: `/${name}`,
1120
- mimeType: "image/png",
1121
- size: 204800,
1122
- width: 800,
1123
- height: 600,
1124
- ...overrides,
1125
- };
1126
- },
1127
-
1128
- /**
1129
- * Create a video asset.
1130
- */
1131
- video(
1132
- storageId: Id<"_storage">,
1133
- overrides: Partial<MediaAssetData> = {},
1134
- ): MediaAssetData {
1135
- mediaCounter++;
1136
- const name = `video-${mediaCounter}.mp4`;
1137
- return {
1138
- kind: "asset",
1139
- storageId,
1140
- name,
1141
- path: `/${name}`,
1142
- mimeType: "video/mp4",
1143
- size: 10485760, // 10MB
1144
- width: 1920,
1145
- height: 1080,
1146
- duration: 120, // 2 minutes
1147
- title: `Test Video ${mediaCounter}`,
1148
- searchText: `Test Video ${mediaCounter}`,
1149
- ...overrides,
1150
- };
1151
- },
1152
-
1153
- /**
1154
- * Create an audio asset.
1155
- */
1156
- audio(
1157
- storageId: Id<"_storage">,
1158
- overrides: Partial<MediaAssetData> = {},
1159
- ): MediaAssetData {
1160
- mediaCounter++;
1161
- const name = `audio-${mediaCounter}.mp3`;
1162
- return {
1163
- kind: "asset",
1164
- storageId,
1165
- name,
1166
- path: `/${name}`,
1167
- mimeType: "audio/mpeg",
1168
- size: 5242880, // 5MB
1169
- duration: 180, // 3 minutes
1170
- title: `Test Audio ${mediaCounter}`,
1171
- searchText: `Test Audio ${mediaCounter}`,
1172
- ...overrides,
1173
- };
1174
- },
1175
-
1176
- /**
1177
- * Create a document asset (PDF).
1178
- */
1179
- document(
1180
- storageId: Id<"_storage">,
1181
- overrides: Partial<MediaAssetData> = {},
1182
- ): MediaAssetData {
1183
- mediaCounter++;
1184
- const name = `document-${mediaCounter}.pdf`;
1185
- return {
1186
- kind: "asset",
1187
- storageId,
1188
- name,
1189
- path: `/${name}`,
1190
- mimeType: "application/pdf",
1191
- size: 1048576, // 1MB
1192
- title: `Test Document ${mediaCounter}`,
1193
- searchText: `Test Document ${mediaCounter}`,
1194
- ...overrides,
1195
- };
1196
- },
1197
-
1198
- /**
1199
- * Create a soft-deleted media asset.
1200
- */
1201
- deleted(
1202
- storageId: Id<"_storage">,
1203
- overrides: Partial<MediaAssetData> = {},
1204
- ): MediaAssetData {
1205
- const asset = mediaAssetFactory.image(storageId, overrides);
1206
- return {
1207
- ...asset,
1208
- deletedAt: Date.now(),
1209
- };
1210
- },
1211
-
1212
- /**
1213
- * Create multiple assets at once.
1214
- */
1215
- batch(
1216
- storageIds: Id<"_storage">[],
1217
- factory: (
1218
- storageId: Id<"_storage">,
1219
- index: number,
1220
- ) => Partial<MediaAssetData> = () => ({}),
1221
- ): MediaAssetData[] {
1222
- return storageIds.map((storageId, index) => {
1223
- const custom = factory(storageId, index);
1224
- return mediaAssetFactory.minimal(storageId, custom);
1225
- });
1226
- },
1227
- };
1228
-
1229
- // =============================================================================
1230
- // Media Folder Factories
1231
- // =============================================================================
1232
-
1233
- let _folderCounter = 0;
1234
-
1235
- /**
1236
- * Factory functions for creating test media folder data.
1237
- */
1238
- export const mediaFolderFactory = {
1239
- /**
1240
- * Reset the internal counter used for generating unique folder names.
1241
- */
1242
- resetCounter(): void {
1243
- _folderCounter = 0;
1244
- },
1245
-
1246
- /**
1247
- * Create a root-level folder.
1248
- */
1249
- root(
1250
- name: string,
1251
- overrides: Partial<MediaFolderData> = {},
1252
- ): MediaFolderData {
1253
- return {
1254
- kind: "folder",
1255
- name,
1256
- path: `/${name}`,
1257
- ...overrides,
1258
- };
1259
- },
1260
-
1261
- /**
1262
- * Create a child folder.
1263
- */
1264
- child(
1265
- name: string,
1266
- parentId: Id<"mediaItems">,
1267
- parentPath: string,
1268
- overrides: Partial<MediaFolderData> = {},
1269
- ): MediaFolderData {
1270
- return {
1271
- kind: "folder",
1272
- name,
1273
- parentId,
1274
- path: `${parentPath}/${name}`,
1275
- ...overrides,
1276
- };
1277
- },
1278
-
1279
- /**
1280
- * Create a common folder structure for testing.
1281
- */
1282
- common(): {
1283
- images: MediaFolderData;
1284
- videos: MediaFolderData;
1285
- documents: MediaFolderData;
1286
- } {
1287
- return {
1288
- images: mediaFolderFactory.root("images"),
1289
- videos: mediaFolderFactory.root("videos"),
1290
- documents: mediaFolderFactory.root("documents"),
1291
- };
1292
- },
1293
- };
1294
-
1295
- // =============================================================================
1296
- // Assertion Utilities
1297
- // =============================================================================
1298
-
1299
- /**
1300
- * Assert that a value is a valid content type document.
1301
- * Throws an error if the assertion fails.
1302
- */
1303
- export function assertContentType(
1304
- value: unknown,
1305
- message?: string,
1306
- ): asserts value is ContentTypeData & { _id: string; _creationTime: number } {
1307
- if (!value || typeof value !== "object") {
1308
- throw new Error(message ?? "Expected a content type object");
1309
- }
1310
-
1311
- const obj = value as Record<string, unknown>;
1312
-
1313
- if (typeof obj.name !== "string") {
1314
- throw new Error(message ?? "Content type missing required 'name' field");
1315
- }
1316
-
1317
- if (typeof obj.displayName !== "string") {
1318
- throw new Error(
1319
- message ?? "Content type missing required 'displayName' field",
1320
- );
1321
- }
1322
-
1323
- if (!Array.isArray(obj.fields)) {
1324
- throw new Error(message ?? "Content type missing required 'fields' array");
1325
- }
1326
-
1327
- if (typeof obj.isActive !== "boolean") {
1328
- throw new Error(
1329
- message ?? "Content type missing required 'isActive' field",
1330
- );
1331
- }
1332
- }
1333
-
1334
- /**
1335
- * Assert that a value is a valid content entry document.
1336
- * Throws an error if the assertion fails.
1337
- */
1338
- export function assertContentEntry(
1339
- value: unknown,
1340
- message?: string,
1341
- ): asserts value is ContentEntryData & { _id: string; _creationTime: number } {
1342
- if (!value || typeof value !== "object") {
1343
- throw new Error(message ?? "Expected a content entry object");
1344
- }
1345
-
1346
- const obj = value as Record<string, unknown>;
1347
-
1348
- if (!obj.contentTypeId) {
1349
- throw new Error(
1350
- message ?? "Content entry missing required 'contentTypeId' field",
1351
- );
1352
- }
1353
-
1354
- if (typeof obj.slug !== "string") {
1355
- throw new Error(message ?? "Content entry missing required 'slug' field");
1356
- }
1357
-
1358
- if (typeof obj.status !== "string") {
1359
- throw new Error(message ?? "Content entry missing required 'status' field");
1360
- }
1361
-
1362
- const validStatuses = ["draft", "published", "archived", "scheduled"];
1363
- if (!validStatuses.includes(obj.status)) {
1364
- throw new Error(
1365
- message ?? `Content entry has invalid status '${obj.status}'`,
1366
- );
1367
- }
1368
-
1369
- if (typeof obj.version !== "number") {
1370
- throw new Error(
1371
- message ?? "Content entry missing required 'version' field",
1372
- );
1373
- }
1374
- }
1375
-
1376
- /**
1377
- * Assert that a value is a valid media asset document.
1378
- * Throws an error if the assertion fails.
1379
- */
1380
- export function assertMediaAsset(
1381
- value: unknown,
1382
- message?: string,
1383
- ): asserts value is MediaAssetData & { _id: string; _creationTime: number } {
1384
- if (!value || typeof value !== "object") {
1385
- throw new Error(message ?? "Expected a media asset object");
1386
- }
1387
-
1388
- const obj = value as Record<string, unknown>;
1389
-
1390
- if (obj.kind !== "asset") {
1391
- throw new Error(message ?? "Media asset missing required kind: 'asset'");
1392
- }
1393
-
1394
- if (!obj.storageId) {
1395
- throw new Error(
1396
- message ?? "Media asset missing required 'storageId' field",
1397
- );
1398
- }
1399
-
1400
- if (typeof obj.name !== "string") {
1401
- throw new Error(message ?? "Media asset missing required 'name' field");
1402
- }
1403
-
1404
- if (typeof obj.mimeType !== "string") {
1405
- throw new Error(message ?? "Media asset missing required 'mimeType' field");
1406
- }
1407
-
1408
- if (typeof obj.path !== "string") {
1409
- throw new Error(message ?? "Media asset missing required 'path' field");
1410
- }
1411
- }
1412
-
1413
- /**
1414
- * Assert that a content entry has a specific status.
1415
- */
1416
- export function assertStatus(
1417
- entry: { status: string },
1418
- expectedStatus: ContentStatus,
1419
- message?: string,
1420
- ): void {
1421
- if (entry.status !== expectedStatus) {
1422
- throw new Error(
1423
- message ??
1424
- `Expected entry status to be '${expectedStatus}', but got '${entry.status}'`,
1425
- );
1426
- }
1427
- }
1428
-
1429
- /**
1430
- * Assert that a content entry is published.
1431
- */
1432
- export function assertPublished(
1433
- entry: { status: string; lastPublishedAt?: number },
1434
- message?: string,
1435
- ): void {
1436
- assertStatus(entry, "published", message);
1437
- if (!entry.lastPublishedAt) {
1438
- throw new Error(
1439
- message ?? "Published entry should have 'lastPublishedAt' set",
1440
- );
1441
- }
1442
- }
1443
-
1444
- /**
1445
- * Assert that a document is soft-deleted.
1446
- */
1447
- export function assertDeleted(
1448
- doc: { deletedAt?: number },
1449
- message?: string,
1450
- ): void {
1451
- if (!doc.deletedAt) {
1452
- throw new Error(message ?? "Expected document to be soft-deleted");
1453
- }
1454
- }
1455
-
1456
- /**
1457
- * Assert that a document is not soft-deleted.
1458
- */
1459
- export function assertNotDeleted(
1460
- doc: { deletedAt?: number },
1461
- message?: string,
1462
- ): void {
1463
- if (doc.deletedAt) {
1464
- throw new Error(message ?? "Expected document to not be soft-deleted");
1465
- }
1466
- }
1467
-
1468
- /**
1469
- * Assert that a field definition has expected properties.
1470
- */
1471
- export function assertField(
1472
- fields: TestFieldDefinition[],
1473
- fieldName: string,
1474
- expectations: Partial<TestFieldDefinition>,
1475
- message?: string,
1476
- ): void {
1477
- const field = fields.find((f) => f.name === fieldName);
1478
-
1479
- if (!field) {
1480
- throw new Error(message ?? `Field '${fieldName}' not found`);
1481
- }
1482
-
1483
- for (const [key, expectedValue] of Object.entries(expectations)) {
1484
- const actualValue = field[key as keyof TestFieldDefinition];
1485
- if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
1486
- throw new Error(
1487
- message ??
1488
- `Field '${fieldName}' expected ${key} to be ${JSON.stringify(
1489
- expectedValue,
1490
- )}, but got ${JSON.stringify(actualValue)}`,
1491
- );
1492
- }
1493
- }
1494
- }
1495
-
1496
- // =============================================================================
1497
- // Test Data Helpers
1498
- // =============================================================================
1499
-
1500
- /**
1501
- * Generate a unique slug for testing.
1502
- */
1503
- export function uniqueSlug(prefix: string = "test"): string {
1504
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1505
- }
1506
-
1507
- /**
1508
- * Generate a unique name for testing.
1509
- */
1510
- export function uniqueName(prefix: string = "test"): string {
1511
- return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1512
- }
1513
-
1514
- /**
1515
- * Create a timestamp in the past.
1516
- */
1517
- export function pastTimestamp(daysAgo: number): number {
1518
- return Date.now() - daysAgo * 24 * 60 * 60 * 1000;
1519
- }
1520
-
1521
- /**
1522
- * Create a timestamp in the future.
1523
- */
1524
- export function futureTimestamp(daysFromNow: number): number {
1525
- return Date.now() + daysFromNow * 24 * 60 * 60 * 1000;
1526
- }
1527
-
1528
- // =============================================================================
1529
- // Exports
1530
- // =============================================================================
1531
-
1532
- /**
1533
- * Export schema and modules for advanced testing scenarios.
1534
- */
1535
- export { schema, modules };
1536
-
1537
- /**
1538
- * Re-export type constants for convenience.
1539
- */
1540
- export {
1541
- fieldTypes,
1542
- contentStatuses,
1543
- mediaTypes,
1544
- variantTypes,
1545
- variantStatuses,
1546
- variantFormats,
1547
- } from "./component/schema.js";
1548
-
1549
- /**
1550
- * Default export for convenient importing.
1551
- */
1552
- export default {
1553
- // Registration
1554
- register,
1555
- schema,
1556
- modules,
1557
-
1558
- // Factories
1559
- fieldFactory,
1560
- contentTypeFactory,
1561
- contentEntryFactory,
1562
- mediaAssetFactory,
1563
- mediaFolderFactory,
1564
-
1565
- // Assertions
1566
- assertContentType,
1567
- assertContentEntry,
1568
- assertMediaAsset,
1569
- assertStatus,
1570
- assertPublished,
1571
- assertDeleted,
1572
- assertNotDeleted,
1573
- assertField,
1574
-
1575
- // Helpers
1576
- uniqueSlug,
1577
- uniqueName,
1578
- pastTimestamp,
1579
- futureTimestamp,
1580
- };