convex-cms 0.0.1

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 (379) hide show
  1. package/dist/cli/commands/admin.d.ts +16 -0
  2. package/dist/cli/commands/admin.d.ts.map +1 -0
  3. package/dist/cli/commands/admin.js +88 -0
  4. package/dist/cli/commands/admin.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +18 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
  10. package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
  11. package/dist/cli/utils/detectConvexUrl.js +48 -0
  12. package/dist/cli/utils/detectConvexUrl.js.map +1 -0
  13. package/dist/cli/utils/openBrowser.d.ts +7 -0
  14. package/dist/cli/utils/openBrowser.d.ts.map +1 -0
  15. package/dist/cli/utils/openBrowser.js +17 -0
  16. package/dist/cli/utils/openBrowser.js.map +1 -0
  17. package/dist/client/admin-config.d.ts +126 -0
  18. package/dist/client/admin-config.d.ts.map +1 -0
  19. package/dist/client/admin-config.js +117 -0
  20. package/dist/client/admin-config.js.map +1 -0
  21. package/dist/client/adminApi.d.ts +2273 -0
  22. package/dist/client/adminApi.d.ts.map +1 -0
  23. package/dist/client/adminApi.js +716 -0
  24. package/dist/client/adminApi.js.map +1 -0
  25. package/dist/client/agentTools.d.ts +933 -0
  26. package/dist/client/agentTools.d.ts.map +1 -0
  27. package/dist/client/agentTools.js +1004 -0
  28. package/dist/client/agentTools.js.map +1 -0
  29. package/dist/client/argTypes.d.ts +212 -0
  30. package/dist/client/argTypes.d.ts.map +1 -0
  31. package/dist/client/argTypes.js +5 -0
  32. package/dist/client/argTypes.js.map +1 -0
  33. package/dist/client/field-types.d.ts +55 -0
  34. package/dist/client/field-types.d.ts.map +1 -0
  35. package/dist/client/field-types.js +152 -0
  36. package/dist/client/field-types.js.map +1 -0
  37. package/dist/client/index.d.ts +189 -0
  38. package/dist/client/index.d.ts.map +1 -0
  39. package/dist/client/index.js +668 -0
  40. package/dist/client/index.js.map +1 -0
  41. package/dist/client/queryBuilder.d.ts +765 -0
  42. package/dist/client/queryBuilder.d.ts.map +1 -0
  43. package/dist/client/queryBuilder.js +970 -0
  44. package/dist/client/queryBuilder.js.map +1 -0
  45. package/dist/client/schema/codegen.d.ts +128 -0
  46. package/dist/client/schema/codegen.d.ts.map +1 -0
  47. package/dist/client/schema/codegen.js +318 -0
  48. package/dist/client/schema/codegen.js.map +1 -0
  49. package/dist/client/schema/defineContentType.d.ts +221 -0
  50. package/dist/client/schema/defineContentType.d.ts.map +1 -0
  51. package/dist/client/schema/defineContentType.js +380 -0
  52. package/dist/client/schema/defineContentType.js.map +1 -0
  53. package/dist/client/schema/index.d.ts +85 -0
  54. package/dist/client/schema/index.d.ts.map +1 -0
  55. package/dist/client/schema/index.js +92 -0
  56. package/dist/client/schema/index.js.map +1 -0
  57. package/dist/client/schema/schemaDrift.d.ts +199 -0
  58. package/dist/client/schema/schemaDrift.d.ts.map +1 -0
  59. package/dist/client/schema/schemaDrift.js +340 -0
  60. package/dist/client/schema/schemaDrift.js.map +1 -0
  61. package/dist/client/schema/typedClient.d.ts +401 -0
  62. package/dist/client/schema/typedClient.d.ts.map +1 -0
  63. package/dist/client/schema/typedClient.js +269 -0
  64. package/dist/client/schema/typedClient.js.map +1 -0
  65. package/dist/client/schema/types.d.ts +477 -0
  66. package/dist/client/schema/types.d.ts.map +1 -0
  67. package/dist/client/schema/types.js +39 -0
  68. package/dist/client/schema/types.js.map +1 -0
  69. package/dist/client/types.d.ts +449 -0
  70. package/dist/client/types.d.ts.map +1 -0
  71. package/dist/client/types.js +149 -0
  72. package/dist/client/types.js.map +1 -0
  73. package/dist/client/workflows.d.ts +51 -0
  74. package/dist/client/workflows.d.ts.map +1 -0
  75. package/dist/client/workflows.js +103 -0
  76. package/dist/client/workflows.js.map +1 -0
  77. package/dist/client/wrapper.d.ts +2198 -0
  78. package/dist/client/wrapper.d.ts.map +1 -0
  79. package/dist/client/wrapper.js +2651 -0
  80. package/dist/client/wrapper.js.map +1 -0
  81. package/dist/component/_generated/api.d.ts +124 -0
  82. package/dist/component/_generated/api.d.ts.map +1 -0
  83. package/dist/component/_generated/api.js +31 -0
  84. package/dist/component/_generated/api.js.map +1 -0
  85. package/dist/component/_generated/component.d.ts +4321 -0
  86. package/dist/component/_generated/component.d.ts.map +1 -0
  87. package/dist/component/_generated/component.js +11 -0
  88. package/dist/component/_generated/component.js.map +1 -0
  89. package/dist/component/_generated/dataModel.d.ts +46 -0
  90. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  91. package/dist/component/_generated/dataModel.js +11 -0
  92. package/dist/component/_generated/dataModel.js.map +1 -0
  93. package/dist/component/_generated/server.d.ts +121 -0
  94. package/dist/component/_generated/server.d.ts.map +1 -0
  95. package/dist/component/_generated/server.js +78 -0
  96. package/dist/component/_generated/server.js.map +1 -0
  97. package/dist/component/auditLog.d.ts +410 -0
  98. package/dist/component/auditLog.d.ts.map +1 -0
  99. package/dist/component/auditLog.js +607 -0
  100. package/dist/component/auditLog.js.map +1 -0
  101. package/dist/component/authorization.d.ts +323 -0
  102. package/dist/component/authorization.d.ts.map +1 -0
  103. package/dist/component/authorization.js +464 -0
  104. package/dist/component/authorization.js.map +1 -0
  105. package/dist/component/authorizationHooks.d.ts +184 -0
  106. package/dist/component/authorizationHooks.d.ts.map +1 -0
  107. package/dist/component/authorizationHooks.js +521 -0
  108. package/dist/component/authorizationHooks.js.map +1 -0
  109. package/dist/component/bulkOperations.d.ts +200 -0
  110. package/dist/component/bulkOperations.d.ts.map +1 -0
  111. package/dist/component/bulkOperations.js +568 -0
  112. package/dist/component/bulkOperations.js.map +1 -0
  113. package/dist/component/contentEntries.d.ts +719 -0
  114. package/dist/component/contentEntries.d.ts.map +1 -0
  115. package/dist/component/contentEntries.js +1617 -0
  116. package/dist/component/contentEntries.js.map +1 -0
  117. package/dist/component/contentEntryMutations.d.ts +505 -0
  118. package/dist/component/contentEntryMutations.d.ts.map +1 -0
  119. package/dist/component/contentEntryMutations.js +1009 -0
  120. package/dist/component/contentEntryMutations.js.map +1 -0
  121. package/dist/component/contentEntryValidation.d.ts +115 -0
  122. package/dist/component/contentEntryValidation.d.ts.map +1 -0
  123. package/dist/component/contentEntryValidation.js +546 -0
  124. package/dist/component/contentEntryValidation.js.map +1 -0
  125. package/dist/component/contentLock.d.ts +328 -0
  126. package/dist/component/contentLock.d.ts.map +1 -0
  127. package/dist/component/contentLock.js +471 -0
  128. package/dist/component/contentLock.js.map +1 -0
  129. package/dist/component/contentTypeMigration.d.ts +411 -0
  130. package/dist/component/contentTypeMigration.d.ts.map +1 -0
  131. package/dist/component/contentTypeMigration.js +805 -0
  132. package/dist/component/contentTypeMigration.js.map +1 -0
  133. package/dist/component/contentTypeMutations.d.ts +975 -0
  134. package/dist/component/contentTypeMutations.d.ts.map +1 -0
  135. package/dist/component/contentTypeMutations.js +768 -0
  136. package/dist/component/contentTypeMutations.js.map +1 -0
  137. package/dist/component/contentTypes.d.ts +538 -0
  138. package/dist/component/contentTypes.d.ts.map +1 -0
  139. package/dist/component/contentTypes.js +304 -0
  140. package/dist/component/contentTypes.js.map +1 -0
  141. package/dist/component/convex.config.d.ts +42 -0
  142. package/dist/component/convex.config.d.ts.map +1 -0
  143. package/dist/component/convex.config.js +43 -0
  144. package/dist/component/convex.config.js.map +1 -0
  145. package/dist/component/documentTypes.d.ts +186 -0
  146. package/dist/component/documentTypes.d.ts.map +1 -0
  147. package/dist/component/documentTypes.js +23 -0
  148. package/dist/component/documentTypes.js.map +1 -0
  149. package/dist/component/eventEmitter.d.ts +281 -0
  150. package/dist/component/eventEmitter.d.ts.map +1 -0
  151. package/dist/component/eventEmitter.js +300 -0
  152. package/dist/component/eventEmitter.js.map +1 -0
  153. package/dist/component/exportImport.d.ts +1120 -0
  154. package/dist/component/exportImport.d.ts.map +1 -0
  155. package/dist/component/exportImport.js +931 -0
  156. package/dist/component/exportImport.js.map +1 -0
  157. package/dist/component/index.d.ts +28 -0
  158. package/dist/component/index.d.ts.map +1 -0
  159. package/dist/component/index.js +142 -0
  160. package/dist/component/index.js.map +1 -0
  161. package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
  162. package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
  163. package/dist/component/lib/deepReferenceResolver.js +601 -0
  164. package/dist/component/lib/deepReferenceResolver.js.map +1 -0
  165. package/dist/component/lib/errors.d.ts +306 -0
  166. package/dist/component/lib/errors.d.ts.map +1 -0
  167. package/dist/component/lib/errors.js +407 -0
  168. package/dist/component/lib/errors.js.map +1 -0
  169. package/dist/component/lib/index.d.ts +10 -0
  170. package/dist/component/lib/index.d.ts.map +1 -0
  171. package/dist/component/lib/index.js +33 -0
  172. package/dist/component/lib/index.js.map +1 -0
  173. package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
  174. package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
  175. package/dist/component/lib/mediaReferenceResolver.js +326 -0
  176. package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
  177. package/dist/component/lib/metadataExtractor.d.ts +245 -0
  178. package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
  179. package/dist/component/lib/metadataExtractor.js +548 -0
  180. package/dist/component/lib/metadataExtractor.js.map +1 -0
  181. package/dist/component/lib/mutationAuth.d.ts +95 -0
  182. package/dist/component/lib/mutationAuth.d.ts.map +1 -0
  183. package/dist/component/lib/mutationAuth.js +146 -0
  184. package/dist/component/lib/mutationAuth.js.map +1 -0
  185. package/dist/component/lib/queries.d.ts +17 -0
  186. package/dist/component/lib/queries.d.ts.map +1 -0
  187. package/dist/component/lib/queries.js +49 -0
  188. package/dist/component/lib/queries.js.map +1 -0
  189. package/dist/component/lib/ragContentChunker.d.ts +423 -0
  190. package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
  191. package/dist/component/lib/ragContentChunker.js +897 -0
  192. package/dist/component/lib/ragContentChunker.js.map +1 -0
  193. package/dist/component/lib/referenceResolver.d.ts +175 -0
  194. package/dist/component/lib/referenceResolver.d.ts.map +1 -0
  195. package/dist/component/lib/referenceResolver.js +293 -0
  196. package/dist/component/lib/referenceResolver.js.map +1 -0
  197. package/dist/component/lib/slugGenerator.d.ts +71 -0
  198. package/dist/component/lib/slugGenerator.d.ts.map +1 -0
  199. package/dist/component/lib/slugGenerator.js +207 -0
  200. package/dist/component/lib/slugGenerator.js.map +1 -0
  201. package/dist/component/lib/slugUniqueness.d.ts +131 -0
  202. package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
  203. package/dist/component/lib/slugUniqueness.js +229 -0
  204. package/dist/component/lib/slugUniqueness.js.map +1 -0
  205. package/dist/component/lib/softDelete.d.ts +18 -0
  206. package/dist/component/lib/softDelete.d.ts.map +1 -0
  207. package/dist/component/lib/softDelete.js +29 -0
  208. package/dist/component/lib/softDelete.js.map +1 -0
  209. package/dist/component/localeFallbackChain.d.ts +410 -0
  210. package/dist/component/localeFallbackChain.d.ts.map +1 -0
  211. package/dist/component/localeFallbackChain.js +467 -0
  212. package/dist/component/localeFallbackChain.js.map +1 -0
  213. package/dist/component/localeFields.d.ts +508 -0
  214. package/dist/component/localeFields.d.ts.map +1 -0
  215. package/dist/component/localeFields.js +592 -0
  216. package/dist/component/localeFields.js.map +1 -0
  217. package/dist/component/mediaAssetMutations.d.ts +235 -0
  218. package/dist/component/mediaAssetMutations.d.ts.map +1 -0
  219. package/dist/component/mediaAssetMutations.js +558 -0
  220. package/dist/component/mediaAssetMutations.js.map +1 -0
  221. package/dist/component/mediaAssets.d.ts +168 -0
  222. package/dist/component/mediaAssets.d.ts.map +1 -0
  223. package/dist/component/mediaAssets.js +618 -0
  224. package/dist/component/mediaAssets.js.map +1 -0
  225. package/dist/component/mediaFolderMutations.d.ts +642 -0
  226. package/dist/component/mediaFolderMutations.d.ts.map +1 -0
  227. package/dist/component/mediaFolderMutations.js +849 -0
  228. package/dist/component/mediaFolderMutations.js.map +1 -0
  229. package/dist/component/mediaUploadMutations.d.ts +136 -0
  230. package/dist/component/mediaUploadMutations.d.ts.map +1 -0
  231. package/dist/component/mediaUploadMutations.js +205 -0
  232. package/dist/component/mediaUploadMutations.js.map +1 -0
  233. package/dist/component/mediaVariantMutations.d.ts +468 -0
  234. package/dist/component/mediaVariantMutations.d.ts.map +1 -0
  235. package/dist/component/mediaVariantMutations.js +737 -0
  236. package/dist/component/mediaVariantMutations.js.map +1 -0
  237. package/dist/component/mediaVariants.d.ts +525 -0
  238. package/dist/component/mediaVariants.d.ts.map +1 -0
  239. package/dist/component/mediaVariants.js +661 -0
  240. package/dist/component/mediaVariants.js.map +1 -0
  241. package/dist/component/ragContentIndexer.d.ts +595 -0
  242. package/dist/component/ragContentIndexer.d.ts.map +1 -0
  243. package/dist/component/ragContentIndexer.js +794 -0
  244. package/dist/component/ragContentIndexer.js.map +1 -0
  245. package/dist/component/rateLimitHooks.d.ts +266 -0
  246. package/dist/component/rateLimitHooks.d.ts.map +1 -0
  247. package/dist/component/rateLimitHooks.js +412 -0
  248. package/dist/component/rateLimitHooks.js.map +1 -0
  249. package/dist/component/roles.d.ts +649 -0
  250. package/dist/component/roles.d.ts.map +1 -0
  251. package/dist/component/roles.js +884 -0
  252. package/dist/component/roles.js.map +1 -0
  253. package/dist/component/scheduledPublish.d.ts +182 -0
  254. package/dist/component/scheduledPublish.d.ts.map +1 -0
  255. package/dist/component/scheduledPublish.js +304 -0
  256. package/dist/component/scheduledPublish.js.map +1 -0
  257. package/dist/component/schema.d.ts +4114 -0
  258. package/dist/component/schema.d.ts.map +1 -0
  259. package/dist/component/schema.js +469 -0
  260. package/dist/component/schema.js.map +1 -0
  261. package/dist/component/taxonomies.d.ts +476 -0
  262. package/dist/component/taxonomies.d.ts.map +1 -0
  263. package/dist/component/taxonomies.js +785 -0
  264. package/dist/component/taxonomies.js.map +1 -0
  265. package/dist/component/taxonomyMutations.d.ts +206 -0
  266. package/dist/component/taxonomyMutations.d.ts.map +1 -0
  267. package/dist/component/taxonomyMutations.js +1001 -0
  268. package/dist/component/taxonomyMutations.js.map +1 -0
  269. package/dist/component/trash.d.ts +265 -0
  270. package/dist/component/trash.d.ts.map +1 -0
  271. package/dist/component/trash.js +621 -0
  272. package/dist/component/trash.js.map +1 -0
  273. package/dist/component/types.d.ts +4 -0
  274. package/dist/component/types.d.ts.map +1 -0
  275. package/dist/component/types.js +2 -0
  276. package/dist/component/types.js.map +1 -0
  277. package/dist/component/userContext.d.ts +508 -0
  278. package/dist/component/userContext.d.ts.map +1 -0
  279. package/dist/component/userContext.js +615 -0
  280. package/dist/component/userContext.js.map +1 -0
  281. package/dist/component/validation.d.ts +387 -0
  282. package/dist/component/validation.d.ts.map +1 -0
  283. package/dist/component/validation.js +1052 -0
  284. package/dist/component/validation.js.map +1 -0
  285. package/dist/component/validators.d.ts +4645 -0
  286. package/dist/component/validators.d.ts.map +1 -0
  287. package/dist/component/validators.js +641 -0
  288. package/dist/component/validators.js.map +1 -0
  289. package/dist/component/versionMutations.d.ts +216 -0
  290. package/dist/component/versionMutations.d.ts.map +1 -0
  291. package/dist/component/versionMutations.js +321 -0
  292. package/dist/component/versionMutations.js.map +1 -0
  293. package/dist/component/webhookTrigger.d.ts +770 -0
  294. package/dist/component/webhookTrigger.d.ts.map +1 -0
  295. package/dist/component/webhookTrigger.js +1413 -0
  296. package/dist/component/webhookTrigger.js.map +1 -0
  297. package/dist/react/index.d.ts +316 -0
  298. package/dist/react/index.d.ts.map +1 -0
  299. package/dist/react/index.js +558 -0
  300. package/dist/react/index.js.map +1 -0
  301. package/dist/test.d.ts +2230 -0
  302. package/dist/test.d.ts.map +1 -0
  303. package/dist/test.js +1107 -0
  304. package/dist/test.js.map +1 -0
  305. package/package.json +95 -0
  306. package/src/cli/commands/admin.ts +104 -0
  307. package/src/cli/index.ts +21 -0
  308. package/src/cli/utils/detectConvexUrl.ts +54 -0
  309. package/src/cli/utils/openBrowser.ts +16 -0
  310. package/src/client/admin-config.ts +138 -0
  311. package/src/client/adminApi.ts +942 -0
  312. package/src/client/agentTools.ts +1311 -0
  313. package/src/client/argTypes.ts +316 -0
  314. package/src/client/field-types.ts +187 -0
  315. package/src/client/index.ts +1301 -0
  316. package/src/client/queryBuilder.ts +1100 -0
  317. package/src/client/schema/codegen.ts +500 -0
  318. package/src/client/schema/defineContentType.ts +501 -0
  319. package/src/client/schema/index.ts +169 -0
  320. package/src/client/schema/schemaDrift.ts +574 -0
  321. package/src/client/schema/typedClient.ts +688 -0
  322. package/src/client/schema/types.ts +666 -0
  323. package/src/client/types.ts +723 -0
  324. package/src/client/workflows.ts +141 -0
  325. package/src/client/wrapper.ts +4304 -0
  326. package/src/component/_generated/api.ts +140 -0
  327. package/src/component/_generated/component.ts +5029 -0
  328. package/src/component/_generated/dataModel.ts +60 -0
  329. package/src/component/_generated/server.ts +156 -0
  330. package/src/component/authorization.ts +647 -0
  331. package/src/component/authorizationHooks.ts +668 -0
  332. package/src/component/bulkOperations.ts +687 -0
  333. package/src/component/contentEntries.ts +1976 -0
  334. package/src/component/contentEntryMutations.ts +1223 -0
  335. package/src/component/contentEntryValidation.ts +707 -0
  336. package/src/component/contentLock.ts +550 -0
  337. package/src/component/contentTypeMigration.ts +1064 -0
  338. package/src/component/contentTypeMutations.ts +969 -0
  339. package/src/component/contentTypes.ts +346 -0
  340. package/src/component/convex.config.ts +44 -0
  341. package/src/component/documentTypes.ts +240 -0
  342. package/src/component/eventEmitter.ts +485 -0
  343. package/src/component/exportImport.ts +1169 -0
  344. package/src/component/index.ts +491 -0
  345. package/src/component/lib/deepReferenceResolver.ts +999 -0
  346. package/src/component/lib/errors.ts +816 -0
  347. package/src/component/lib/index.ts +145 -0
  348. package/src/component/lib/mediaReferenceResolver.ts +495 -0
  349. package/src/component/lib/metadataExtractor.ts +792 -0
  350. package/src/component/lib/mutationAuth.ts +199 -0
  351. package/src/component/lib/queries.ts +79 -0
  352. package/src/component/lib/ragContentChunker.ts +1371 -0
  353. package/src/component/lib/referenceResolver.ts +430 -0
  354. package/src/component/lib/slugGenerator.ts +262 -0
  355. package/src/component/lib/slugUniqueness.ts +333 -0
  356. package/src/component/lib/softDelete.ts +44 -0
  357. package/src/component/localeFallbackChain.ts +673 -0
  358. package/src/component/localeFields.ts +896 -0
  359. package/src/component/mediaAssetMutations.ts +725 -0
  360. package/src/component/mediaAssets.ts +932 -0
  361. package/src/component/mediaFolderMutations.ts +1046 -0
  362. package/src/component/mediaUploadMutations.ts +224 -0
  363. package/src/component/mediaVariantMutations.ts +900 -0
  364. package/src/component/mediaVariants.ts +793 -0
  365. package/src/component/ragContentIndexer.ts +1067 -0
  366. package/src/component/rateLimitHooks.ts +572 -0
  367. package/src/component/roles.ts +1360 -0
  368. package/src/component/scheduledPublish.ts +358 -0
  369. package/src/component/schema.ts +617 -0
  370. package/src/component/taxonomies.ts +949 -0
  371. package/src/component/taxonomyMutations.ts +1210 -0
  372. package/src/component/trash.ts +724 -0
  373. package/src/component/userContext.ts +898 -0
  374. package/src/component/validation.ts +1388 -0
  375. package/src/component/validators.ts +949 -0
  376. package/src/component/versionMutations.ts +392 -0
  377. package/src/component/webhookTrigger.ts +1922 -0
  378. package/src/react/index.ts +898 -0
  379. package/src/test.ts +1580 -0
@@ -0,0 +1,1388 @@
1
+ /**
2
+ * Content Validation Functions
3
+ *
4
+ * Runtime validation helpers that check content data against field configurations.
5
+ * These complement the Convex validators by providing detailed validation logic
6
+ * and human-readable error messages.
7
+ *
8
+ * Supports both plain field values and localized field values (LocalizedFieldValue<T>)
9
+ * for fields marked as `localized: true` in their field definition.
10
+ */
11
+ import { FieldType } from "./validators.js";
12
+ import {
13
+ isLocalizedFieldValue,
14
+ type LocalizedFieldValue,
15
+ } from "./localeFields.js";
16
+
17
+ /**
18
+ * Field options structure (matches schema.ts fieldOptionsValidator)
19
+ */
20
+ export interface FieldOptions {
21
+ // Text fields
22
+ minLength?: number;
23
+ maxLength?: number;
24
+ pattern?: string;
25
+
26
+ // Number fields
27
+ min?: number;
28
+ max?: number;
29
+ step?: number;
30
+ precision?: number;
31
+
32
+ // Reference fields
33
+ allowedContentTypes?: string[];
34
+ multiple?: boolean;
35
+ /** Minimum number of references required (only applies when multiple is true) */
36
+ minItems?: number;
37
+
38
+ // Media fields
39
+ allowedMimeTypes?: string[];
40
+ maxFileSize?: number;
41
+
42
+ // Select fields
43
+ options?: Array<{ value: string; label: string }>;
44
+
45
+ // Rich text fields
46
+ allowedBlocks?: string[];
47
+ allowedMarks?: string[];
48
+
49
+ // Tag fields
50
+ taxonomyId?: string;
51
+ allowCreate?: boolean;
52
+ maxTags?: number;
53
+ minTags?: number;
54
+
55
+ // Category fields
56
+ allowMultiple?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Field definition structure (matches schema.ts fieldDefinitionValidator)
61
+ */
62
+ export interface FieldDefinition {
63
+ name: string;
64
+ label: string;
65
+ type: FieldType;
66
+ required: boolean;
67
+ searchable?: boolean;
68
+ localized?: boolean;
69
+ description?: string;
70
+ defaultValue?: unknown;
71
+ options?: FieldOptions;
72
+ }
73
+
74
+ /**
75
+ * Content type schema structure
76
+ */
77
+ export interface ContentTypeSchema {
78
+ name: string;
79
+ displayName: string;
80
+ description?: string;
81
+ fields: FieldDefinition[];
82
+ titleField?: string;
83
+ slugField?: string;
84
+ singleton?: boolean;
85
+ }
86
+
87
+ /**
88
+ * Content data is a record of field names to their values
89
+ */
90
+ export type ContentData = Record<string, unknown>;
91
+
92
+ // =============================================================================
93
+ // Validation Result Types
94
+ // =============================================================================
95
+
96
+ export type ValidationError = {
97
+ field: string;
98
+ message: string;
99
+ code: ValidationErrorCode;
100
+ };
101
+
102
+ export type ValidationErrorCode =
103
+ | "REQUIRED"
104
+ | "MIN_LENGTH"
105
+ | "MAX_LENGTH"
106
+ | "PATTERN_MISMATCH"
107
+ | "MIN_VALUE"
108
+ | "MAX_VALUE"
109
+ | "NOT_INTEGER"
110
+ | "MIN_DATE"
111
+ | "MAX_DATE"
112
+ | "INVALID_TYPE"
113
+ | "MIN_ITEMS"
114
+ | "MAX_ITEMS"
115
+ | "INVALID_CONTENT_TYPE"
116
+ | "UNKNOWN_FIELD"
117
+ | "INVALID_MIME_TYPE"
118
+ | "FILE_TOO_LARGE"
119
+ | "INVALID_LOCALIZED_STRUCTURE"
120
+ | "MISSING_LOCALE";
121
+
122
+ export type ValidationResult =
123
+ | { valid: true; errors: [] }
124
+ | { valid: false; errors: ValidationError[] };
125
+
126
+ // =============================================================================
127
+ // Field Value Validators
128
+ // =============================================================================
129
+
130
+ /**
131
+ * Validate a text field value against its configuration
132
+ */
133
+ export function validateTextField(
134
+ value: unknown,
135
+ fieldDef: FieldDefinition
136
+ ): ValidationError[] {
137
+ const errors: ValidationError[] = [];
138
+ const { name, required, options } = fieldDef;
139
+
140
+ // Check required
141
+ if (required && (value === null || value === undefined || value === "")) {
142
+ errors.push({
143
+ field: name,
144
+ message: `${name} is required`,
145
+ code: "REQUIRED",
146
+ });
147
+ return errors;
148
+ }
149
+
150
+ // Skip further validation if value is empty and not required
151
+ if (value === null || value === undefined || value === "") {
152
+ return errors;
153
+ }
154
+
155
+ // Type check
156
+ if (typeof value !== "string") {
157
+ errors.push({
158
+ field: name,
159
+ message: `${name} must be a string`,
160
+ code: "INVALID_TYPE",
161
+ });
162
+ return errors;
163
+ }
164
+
165
+ // Min length
166
+ if (options?.minLength !== undefined && value.length < options.minLength) {
167
+ errors.push({
168
+ field: name,
169
+ message: `${name} must be at least ${options.minLength} characters`,
170
+ code: "MIN_LENGTH",
171
+ });
172
+ }
173
+
174
+ // Max length
175
+ if (options?.maxLength !== undefined && value.length > options.maxLength) {
176
+ errors.push({
177
+ field: name,
178
+ message: `${name} must be at most ${options.maxLength} characters`,
179
+ code: "MAX_LENGTH",
180
+ });
181
+ }
182
+
183
+ // Pattern
184
+ if (options?.pattern !== undefined) {
185
+ const regex = new RegExp(options.pattern);
186
+ if (!regex.test(value)) {
187
+ errors.push({
188
+ field: name,
189
+ message: `${name} does not match the required pattern`,
190
+ code: "PATTERN_MISMATCH",
191
+ });
192
+ }
193
+ }
194
+
195
+ return errors;
196
+ }
197
+
198
+ /**
199
+ * Validate a rich text field value against its configuration
200
+ */
201
+ export function validateRichTextField(
202
+ value: unknown,
203
+ fieldDef: FieldDefinition
204
+ ): ValidationError[] {
205
+ const errors: ValidationError[] = [];
206
+ const { name, required, options } = fieldDef;
207
+
208
+ // Check required
209
+ if (required && (value === null || value === undefined || value === "")) {
210
+ errors.push({
211
+ field: name,
212
+ message: `${name} is required`,
213
+ code: "REQUIRED",
214
+ });
215
+ return errors;
216
+ }
217
+
218
+ // Skip further validation if value is empty and not required
219
+ if (value === null || value === undefined || value === "") {
220
+ return errors;
221
+ }
222
+
223
+ // Type check
224
+ if (typeof value !== "string") {
225
+ errors.push({
226
+ field: name,
227
+ message: `${name} must be a string`,
228
+ code: "INVALID_TYPE",
229
+ });
230
+ return errors;
231
+ }
232
+
233
+ // Max length (strip HTML tags before counting)
234
+ if (options?.maxLength !== undefined) {
235
+ const plainText = value.replace(/<[^>]*>/g, "");
236
+ if (plainText.length > options.maxLength) {
237
+ errors.push({
238
+ field: name,
239
+ message: `${name} content must be at most ${options.maxLength} characters`,
240
+ code: "MAX_LENGTH",
241
+ });
242
+ }
243
+ }
244
+
245
+ return errors;
246
+ }
247
+
248
+ /**
249
+ * Validate a number field value against its configuration
250
+ */
251
+ export function validateNumberField(
252
+ value: unknown,
253
+ fieldDef: FieldDefinition
254
+ ): ValidationError[] {
255
+ const errors: ValidationError[] = [];
256
+ const { name, required, options } = fieldDef;
257
+
258
+ // Check required
259
+ if (required && (value === null || value === undefined)) {
260
+ errors.push({
261
+ field: name,
262
+ message: `${name} is required`,
263
+ code: "REQUIRED",
264
+ });
265
+ return errors;
266
+ }
267
+
268
+ // Skip further validation if value is empty and not required
269
+ if (value === null || value === undefined) {
270
+ return errors;
271
+ }
272
+
273
+ // Type check
274
+ if (typeof value !== "number" || isNaN(value)) {
275
+ errors.push({
276
+ field: name,
277
+ message: `${name} must be a number`,
278
+ code: "INVALID_TYPE",
279
+ });
280
+ return errors;
281
+ }
282
+
283
+ // Precision check (step = 1 means integer)
284
+ if (options?.precision === 0 && !Number.isInteger(value)) {
285
+ errors.push({
286
+ field: name,
287
+ message: `${name} must be a whole number`,
288
+ code: "NOT_INTEGER",
289
+ });
290
+ }
291
+
292
+ // Min value
293
+ if (options?.min !== undefined && value < options.min) {
294
+ errors.push({
295
+ field: name,
296
+ message: `${name} must be at least ${options.min}`,
297
+ code: "MIN_VALUE",
298
+ });
299
+ }
300
+
301
+ // Max value
302
+ if (options?.max !== undefined && value > options.max) {
303
+ errors.push({
304
+ field: name,
305
+ message: `${name} must be at most ${options.max}`,
306
+ code: "MAX_VALUE",
307
+ });
308
+ }
309
+
310
+ return errors;
311
+ }
312
+
313
+ /**
314
+ * Validate a boolean field value against its configuration
315
+ */
316
+ export function validateBooleanField(
317
+ value: unknown,
318
+ fieldDef: FieldDefinition
319
+ ): ValidationError[] {
320
+ const errors: ValidationError[] = [];
321
+ const { name, required } = fieldDef;
322
+
323
+ // Check required
324
+ if (required && (value === null || value === undefined)) {
325
+ errors.push({
326
+ field: name,
327
+ message: `${name} is required`,
328
+ code: "REQUIRED",
329
+ });
330
+ return errors;
331
+ }
332
+
333
+ // Skip further validation if value is empty and not required
334
+ if (value === null || value === undefined) {
335
+ return errors;
336
+ }
337
+
338
+ // Type check
339
+ if (typeof value !== "boolean") {
340
+ errors.push({
341
+ field: name,
342
+ message: `${name} must be a boolean`,
343
+ code: "INVALID_TYPE",
344
+ });
345
+ }
346
+
347
+ return errors;
348
+ }
349
+
350
+ /**
351
+ * Validate a date or datetime field value against its configuration
352
+ */
353
+ export function validateDateField(
354
+ value: unknown,
355
+ fieldDef: FieldDefinition
356
+ ): ValidationError[] {
357
+ const errors: ValidationError[] = [];
358
+ const { name, required, options } = fieldDef;
359
+
360
+ // Check required
361
+ if (required && (value === null || value === undefined)) {
362
+ errors.push({
363
+ field: name,
364
+ message: `${name} is required`,
365
+ code: "REQUIRED",
366
+ });
367
+ return errors;
368
+ }
369
+
370
+ // Skip further validation if value is empty and not required
371
+ if (value === null || value === undefined) {
372
+ return errors;
373
+ }
374
+
375
+ // Type check (must be a valid timestamp)
376
+ if (typeof value !== "number" || isNaN(value)) {
377
+ errors.push({
378
+ field: name,
379
+ message: `${name} must be a valid timestamp`,
380
+ code: "INVALID_TYPE",
381
+ });
382
+ return errors;
383
+ }
384
+
385
+ // Min date (using min from options)
386
+ if (options?.min !== undefined && value < options.min) {
387
+ errors.push({
388
+ field: name,
389
+ message: `${name} must be on or after the minimum date`,
390
+ code: "MIN_DATE",
391
+ });
392
+ }
393
+
394
+ // Max date (using max from options)
395
+ if (options?.max !== undefined && value > options.max) {
396
+ errors.push({
397
+ field: name,
398
+ message: `${name} must be on or before the maximum date`,
399
+ code: "MAX_DATE",
400
+ });
401
+ }
402
+
403
+ return errors;
404
+ }
405
+
406
+ /**
407
+ * Validate a reference field value against its configuration.
408
+ *
409
+ * Reference fields store IDs to other content entries. They support:
410
+ * - Single reference: `string` (one entry ID)
411
+ * - Multiple references: `string[]` (array of entry IDs) when `multiple: true`
412
+ *
413
+ * Configuration options:
414
+ * - `allowedContentTypes`: Array of content type names that can be referenced
415
+ * - `multiple`: If true, accepts an array of references
416
+ * - `minItems`: Minimum number of references required (only when `multiple: true`)
417
+ * - `max`: Maximum number of references allowed (only when `multiple: true`)
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * // Single reference to an author
422
+ * const authorField: FieldDefinition = {
423
+ * name: "author",
424
+ * label: "Author",
425
+ * type: "reference",
426
+ * required: true,
427
+ * options: {
428
+ * allowedContentTypes: ["user"],
429
+ * },
430
+ * };
431
+ *
432
+ * // Multiple references to related posts (1-5 required)
433
+ * const relatedPostsField: FieldDefinition = {
434
+ * name: "relatedPosts",
435
+ * label: "Related Posts",
436
+ * type: "reference",
437
+ * required: true,
438
+ * options: {
439
+ * allowedContentTypes: ["blog_post"],
440
+ * multiple: true,
441
+ * minItems: 1,
442
+ * max: 5,
443
+ * },
444
+ * };
445
+ * ```
446
+ */
447
+ export function validateReferenceField(
448
+ value: unknown,
449
+ fieldDef: FieldDefinition
450
+ ): ValidationError[] {
451
+ const errors: ValidationError[] = [];
452
+ const { name, required, options } = fieldDef;
453
+ const multiple = options?.multiple ?? false;
454
+
455
+ // Check required
456
+ if (required && (value === null || value === undefined)) {
457
+ errors.push({
458
+ field: name,
459
+ message: `${name} is required`,
460
+ code: "REQUIRED",
461
+ });
462
+ return errors;
463
+ }
464
+
465
+ // Skip further validation if value is empty and not required
466
+ if (value === null || value === undefined) {
467
+ return errors;
468
+ }
469
+
470
+ // Type check based on multiple setting
471
+ if (multiple) {
472
+ if (!Array.isArray(value)) {
473
+ errors.push({
474
+ field: name,
475
+ message: `${name} must be an array of references`,
476
+ code: "INVALID_TYPE",
477
+ });
478
+ return errors;
479
+ }
480
+
481
+ // Check if required and empty array
482
+ if (required && value.length === 0) {
483
+ errors.push({
484
+ field: name,
485
+ message: `${name} requires at least one reference`,
486
+ code: "REQUIRED",
487
+ });
488
+ }
489
+
490
+ // Check each item is a string (valid ID format)
491
+ for (const item of value) {
492
+ if (typeof item !== "string") {
493
+ errors.push({
494
+ field: name,
495
+ message: `${name} contains invalid reference IDs`,
496
+ code: "INVALID_TYPE",
497
+ });
498
+ break;
499
+ }
500
+ }
501
+
502
+ // Min items validation (only for multiple references)
503
+ if (options?.minItems !== undefined && value.length < options.minItems) {
504
+ errors.push({
505
+ field: name,
506
+ message: `${name} requires at least ${options.minItems} reference${options.minItems === 1 ? "" : "s"}`,
507
+ code: "MIN_ITEMS",
508
+ });
509
+ }
510
+
511
+ // Max items (using max from options)
512
+ if (options?.max !== undefined && value.length > options.max) {
513
+ errors.push({
514
+ field: name,
515
+ message: `${name} can have at most ${options.max} reference${options.max === 1 ? "" : "s"}`,
516
+ code: "MAX_ITEMS",
517
+ });
518
+ }
519
+ } else {
520
+ if (typeof value !== "string") {
521
+ errors.push({
522
+ field: name,
523
+ message: `${name} must be a reference ID`,
524
+ code: "INVALID_TYPE",
525
+ });
526
+ }
527
+ }
528
+
529
+ return errors;
530
+ }
531
+
532
+ /**
533
+ * Check if a reference value is valid for a given content type constraint.
534
+ *
535
+ * This is a helper function that can be used in mutation handlers to validate
536
+ * that referenced entries exist and belong to allowed content types.
537
+ *
538
+ * @param referenceId - The content entry ID to validate
539
+ * @param allowedContentTypes - Array of allowed content type names (optional)
540
+ * @param contentTypeLookup - Function to get content type name by entry ID
541
+ * @returns Object with `valid` boolean and optional `error` message
542
+ */
543
+ export async function validateReferenceContentType(
544
+ referenceId: string,
545
+ allowedContentTypes: string[] | undefined,
546
+ contentTypeLookup: (entryId: string) => Promise<string | null>
547
+ ): Promise<{ valid: boolean; error?: string }> {
548
+ // If no content type constraints, the reference is valid
549
+ if (!allowedContentTypes || allowedContentTypes.length === 0) {
550
+ return { valid: true };
551
+ }
552
+
553
+ // Look up the content type of the referenced entry
554
+ const contentTypeName = await contentTypeLookup(referenceId);
555
+
556
+ // If the entry doesn't exist, it's invalid
557
+ if (contentTypeName === null) {
558
+ return {
559
+ valid: false,
560
+ error: `Referenced entry not found: ${referenceId}`,
561
+ };
562
+ }
563
+
564
+ // Check if the content type is in the allowed list
565
+ if (!allowedContentTypes.includes(contentTypeName)) {
566
+ return {
567
+ valid: false,
568
+ error: `Reference must be of type: ${allowedContentTypes.join(", ")}. Got: ${contentTypeName}`,
569
+ };
570
+ }
571
+
572
+ return { valid: true };
573
+ }
574
+
575
+ /**
576
+ * Validate a media field value against its configuration.
577
+ *
578
+ * Media fields store IDs to media assets. They support:
579
+ * - Single reference: `string` (one media asset ID)
580
+ * - Multiple references (gallery): `string[]` (array of media asset IDs) when `multiple: true`
581
+ *
582
+ * Configuration options:
583
+ * - `allowedMimeTypes`: Array of allowed MIME types (supports wildcards like "image/*")
584
+ * - `multiple`: If true, accepts an array of references (gallery mode)
585
+ * - `minItems`: Minimum number of media assets required (only when `multiple: true`)
586
+ * - `max`: Maximum number of media assets allowed (only when `multiple: true`)
587
+ * - `maxFileSize`: Maximum file size in bytes (validated at upload time, not here)
588
+ *
589
+ * Note: MIME type validation requires database lookups and is performed by
590
+ * `validateAllMediaReferences` in the mediaReferenceResolver module.
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * // Single featured image (images only)
595
+ * const featuredImageField: FieldDefinition = {
596
+ * name: "featuredImage",
597
+ * label: "Featured Image",
598
+ * type: "media",
599
+ * required: true,
600
+ * options: {
601
+ * allowedMimeTypes: ["image/*"],
602
+ * },
603
+ * };
604
+ *
605
+ * // Gallery with 2-10 images
606
+ * const galleryField: FieldDefinition = {
607
+ * name: "gallery",
608
+ * label: "Photo Gallery",
609
+ * type: "media",
610
+ * required: true,
611
+ * options: {
612
+ * allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
613
+ * multiple: true,
614
+ * minItems: 2,
615
+ * max: 10,
616
+ * },
617
+ * };
618
+ * ```
619
+ */
620
+ export function validateMediaField(
621
+ value: unknown,
622
+ fieldDef: FieldDefinition
623
+ ): ValidationError[] {
624
+ const errors: ValidationError[] = [];
625
+ const { name, required, options } = fieldDef;
626
+ const multiple = options?.multiple ?? false;
627
+
628
+ // Check required
629
+ if (required && (value === null || value === undefined)) {
630
+ errors.push({
631
+ field: name,
632
+ message: `${name} is required`,
633
+ code: "REQUIRED",
634
+ });
635
+ return errors;
636
+ }
637
+
638
+ // Skip further validation if value is empty and not required
639
+ if (value === null || value === undefined) {
640
+ return errors;
641
+ }
642
+
643
+ // Type check based on multiple setting
644
+ if (multiple) {
645
+ if (!Array.isArray(value)) {
646
+ errors.push({
647
+ field: name,
648
+ message: `${name} must be an array of media asset IDs`,
649
+ code: "INVALID_TYPE",
650
+ });
651
+ return errors;
652
+ }
653
+
654
+ // Check if required and empty array
655
+ if (required && value.length === 0) {
656
+ errors.push({
657
+ field: name,
658
+ message: `${name} requires at least one media asset`,
659
+ code: "REQUIRED",
660
+ });
661
+ }
662
+
663
+ // Check each item is a string (valid ID format)
664
+ for (const item of value) {
665
+ if (typeof item !== "string") {
666
+ errors.push({
667
+ field: name,
668
+ message: `${name} contains invalid media asset IDs`,
669
+ code: "INVALID_TYPE",
670
+ });
671
+ break;
672
+ }
673
+ }
674
+
675
+ // Min items validation (only for multiple/gallery media fields)
676
+ if (options?.minItems !== undefined && value.length < options.minItems) {
677
+ errors.push({
678
+ field: name,
679
+ message: `${name} requires at least ${options.minItems} media asset${options.minItems === 1 ? "" : "s"}`,
680
+ code: "MIN_ITEMS",
681
+ });
682
+ }
683
+
684
+ // Max items (using max from options)
685
+ if (options?.max !== undefined && value.length > options.max) {
686
+ errors.push({
687
+ field: name,
688
+ message: `${name} can have at most ${options.max} media asset${options.max === 1 ? "" : "s"}`,
689
+ code: "MAX_ITEMS",
690
+ });
691
+ }
692
+ } else {
693
+ if (typeof value !== "string") {
694
+ errors.push({
695
+ field: name,
696
+ message: `${name} must be a media asset ID`,
697
+ code: "INVALID_TYPE",
698
+ });
699
+ }
700
+ }
701
+
702
+ return errors;
703
+ }
704
+
705
+ /**
706
+ * Validate a select field value against its configuration
707
+ */
708
+ export function validateSelectField(
709
+ value: unknown,
710
+ fieldDef: FieldDefinition
711
+ ): ValidationError[] {
712
+ const errors: ValidationError[] = [];
713
+ const { name, required, options } = fieldDef;
714
+
715
+ // Check required
716
+ if (required && (value === null || value === undefined || value === "")) {
717
+ errors.push({
718
+ field: name,
719
+ message: `${name} is required`,
720
+ code: "REQUIRED",
721
+ });
722
+ return errors;
723
+ }
724
+
725
+ // Skip further validation if value is empty and not required
726
+ if (value === null || value === undefined || value === "") {
727
+ return errors;
728
+ }
729
+
730
+ // Type check
731
+ if (typeof value !== "string") {
732
+ errors.push({
733
+ field: name,
734
+ message: `${name} must be a string`,
735
+ code: "INVALID_TYPE",
736
+ });
737
+ return errors;
738
+ }
739
+
740
+ // Validate against allowed options
741
+ if (options?.options) {
742
+ const allowedValues = options.options.map((opt) => opt.value);
743
+ if (!allowedValues.includes(value)) {
744
+ errors.push({
745
+ field: name,
746
+ message: `${name} has an invalid value`,
747
+ code: "INVALID_TYPE",
748
+ });
749
+ }
750
+ }
751
+
752
+ return errors;
753
+ }
754
+
755
+ /**
756
+ * Validate a multi-select field value against its configuration
757
+ */
758
+ export function validateMultiSelectField(
759
+ value: unknown,
760
+ fieldDef: FieldDefinition
761
+ ): ValidationError[] {
762
+ const errors: ValidationError[] = [];
763
+ const { name, required, options } = fieldDef;
764
+
765
+ // Check required
766
+ if (required && (value === null || value === undefined)) {
767
+ errors.push({
768
+ field: name,
769
+ message: `${name} is required`,
770
+ code: "REQUIRED",
771
+ });
772
+ return errors;
773
+ }
774
+
775
+ // Skip further validation if value is empty and not required
776
+ if (value === null || value === undefined) {
777
+ return errors;
778
+ }
779
+
780
+ // Type check - must be array
781
+ if (!Array.isArray(value)) {
782
+ errors.push({
783
+ field: name,
784
+ message: `${name} must be an array`,
785
+ code: "INVALID_TYPE",
786
+ });
787
+ return errors;
788
+ }
789
+
790
+ // Check if required and empty array
791
+ if (required && value.length === 0) {
792
+ errors.push({
793
+ field: name,
794
+ message: `${name} requires at least one selection`,
795
+ code: "REQUIRED",
796
+ });
797
+ }
798
+
799
+ // Validate each item is a string and in allowed options
800
+ const allowedValues = options?.options?.map((opt) => opt.value) ?? [];
801
+ for (const item of value) {
802
+ if (typeof item !== "string") {
803
+ errors.push({
804
+ field: name,
805
+ message: `${name} contains invalid values`,
806
+ code: "INVALID_TYPE",
807
+ });
808
+ break;
809
+ }
810
+ if (allowedValues.length > 0 && !allowedValues.includes(item)) {
811
+ errors.push({
812
+ field: name,
813
+ message: `${name} contains an invalid option`,
814
+ code: "INVALID_TYPE",
815
+ });
816
+ break;
817
+ }
818
+ }
819
+
820
+ return errors;
821
+ }
822
+
823
+ /**
824
+ * Validate a JSON field value
825
+ */
826
+ export function validateJsonField(
827
+ value: unknown,
828
+ fieldDef: FieldDefinition
829
+ ): ValidationError[] {
830
+ const errors: ValidationError[] = [];
831
+ const { name, required } = fieldDef;
832
+
833
+ // Check required
834
+ if (required && (value === null || value === undefined)) {
835
+ errors.push({
836
+ field: name,
837
+ message: `${name} is required`,
838
+ code: "REQUIRED",
839
+ });
840
+ return errors;
841
+ }
842
+
843
+ // JSON fields can be any valid JSON value, so minimal type checking
844
+ // The value has already been parsed if it was a string
845
+ return errors;
846
+ }
847
+
848
+ /**
849
+ * Validate a tags field value against its configuration.
850
+ *
851
+ * Tags fields store arrays of taxonomy term IDs for flexible content categorization.
852
+ * They support:
853
+ * - Multiple term selection
854
+ * - Optional inline term creation (when allowCreate is true)
855
+ * - Min/max limits on number of tags
856
+ *
857
+ * Configuration options:
858
+ * - `taxonomyId`: The taxonomy these tags belong to (required at content type level)
859
+ * - `allowCreate`: If true, users can create new tags inline
860
+ * - `minTags`: Minimum number of tags required
861
+ * - `maxTags`: Maximum number of tags allowed
862
+ *
863
+ * @example
864
+ * ```typescript
865
+ * const tagsField: FieldDefinition = {
866
+ * name: "tags",
867
+ * label: "Tags",
868
+ * type: "tags",
869
+ * required: true,
870
+ * options: {
871
+ * taxonomyId: "tags_taxonomy_id",
872
+ * allowCreate: true,
873
+ * minTags: 1,
874
+ * maxTags: 10,
875
+ * },
876
+ * };
877
+ * ```
878
+ */
879
+ export function validateTagsField(
880
+ value: unknown,
881
+ fieldDef: FieldDefinition
882
+ ): ValidationError[] {
883
+ const errors: ValidationError[] = [];
884
+ const { name, required, options } = fieldDef;
885
+
886
+ // Check required
887
+ if (required && (value === null || value === undefined)) {
888
+ errors.push({
889
+ field: name,
890
+ message: `${name} is required`,
891
+ code: "REQUIRED",
892
+ });
893
+ return errors;
894
+ }
895
+
896
+ // Skip further validation if value is empty and not required
897
+ if (value === null || value === undefined) {
898
+ return errors;
899
+ }
900
+
901
+ // Type check - must be array of strings (term IDs)
902
+ if (!Array.isArray(value)) {
903
+ errors.push({
904
+ field: name,
905
+ message: `${name} must be an array of tag IDs`,
906
+ code: "INVALID_TYPE",
907
+ });
908
+ return errors;
909
+ }
910
+
911
+ // Check if required and empty array
912
+ if (required && value.length === 0) {
913
+ errors.push({
914
+ field: name,
915
+ message: `${name} requires at least one tag`,
916
+ code: "REQUIRED",
917
+ });
918
+ return errors;
919
+ }
920
+
921
+ // Validate each item is a string
922
+ for (const item of value) {
923
+ if (typeof item !== "string") {
924
+ errors.push({
925
+ field: name,
926
+ message: `${name} contains invalid tag IDs`,
927
+ code: "INVALID_TYPE",
928
+ });
929
+ break;
930
+ }
931
+ }
932
+
933
+ // Min tags validation
934
+ const minTags = options?.minTags;
935
+ if (minTags !== undefined && value.length < minTags) {
936
+ errors.push({
937
+ field: name,
938
+ message: `${name} requires at least ${minTags} tag${minTags === 1 ? "" : "s"}`,
939
+ code: "MIN_ITEMS",
940
+ });
941
+ }
942
+
943
+ // Max tags validation
944
+ const maxTags = options?.maxTags;
945
+ if (maxTags !== undefined && value.length > maxTags) {
946
+ errors.push({
947
+ field: name,
948
+ message: `${name} can have at most ${maxTags} tag${maxTags === 1 ? "" : "s"}`,
949
+ code: "MAX_ITEMS",
950
+ });
951
+ }
952
+
953
+ return errors;
954
+ }
955
+
956
+ /**
957
+ * Validate a category field value against its configuration.
958
+ *
959
+ * Category fields store taxonomy term IDs for hierarchical content organization.
960
+ * They support:
961
+ * - Single category selection (default)
962
+ * - Multiple category selection (when allowMultiple is true)
963
+ *
964
+ * Configuration options:
965
+ * - `taxonomyId`: The taxonomy these categories belong to (required at content type level)
966
+ * - `allowMultiple`: If true, accepts an array of category IDs
967
+ *
968
+ * @example
969
+ * ```typescript
970
+ * // Single category selection
971
+ * const categoryField: FieldDefinition = {
972
+ * name: "category",
973
+ * label: "Category",
974
+ * type: "category",
975
+ * required: true,
976
+ * options: {
977
+ * taxonomyId: "categories_taxonomy_id",
978
+ * },
979
+ * };
980
+ *
981
+ * // Multiple category selection
982
+ * const categoriesField: FieldDefinition = {
983
+ * name: "categories",
984
+ * label: "Categories",
985
+ * type: "category",
986
+ * required: true,
987
+ * options: {
988
+ * taxonomyId: "categories_taxonomy_id",
989
+ * allowMultiple: true,
990
+ * },
991
+ * };
992
+ * ```
993
+ */
994
+ export function validateCategoryField(
995
+ value: unknown,
996
+ fieldDef: FieldDefinition
997
+ ): ValidationError[] {
998
+ const errors: ValidationError[] = [];
999
+ const { name, required, options } = fieldDef;
1000
+ const allowMultiple = options?.allowMultiple ?? false;
1001
+
1002
+ // Check required
1003
+ if (required && (value === null || value === undefined)) {
1004
+ errors.push({
1005
+ field: name,
1006
+ message: `${name} is required`,
1007
+ code: "REQUIRED",
1008
+ });
1009
+ return errors;
1010
+ }
1011
+
1012
+ // Skip further validation if value is empty and not required
1013
+ if (value === null || value === undefined) {
1014
+ return errors;
1015
+ }
1016
+
1017
+ // Type check based on allowMultiple setting
1018
+ if (allowMultiple) {
1019
+ if (!Array.isArray(value)) {
1020
+ errors.push({
1021
+ field: name,
1022
+ message: `${name} must be an array of category IDs`,
1023
+ code: "INVALID_TYPE",
1024
+ });
1025
+ return errors;
1026
+ }
1027
+
1028
+ // Check if required and empty array
1029
+ if (required && value.length === 0) {
1030
+ errors.push({
1031
+ field: name,
1032
+ message: `${name} requires at least one category`,
1033
+ code: "REQUIRED",
1034
+ });
1035
+ }
1036
+
1037
+ // Check each item is a string (valid ID format)
1038
+ for (const item of value) {
1039
+ if (typeof item !== "string") {
1040
+ errors.push({
1041
+ field: name,
1042
+ message: `${name} contains invalid category IDs`,
1043
+ code: "INVALID_TYPE",
1044
+ });
1045
+ break;
1046
+ }
1047
+ }
1048
+ } else {
1049
+ // Single category selection
1050
+ if (typeof value !== "string") {
1051
+ errors.push({
1052
+ field: name,
1053
+ message: `${name} must be a category ID`,
1054
+ code: "INVALID_TYPE",
1055
+ });
1056
+ }
1057
+ }
1058
+
1059
+ return errors;
1060
+ }
1061
+
1062
+ // =============================================================================
1063
+ // Main Validation Function
1064
+ // =============================================================================
1065
+
1066
+ /**
1067
+ * Options for validating localized fields.
1068
+ */
1069
+ export interface LocalizedValidationOptions {
1070
+ /**
1071
+ * The locale to validate. If provided, only that locale's value is validated
1072
+ * for localized fields. If not provided, all locale values are validated.
1073
+ */
1074
+ locale?: string;
1075
+
1076
+ /**
1077
+ * Locales that must have values for required localized fields.
1078
+ * If not provided, only checks if at least one locale has a value for required fields.
1079
+ */
1080
+ requiredLocales?: string[];
1081
+ }
1082
+
1083
+ /**
1084
+ * Validate a single field value (non-localized) based on its type.
1085
+ * This is the core validation logic that handles the actual value checking.
1086
+ */
1087
+ function validateSingleValue(
1088
+ value: unknown,
1089
+ fieldDef: FieldDefinition
1090
+ ): ValidationError[] {
1091
+ const { name, type } = fieldDef;
1092
+
1093
+ switch (type) {
1094
+ case "text":
1095
+ return validateTextField(value, fieldDef);
1096
+ case "richText":
1097
+ return validateRichTextField(value, fieldDef);
1098
+ case "number":
1099
+ return validateNumberField(value, fieldDef);
1100
+ case "boolean":
1101
+ return validateBooleanField(value, fieldDef);
1102
+ case "date":
1103
+ case "datetime":
1104
+ return validateDateField(value, fieldDef);
1105
+ case "reference":
1106
+ return validateReferenceField(value, fieldDef);
1107
+ case "media":
1108
+ return validateMediaField(value, fieldDef);
1109
+ case "select":
1110
+ return validateSelectField(value, fieldDef);
1111
+ case "multiSelect":
1112
+ return validateMultiSelectField(value, fieldDef);
1113
+ case "json":
1114
+ return validateJsonField(value, fieldDef);
1115
+ case "tags":
1116
+ return validateTagsField(value, fieldDef);
1117
+ case "category":
1118
+ return validateCategoryField(value, fieldDef);
1119
+ default: {
1120
+ // Unknown field type
1121
+ return [
1122
+ {
1123
+ field: name,
1124
+ message: `Unknown field type: ${type}`,
1125
+ code: "INVALID_TYPE",
1126
+ },
1127
+ ];
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Validate a localized field value.
1134
+ *
1135
+ * For localized fields, the value should be a LocalizedFieldValue structure:
1136
+ * `{ "en-US": "Hello", "es-ES": "Hola" }`
1137
+ *
1138
+ * This function validates:
1139
+ * 1. The structure is a valid LocalizedFieldValue
1140
+ * 2. Each locale's value passes the field type validation
1141
+ * 3. Required locales have values (if specified)
1142
+ *
1143
+ * @param value - The localized field value to validate
1144
+ * @param fieldDef - The field definition
1145
+ * @param options - Validation options for localized fields
1146
+ * @returns Array of validation errors
1147
+ */
1148
+ export function validateLocalizedFieldValue(
1149
+ value: unknown,
1150
+ fieldDef: FieldDefinition,
1151
+ options: LocalizedValidationOptions = {}
1152
+ ): ValidationError[] {
1153
+ const errors: ValidationError[] = [];
1154
+ const { name, required } = fieldDef;
1155
+ const { locale, requiredLocales } = options;
1156
+
1157
+ // Handle null/undefined for required fields
1158
+ if (value === null || value === undefined) {
1159
+ if (required) {
1160
+ errors.push({
1161
+ field: name,
1162
+ message: `${name} is required`,
1163
+ code: "REQUIRED",
1164
+ });
1165
+ }
1166
+ return errors;
1167
+ }
1168
+
1169
+ // Check if the value is a valid LocalizedFieldValue structure
1170
+ if (!isLocalizedFieldValue(value)) {
1171
+ errors.push({
1172
+ field: name,
1173
+ message: `${name} must be a localized field structure (object with locale codes as keys)`,
1174
+ code: "INVALID_LOCALIZED_STRUCTURE",
1175
+ });
1176
+ return errors;
1177
+ }
1178
+
1179
+ const localizedValue = value as LocalizedFieldValue;
1180
+ const locales = Object.keys(localizedValue);
1181
+
1182
+ // Check if required and empty
1183
+ if (required && locales.length === 0) {
1184
+ errors.push({
1185
+ field: name,
1186
+ message: `${name} requires at least one locale value`,
1187
+ code: "REQUIRED",
1188
+ });
1189
+ return errors;
1190
+ }
1191
+
1192
+ // Check required locales
1193
+ if (requiredLocales && requiredLocales.length > 0) {
1194
+ for (const requiredLocale of requiredLocales) {
1195
+ if (!(requiredLocale in localizedValue)) {
1196
+ errors.push({
1197
+ field: name,
1198
+ message: `${name} is missing required translation for locale: ${requiredLocale}`,
1199
+ code: "MISSING_LOCALE",
1200
+ });
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ // If a specific locale is specified, validate only that locale
1206
+ if (locale) {
1207
+ if (locale in localizedValue) {
1208
+ // Create a non-localized field definition for single value validation
1209
+ const nonLocalizedFieldDef = { ...fieldDef, localized: false };
1210
+ const localeErrors = validateSingleValue(
1211
+ localizedValue[locale],
1212
+ nonLocalizedFieldDef
1213
+ );
1214
+ // Prefix errors with locale info
1215
+ for (const error of localeErrors) {
1216
+ errors.push({
1217
+ ...error,
1218
+ field: `${name}[${locale}]`,
1219
+ message: `${name} (${locale}): ${error.message.replace(`${name} `, "")}`,
1220
+ });
1221
+ }
1222
+ }
1223
+ } else {
1224
+ // Validate all locale values
1225
+ for (const [localeCode, localeValue] of Object.entries(localizedValue)) {
1226
+ // Create a non-localized field definition for single value validation
1227
+ const nonLocalizedFieldDef = { ...fieldDef, localized: false, required: false };
1228
+ const localeErrors = validateSingleValue(localeValue, nonLocalizedFieldDef);
1229
+ // Prefix errors with locale info
1230
+ for (const error of localeErrors) {
1231
+ errors.push({
1232
+ ...error,
1233
+ field: `${name}[${localeCode}]`,
1234
+ message: `${name} (${localeCode}): ${error.message.replace(`${name} `, "")}`,
1235
+ });
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ return errors;
1241
+ }
1242
+
1243
+ /**
1244
+ * Validate a single field value based on its definition.
1245
+ *
1246
+ * Handles both localized and non-localized fields:
1247
+ * - Non-localized fields: Validates the value directly
1248
+ * - Localized fields: Validates the LocalizedFieldValue structure and each locale's value
1249
+ *
1250
+ * @param value - The field value to validate (plain value or LocalizedFieldValue)
1251
+ * @param fieldDef - The field definition
1252
+ * @param options - Optional validation options for localized fields
1253
+ * @returns Array of validation errors
1254
+ */
1255
+ export function validateFieldValue(
1256
+ value: unknown,
1257
+ fieldDef: FieldDefinition,
1258
+ options?: LocalizedValidationOptions
1259
+ ): ValidationError[] {
1260
+ // Check if this is a localized field
1261
+ if (fieldDef.localized) {
1262
+ return validateLocalizedFieldValue(value, fieldDef, options);
1263
+ }
1264
+
1265
+ // Non-localized field - use standard validation
1266
+ return validateSingleValue(value, fieldDef);
1267
+ }
1268
+
1269
+ /**
1270
+ * Options for validating content data.
1271
+ */
1272
+ export interface ContentValidationOptions {
1273
+ /**
1274
+ * If true, reports unknown fields as errors.
1275
+ * If false (default), unknown fields are silently ignored.
1276
+ */
1277
+ strictFields?: boolean;
1278
+
1279
+ /**
1280
+ * Locale to validate for localized fields.
1281
+ * If provided, only that locale's values are validated.
1282
+ */
1283
+ locale?: string;
1284
+
1285
+ /**
1286
+ * Locales that must have values for required localized fields.
1287
+ */
1288
+ requiredLocales?: string[];
1289
+ }
1290
+
1291
+ /**
1292
+ * Validate content data against a content type schema
1293
+ *
1294
+ * @param data - The content data to validate
1295
+ * @param schema - The content type schema defining expected fields
1296
+ * @param options - Validation options
1297
+ * @returns ValidationResult with any errors found
1298
+ *
1299
+ * @example
1300
+ * ```typescript
1301
+ * // Basic validation
1302
+ * const result = validateContentData(data, schema);
1303
+ *
1304
+ * // Validate with localized field support
1305
+ * const result = validateContentData(data, schema, {
1306
+ * locale: "en-US",
1307
+ * requiredLocales: ["en-US", "es-ES"],
1308
+ * });
1309
+ * ```
1310
+ */
1311
+ export function validateContentData(
1312
+ data: ContentData,
1313
+ schema: ContentTypeSchema,
1314
+ options: ContentValidationOptions = {}
1315
+ ): ValidationResult {
1316
+ const errors: ValidationError[] = [];
1317
+ const fieldMap = new Map(schema.fields.map((f) => [f.name, f]));
1318
+ const { strictFields, locale, requiredLocales } = options;
1319
+
1320
+ // Create localized validation options
1321
+ const localizedOptions: LocalizedValidationOptions = {
1322
+ locale,
1323
+ requiredLocales,
1324
+ };
1325
+
1326
+ // Validate each defined field
1327
+ for (const fieldDef of schema.fields) {
1328
+ const value = data[fieldDef.name];
1329
+ const fieldErrors = validateFieldValue(value, fieldDef, localizedOptions);
1330
+ errors.push(...fieldErrors);
1331
+ }
1332
+
1333
+ // Check for unknown fields if strict mode
1334
+ if (strictFields) {
1335
+ for (const key of Object.keys(data)) {
1336
+ if (!fieldMap.has(key)) {
1337
+ errors.push({
1338
+ field: key,
1339
+ message: `Unknown field: ${key}`,
1340
+ code: "UNKNOWN_FIELD",
1341
+ });
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ if (errors.length === 0) {
1347
+ return { valid: true, errors: [] };
1348
+ }
1349
+
1350
+ return { valid: false, errors };
1351
+ }
1352
+
1353
+ /**
1354
+ * Apply default values to content data based on field definitions
1355
+ */
1356
+ export function applyFieldDefaults(
1357
+ data: ContentData,
1358
+ schema: ContentTypeSchema
1359
+ ): ContentData {
1360
+ const result = { ...data };
1361
+
1362
+ for (const fieldDef of schema.fields) {
1363
+ const { name, defaultValue } = fieldDef;
1364
+
1365
+ // Only apply default if field is not already set
1366
+ if (result[name] === undefined || result[name] === null) {
1367
+ if (defaultValue !== undefined) {
1368
+ result[name] = defaultValue;
1369
+ }
1370
+ }
1371
+ }
1372
+
1373
+ return result;
1374
+ }
1375
+
1376
+ /**
1377
+ * Get the field type from a field definition
1378
+ */
1379
+ export function getFieldType(fieldDef: FieldDefinition): FieldType {
1380
+ return fieldDef.type;
1381
+ }
1382
+
1383
+ /**
1384
+ * Check if a field is required based on its configuration
1385
+ */
1386
+ export function isFieldRequired(fieldDef: FieldDefinition): boolean {
1387
+ return fieldDef.required === true;
1388
+ }