@rakun-kit/core 1.4.5 → 1.4.7

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 (216) hide show
  1. package/README.md +712 -675
  2. package/dist/cjs/api/operations/manager-contract.js +9 -0
  3. package/dist/cjs/api/operations/manager-contract.js.map +1 -1
  4. package/dist/cjs/api/operations/manager-manifest.js +5 -0
  5. package/dist/cjs/api/operations/manager-manifest.js.map +1 -1
  6. package/dist/cjs/api/operations/manager.js +4 -0
  7. package/dist/cjs/api/operations/manager.js.map +1 -1
  8. package/dist/cjs/api/routes/manager/create.js +79 -2
  9. package/dist/cjs/api/routes/manager/create.js.map +1 -1
  10. package/dist/cjs/api/routes/manager/delete.js +2 -0
  11. package/dist/cjs/api/routes/manager/delete.js.map +1 -1
  12. package/dist/cjs/api/routes/manager/duplicate.js +2 -0
  13. package/dist/cjs/api/routes/manager/duplicate.js.map +1 -1
  14. package/dist/cjs/api/routes/manager/get.js +4 -1
  15. package/dist/cjs/api/routes/manager/get.js.map +1 -1
  16. package/dist/cjs/api/routes/manager/linkedIterator.js +69 -0
  17. package/dist/cjs/api/routes/manager/linkedIterator.js.map +1 -0
  18. package/dist/cjs/api/routes/manager/list.js +2 -0
  19. package/dist/cjs/api/routes/manager/list.js.map +1 -1
  20. package/dist/cjs/api/routes/manager/translateDocument.js +46 -2
  21. package/dist/cjs/api/routes/manager/translateDocument.js.map +1 -1
  22. package/dist/cjs/api/routes/manager/trash.js +2 -0
  23. package/dist/cjs/api/routes/manager/trash.js.map +1 -1
  24. package/dist/cjs/api/routes/manager/update.js +74 -2
  25. package/dist/cjs/api/routes/manager/update.js.map +1 -1
  26. package/dist/cjs/api/routes/web/page.js +14 -4
  27. package/dist/cjs/api/routes/web/page.js.map +1 -1
  28. package/dist/cjs/api/routes/web/previewPage.js +1 -0
  29. package/dist/cjs/api/routes/web/previewPage.js.map +1 -1
  30. package/dist/cjs/api/utils/dynamicData.js +63 -14
  31. package/dist/cjs/api/utils/dynamicData.js.map +1 -1
  32. package/dist/cjs/api/utils/index.js +2 -1
  33. package/dist/cjs/api/utils/index.js.map +1 -1
  34. package/dist/cjs/api/utils/linkedIterator.js +167 -0
  35. package/dist/cjs/api/utils/linkedIterator.js.map +1 -0
  36. package/dist/cjs/api/utils/routes/revalidatePath.js +9 -0
  37. package/dist/cjs/api/utils/routes/revalidatePath.js.map +1 -1
  38. package/dist/cjs/client.js +2 -1
  39. package/dist/cjs/client.js.map +1 -1
  40. package/dist/cjs/index.js +3 -2
  41. package/dist/cjs/index.js.map +1 -1
  42. package/dist/cjs/internal-content-types/LinkedIteratorTemplate.js +20 -0
  43. package/dist/cjs/internal-content-types/LinkedIteratorTemplate.js.map +1 -0
  44. package/dist/cjs/internal-content-types/index.js +1 -0
  45. package/dist/cjs/internal-content-types/index.js.map +1 -1
  46. package/dist/cjs/lib/ContentType.js +24 -0
  47. package/dist/cjs/lib/ContentType.js.map +1 -1
  48. package/dist/cjs/lib/Registry.js +1 -0
  49. package/dist/cjs/lib/Registry.js.map +1 -1
  50. package/dist/cjs/lib/dynamicData.js +7 -0
  51. package/dist/cjs/lib/dynamicData.js.map +1 -1
  52. package/dist/cjs/lib/systemFields.js +2 -1
  53. package/dist/cjs/lib/systemFields.js.map +1 -1
  54. package/dist/cjs/schemas/manager/create.js +2 -0
  55. package/dist/cjs/schemas/manager/create.js.map +1 -1
  56. package/dist/cjs/schemas/manager/linkedIterator.js +27 -0
  57. package/dist/cjs/schemas/manager/linkedIterator.js.map +1 -0
  58. package/dist/cjs/schemas/manager/update.js +2 -0
  59. package/dist/cjs/schemas/manager/update.js.map +1 -1
  60. package/dist/esm/api/operations/index.d.ts +23 -0
  61. package/dist/esm/api/operations/index.d.ts.map +1 -1
  62. package/dist/esm/api/operations/manager-contract.d.ts +23 -0
  63. package/dist/esm/api/operations/manager-contract.d.ts.map +1 -1
  64. package/dist/esm/api/operations/manager-contract.js +9 -0
  65. package/dist/esm/api/operations/manager-contract.js.map +1 -1
  66. package/dist/esm/api/operations/manager-manifest.d.ts +5 -0
  67. package/dist/esm/api/operations/manager-manifest.d.ts.map +1 -1
  68. package/dist/esm/api/operations/manager-manifest.js +5 -0
  69. package/dist/esm/api/operations/manager-manifest.js.map +1 -1
  70. package/dist/esm/api/operations/manager.d.ts +23 -0
  71. package/dist/esm/api/operations/manager.d.ts.map +1 -1
  72. package/dist/esm/api/operations/manager.js +4 -0
  73. package/dist/esm/api/operations/manager.js.map +1 -1
  74. package/dist/esm/api/routes/manager/auth/markTourSeen.d.ts +2 -0
  75. package/dist/esm/api/routes/manager/auth/markTourSeen.d.ts.map +1 -1
  76. package/dist/esm/api/routes/manager/auth/updateAccount.d.ts +2 -0
  77. package/dist/esm/api/routes/manager/auth/updateAccount.d.ts.map +1 -1
  78. package/dist/esm/api/routes/manager/auth/updateTutorialPreferences.d.ts +2 -0
  79. package/dist/esm/api/routes/manager/auth/updateTutorialPreferences.d.ts.map +1 -1
  80. package/dist/esm/api/routes/manager/contentType.d.ts +1 -0
  81. package/dist/esm/api/routes/manager/contentType.d.ts.map +1 -1
  82. package/dist/esm/api/routes/manager/contentTypes.d.ts +1 -0
  83. package/dist/esm/api/routes/manager/contentTypes.d.ts.map +1 -1
  84. package/dist/esm/api/routes/manager/create.d.ts +1 -0
  85. package/dist/esm/api/routes/manager/create.d.ts.map +1 -1
  86. package/dist/esm/api/routes/manager/create.js +79 -2
  87. package/dist/esm/api/routes/manager/create.js.map +1 -1
  88. package/dist/esm/api/routes/manager/delete.d.ts.map +1 -1
  89. package/dist/esm/api/routes/manager/delete.js +2 -0
  90. package/dist/esm/api/routes/manager/delete.js.map +1 -1
  91. package/dist/esm/api/routes/manager/duplicate.d.ts +1 -0
  92. package/dist/esm/api/routes/manager/duplicate.d.ts.map +1 -1
  93. package/dist/esm/api/routes/manager/duplicate.js +2 -0
  94. package/dist/esm/api/routes/manager/duplicate.js.map +1 -1
  95. package/dist/esm/api/routes/manager/get.d.ts.map +1 -1
  96. package/dist/esm/api/routes/manager/get.js +4 -1
  97. package/dist/esm/api/routes/manager/get.js.map +1 -1
  98. package/dist/esm/api/routes/manager/languages.d.ts +1 -0
  99. package/dist/esm/api/routes/manager/languages.d.ts.map +1 -1
  100. package/dist/esm/api/routes/manager/linkedIterator.d.ts +11 -0
  101. package/dist/esm/api/routes/manager/linkedIterator.d.ts.map +1 -0
  102. package/dist/esm/api/routes/manager/linkedIterator.js +62 -0
  103. package/dist/esm/api/routes/manager/linkedIterator.js.map +1 -0
  104. package/dist/esm/api/routes/manager/list.d.ts.map +1 -1
  105. package/dist/esm/api/routes/manager/list.js +2 -0
  106. package/dist/esm/api/routes/manager/list.js.map +1 -1
  107. package/dist/esm/api/routes/manager/translateDocument.d.ts.map +1 -1
  108. package/dist/esm/api/routes/manager/translateDocument.js +46 -2
  109. package/dist/esm/api/routes/manager/translateDocument.js.map +1 -1
  110. package/dist/esm/api/routes/manager/trash.d.ts.map +1 -1
  111. package/dist/esm/api/routes/manager/trash.js +2 -0
  112. package/dist/esm/api/routes/manager/trash.js.map +1 -1
  113. package/dist/esm/api/routes/manager/update.d.ts +1 -0
  114. package/dist/esm/api/routes/manager/update.d.ts.map +1 -1
  115. package/dist/esm/api/routes/manager/update.js +74 -2
  116. package/dist/esm/api/routes/manager/update.js.map +1 -1
  117. package/dist/esm/api/routes/web/page.d.ts +2 -1
  118. package/dist/esm/api/routes/web/page.d.ts.map +1 -1
  119. package/dist/esm/api/routes/web/page.js +14 -4
  120. package/dist/esm/api/routes/web/page.js.map +1 -1
  121. package/dist/esm/api/routes/web/previewPage.d.ts.map +1 -1
  122. package/dist/esm/api/routes/web/previewPage.js +1 -0
  123. package/dist/esm/api/routes/web/previewPage.js.map +1 -1
  124. package/dist/esm/api/utils/dynamicData.d.ts +1 -0
  125. package/dist/esm/api/utils/dynamicData.d.ts.map +1 -1
  126. package/dist/esm/api/utils/dynamicData.js +61 -13
  127. package/dist/esm/api/utils/dynamicData.js.map +1 -1
  128. package/dist/esm/api/utils/getUser.d.ts +2 -0
  129. package/dist/esm/api/utils/getUser.d.ts.map +1 -1
  130. package/dist/esm/api/utils/index.d.ts +1 -1
  131. package/dist/esm/api/utils/index.d.ts.map +1 -1
  132. package/dist/esm/api/utils/index.js +1 -1
  133. package/dist/esm/api/utils/index.js.map +1 -1
  134. package/dist/esm/api/utils/linkedIterator.d.ts +41 -0
  135. package/dist/esm/api/utils/linkedIterator.d.ts.map +1 -0
  136. package/dist/esm/api/utils/linkedIterator.js +157 -0
  137. package/dist/esm/api/utils/linkedIterator.js.map +1 -0
  138. package/dist/esm/api/utils/routes/revalidatePath.d.ts +1 -0
  139. package/dist/esm/api/utils/routes/revalidatePath.d.ts.map +1 -1
  140. package/dist/esm/api/utils/routes/revalidatePath.js +8 -0
  141. package/dist/esm/api/utils/routes/revalidatePath.js.map +1 -1
  142. package/dist/esm/api/utils/routes/routeMapHelpers.d.ts +1 -0
  143. package/dist/esm/api/utils/routes/routeMapHelpers.d.ts.map +1 -1
  144. package/dist/esm/client.d.ts +3 -2
  145. package/dist/esm/client.d.ts.map +1 -1
  146. package/dist/esm/client.js +1 -1
  147. package/dist/esm/client.js.map +1 -1
  148. package/dist/esm/index.d.ts +2 -2
  149. package/dist/esm/index.d.ts.map +1 -1
  150. package/dist/esm/index.js +1 -1
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/internal-content-types/Backup.d.ts.map +1 -1
  153. package/dist/esm/internal-content-types/ContentComment.d.ts +5 -0
  154. package/dist/esm/internal-content-types/ContentComment.d.ts.map +1 -1
  155. package/dist/esm/internal-content-types/ContentVersion.d.ts.map +1 -1
  156. package/dist/esm/internal-content-types/LinkedIteratorTemplate.d.ts +551 -0
  157. package/dist/esm/internal-content-types/LinkedIteratorTemplate.d.ts.map +1 -0
  158. package/dist/esm/internal-content-types/LinkedIteratorTemplate.js +14 -0
  159. package/dist/esm/internal-content-types/LinkedIteratorTemplate.js.map +1 -0
  160. package/dist/esm/internal-content-types/ManagerFavorite.d.ts +4 -0
  161. package/dist/esm/internal-content-types/ManagerFavorite.d.ts.map +1 -1
  162. package/dist/esm/internal-content-types/ManagerUser.d.ts +1 -0
  163. package/dist/esm/internal-content-types/ManagerUser.d.ts.map +1 -1
  164. package/dist/esm/internal-content-types/MfaChallenge.d.ts +4 -0
  165. package/dist/esm/internal-content-types/MfaChallenge.d.ts.map +1 -1
  166. package/dist/esm/internal-content-types/Migration.d.ts.map +1 -1
  167. package/dist/esm/internal-content-types/RobotsRule.d.ts.map +1 -1
  168. package/dist/esm/internal-content-types/Route.d.ts.map +1 -1
  169. package/dist/esm/internal-content-types/RouteLayoutModule.d.ts.map +1 -1
  170. package/dist/esm/internal-content-types/SchemaState.d.ts.map +1 -1
  171. package/dist/esm/internal-content-types/Session.d.ts +4 -0
  172. package/dist/esm/internal-content-types/Session.d.ts.map +1 -1
  173. package/dist/esm/internal-content-types/UserMfa.d.ts +4 -0
  174. package/dist/esm/internal-content-types/UserMfa.d.ts.map +1 -1
  175. package/dist/esm/internal-content-types/WebAuthnCredential.d.ts +4 -0
  176. package/dist/esm/internal-content-types/WebAuthnCredential.d.ts.map +1 -1
  177. package/dist/esm/internal-content-types/WebAuthnRegChallenge.d.ts +4 -0
  178. package/dist/esm/internal-content-types/WebAuthnRegChallenge.d.ts.map +1 -1
  179. package/dist/esm/internal-content-types/index.d.ts +1 -0
  180. package/dist/esm/internal-content-types/index.d.ts.map +1 -1
  181. package/dist/esm/internal-content-types/index.js +1 -0
  182. package/dist/esm/internal-content-types/index.js.map +1 -1
  183. package/dist/esm/internal-content-types/syncAdminRole.d.ts +1 -0
  184. package/dist/esm/internal-content-types/syncAdminRole.d.ts.map +1 -1
  185. package/dist/esm/lib/ContentType.d.ts +19 -5
  186. package/dist/esm/lib/ContentType.d.ts.map +1 -1
  187. package/dist/esm/lib/ContentType.js +25 -1
  188. package/dist/esm/lib/ContentType.js.map +1 -1
  189. package/dist/esm/lib/Registry.d.ts +2 -0
  190. package/dist/esm/lib/Registry.d.ts.map +1 -1
  191. package/dist/esm/lib/Registry.js +1 -0
  192. package/dist/esm/lib/Registry.js.map +1 -1
  193. package/dist/esm/lib/dynamicData.d.ts +19 -0
  194. package/dist/esm/lib/dynamicData.d.ts.map +1 -1
  195. package/dist/esm/lib/dynamicData.js +7 -0
  196. package/dist/esm/lib/dynamicData.js.map +1 -1
  197. package/dist/esm/lib/systemFields.d.ts +1 -0
  198. package/dist/esm/lib/systemFields.d.ts.map +1 -1
  199. package/dist/esm/lib/systemFields.js +1 -0
  200. package/dist/esm/lib/systemFields.js.map +1 -1
  201. package/dist/esm/manager.d.ts +5 -0
  202. package/dist/esm/manager.d.ts.map +1 -1
  203. package/dist/esm/schemas/manager/create.d.ts +11 -0
  204. package/dist/esm/schemas/manager/create.d.ts.map +1 -1
  205. package/dist/esm/schemas/manager/create.js +2 -0
  206. package/dist/esm/schemas/manager/create.js.map +1 -1
  207. package/dist/esm/schemas/manager/linkedIterator.d.ts +41 -0
  208. package/dist/esm/schemas/manager/linkedIterator.d.ts.map +1 -0
  209. package/dist/esm/schemas/manager/linkedIterator.js +21 -0
  210. package/dist/esm/schemas/manager/linkedIterator.js.map +1 -0
  211. package/dist/esm/schemas/manager/update.d.ts +11 -0
  212. package/dist/esm/schemas/manager/update.d.ts.map +1 -1
  213. package/dist/esm/schemas/manager/update.js +2 -0
  214. package/dist/esm/schemas/manager/update.js.map +1 -1
  215. package/dist/tsconfig.build.tsbuildinfo +1 -1
  216. package/package.json +131 -131
package/README.md CHANGED
@@ -1,677 +1,714 @@
1
- # @rakun-kit/core
2
-
3
- `@rakun-kit/core` contains Rakun's shared domain model and runtime. It defines content types, fields, Zod schemas, manager/web operations, request context, MongoDB connection handling, media service, permissions, translations, and typed contracts used by adapters (`@rakun-kit/next`, `@rakun-kit/express`, `@rakun-kit/trpc`) and the manager client.
4
-
5
- ## Main Entry
6
-
7
- Typical imports:
8
-
9
- ```ts
10
- import {
11
- ContentType,
12
- Fields,
13
- rakunBootstrap,
14
- ensureRakunInitialized,
15
- } from "@rakun-kit/core";
16
- ```
17
-
18
- The main entrypoint exports:
19
-
20
- - Bootstrap: `rakunBootstrap`, `ensureRakunBootstrap`, `ensureRakunInitialized`, `getRakunBootstrapOptions`.
21
- - Context: `createRequestContext`, `getSessionCookie`, `setSessionCookie`.
22
- - Operations: manager/web contracts and definitions.
23
- - Media: `createMediaService`, `getMediaService`, adapters, and types.
24
- - Permissions, translations, errors, contracts, and public types.
25
- - Basic internal content types: `Language`, `ManagerUser`, `Seo`.
26
-
27
- Relevant subpaths:
28
-
29
- - `@rakun-kit/core/manager`: manager operation metadata and types.
30
- - `@rakun-kit/core/contracts`: shared manager/web payload schemas.
31
- - `@rakun-kit/core/client`: client-facing types and utilities.
32
- - `@rakun-kit/core/logger`: logger.
33
- - `@rakun-kit/core/errors`: application errors.
34
- - `@rakun-kit/core/types`: types derived from content types.
35
-
36
- ## Bootstrap
37
-
38
- `rakunBootstrap(options)` registers global configuration and content types:
39
-
40
- ```ts
41
- rakunBootstrap({
42
- literals,
43
- contentTypes: [Page, Post],
44
- routes,
45
- apiOperations,
46
- mongo: {
47
- MONGO_URI: process.env.MONGO_URI!,
48
- ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
49
- },
50
- media,
51
- logger: {
52
- level: "info",
53
- prettify: true,
54
- },
55
- syncRoutes: true,
56
- });
57
- ```
58
-
59
- Options:
60
-
61
- - `literals`: text/translation catalog.
62
- - `contentTypes`: application content types.
63
- - `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
64
- - `routes`: configured routes used to resolve pages.
65
- - `apiOperations`: custom API operations added to the Rakun operation registry.
66
- - `mongo`: MongoDB connection. Required before serving Rakun requests.
67
- - `media`: media adapter/configuration. Optional.
68
- - `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
69
- - `syncRoutes`: syncs configured routes during initialization. Enabled by default.
70
-
71
- Content types can define lifecycle `hooks` and opt into manager-selected
72
- `dynamicData` sources.
73
-
74
- Routes can define fixed layout module slots. Rakun syncs those slots and the
75
- manager lets users select an existing entry for each route:
76
-
77
- ```ts
78
- rakunBootstrap({
79
- // ...
80
- routes: [
81
- {
82
- key: "pages",
83
- contentType: "Page",
84
- field: "slug",
85
- hasPage: true,
86
- dynamic: false,
87
- defaultBasePath: "",
88
- layout: [
89
- { key: "header", contentType: "Header" },
90
- { type: "content" },
91
- { key: "footer", contentType: "Footer" },
92
- ],
93
- },
94
- ],
95
- });
96
- ```
97
-
98
- The web page response includes an ordered `layout` array containing module slots
99
- plus the content slot.
100
-
101
- `ensureRakunInitialized()` prepares logger, MongoDB, media, and route syncing. It uses a singleton promise to avoid concurrent initialization; if initialization fails, the promise is cleared so the next call can retry.
102
-
103
- `ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
104
-
105
- ## Plugins
106
-
107
- Trusted server plugins contribute to the same bootstrap registry without coupling
108
- core to React:
109
-
110
- ```ts
111
- import { defineRakunPlugin, rakunBootstrap } from '@rakun-kit/core'
112
-
113
- export const analyticsPlugin = defineRakunPlugin({
114
- id: '@acme/rakun-analytics',
115
- contentTypes: [AnalyticsEvent],
116
- routes: analyticsRoutes,
117
- apiOperations: analyticsOperations,
118
- permissions: ['plugin.analytics.view'],
119
- literals: {},
120
- initialize: async ({ db }) => {
121
- // Services and migrations are ready here. Keep initialization idempotent.
122
- },
123
- })
124
-
125
- rakunBootstrap({
126
- plugins: [analyticsPlugin],
127
- contentTypes: [],
128
- literals: {},
129
- mongo,
130
- })
131
- ```
132
-
133
- Plugin ids and contributed content types, routes, operations, literals, and
134
- custom field editor ids must be unique. Rakun reports both owners on conflicts.
135
- Visual manager and web facets are registered separately in their browser
136
- runtimes.
137
-
138
- Custom field factories can use `createPluginField`. Their serializable
139
- `meta.editor` must match a field declaration in the server plugin and a React
140
- editor registered by its manager facet.
141
-
142
- ## Content Types
143
-
144
- A `ContentType` defines a logical collection:
145
-
146
- ```ts
147
- const Post = new ContentType({
148
- name: "Post",
149
- comments: true,
150
- menu: {
151
- title: "Posts",
152
- icon: "newspaper",
153
- category: "Content",
154
- },
155
- fields: {
156
- title: Fields.string().required(),
157
- slug: Fields.string().type("Slug").required(),
158
- body: Fields.string().type("RichText"),
159
- published: Fields.boolean(),
160
- },
161
- uniques: [["slug"]],
162
- listFields: ["title", "slug", "published"],
163
- });
164
- ```
165
-
166
- Page-like content types can define ordered page modules with `iterator` outside
167
- `fields`. Rakun persists this generated field as `_iterator`:
168
-
169
- ```ts
170
- const Page = new ContentType({
171
- name: "Page",
172
- fields: {
173
- title: Fields.string().required(),
174
- slug: Fields.string().type("Slug").required(),
175
- },
176
- iterator: [{ contentType: PageSection, type: "existing" }],
177
- });
178
- ```
179
-
180
- Main properties:
181
-
182
- - `name`: stable type name. Also used as `_type`.
183
- - `fields`: field map.
184
- - `iterator`: page module entries. Generates the reserved `_iterator` field.
185
- - `menu`: manager metadata.
186
- - `uniques`: unique field groups.
187
- - `listFields`: preferred fields in lists and relations.
188
- - `hideFromManager()`: hides the content type from manager content type lists.
189
- - `apiOnly()`: applies `.apiOnly()` to every field in the content type.
190
- - `managerOnly()`: applies `.managerOnly()` to every field in the content type.
191
- - `comments: true` or `enableComments()`: enables manager document comments and user mentions for saved entries.
192
- - `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
193
- - Dynamic data bindings are available on manager-visible fields by default; use
194
- field-level `.noDynamic()` to opt out.
195
-
196
- When a content type has a configured route with `hasPage: true`, Rakun also
197
- adds an optional reserved `_seo` relation automatically.
198
-
199
- Hooks run around DB mutations and public output resolution:
200
-
201
- ```ts
202
- const User = new ContentType({
203
- name: "User",
204
- fields: {
205
- email: Fields.string().type("Email").required(),
206
- password: Fields.string().type("Password").required().managerOnly(),
207
- },
208
- }).withHooks({
209
- beforeInsert: ({ data }) => ({
210
- ...data,
211
- password: hashPassword(String(data.password)),
212
- }),
213
- onGet: ({ data }) => ({
214
- ...data,
215
- displayName: String(data.email).split("@")[0],
216
- }),
217
- });
218
- ```
219
-
220
- Dynamic data turns a content type into a reusable layout. The manager can bind
221
- manager-visible fields to another content item field, or to a generated `href`
222
- when a source content type has a page route. Fields are dynamic by default; call
223
- `.noDynamic()` on fields that should not accept bindings or be exported as
224
- source paths. Source content types are hidden by default; set
225
- `dynamicDataSource: true` on content types that should appear in the manager
226
- source selector. Field bindings can also select `Current document` to read values
227
- from the document being edited without marking its content type as a source.
228
-
229
- Source field selectors are type-aware. A string target only offers string-like
230
- source paths, number targets only offer numbers, and boolean targets only offer
231
- booleans. Object-like source fields are traversed so nested leaf fields can be
232
- selected, while reserved SEO metadata is omitted from dynamic data mappings.
233
- The generated `href` source is only shown for content types that have a
234
- configured route with `hasPage: true`.
235
-
236
- ```ts
237
- const Project = new ContentType({
238
- name: "Project",
239
- dynamicDataSource: true,
240
- fields: {
241
- title: Fields.string().required(),
242
- slug: Fields.string().type("Slug").required(),
243
- },
244
- });
245
-
246
- const Carousel = new ContentType({
247
- name: "Carousel",
248
- fields: {
249
- title: Fields.string().required(),
250
- internalNote: Fields.string().noDynamic(),
251
- items: Fields.blocks([
252
- {
253
- name: "CarouselItem",
254
- field: Fields.relation(CarouselItem, "new"),
255
- },
256
- ]),
257
- },
258
- });
259
- ```
260
-
261
- List bindings append dynamic items to manually stored items instead of replacing
262
- the list. If the same stable item id appears in both sources, the dynamically
263
- resolved item wins and the duplicate manual copy is skipped.
264
-
1
+ # @rakun-kit/core
2
+
3
+ `@rakun-kit/core` contains Rakun's shared domain model and runtime. It defines content types, fields, Zod schemas, manager/web operations, request context, MongoDB connection handling, media service, permissions, translations, and typed contracts used by adapters (`@rakun-kit/next`, `@rakun-kit/express`, `@rakun-kit/trpc`) and the manager client.
4
+
5
+ ## Main Entry
6
+
7
+ Typical imports:
8
+
9
+ ```ts
10
+ import {
11
+ ContentType,
12
+ Fields,
13
+ rakunBootstrap,
14
+ ensureRakunInitialized,
15
+ } from "@rakun-kit/core";
16
+ ```
17
+
18
+ The main entrypoint exports:
19
+
20
+ - Bootstrap: `rakunBootstrap`, `ensureRakunBootstrap`, `ensureRakunInitialized`, `getRakunBootstrapOptions`.
21
+ - Context: `createRequestContext`, `getSessionCookie`, `setSessionCookie`.
22
+ - Operations: manager/web contracts and definitions.
23
+ - Media: `createMediaService`, `getMediaService`, adapters, and types.
24
+ - Permissions, translations, errors, contracts, and public types.
25
+ - Basic internal content types: `Language`, `ManagerUser`, `Seo`.
26
+
27
+ Relevant subpaths:
28
+
29
+ - `@rakun-kit/core/manager`: manager operation metadata and types.
30
+ - `@rakun-kit/core/contracts`: shared manager/web payload schemas.
31
+ - `@rakun-kit/core/client`: client-facing types and utilities.
32
+ - `@rakun-kit/core/logger`: logger.
33
+ - `@rakun-kit/core/errors`: application errors.
34
+ - `@rakun-kit/core/types`: types derived from content types.
35
+
36
+ ## Bootstrap
37
+
38
+ `rakunBootstrap(options)` registers global configuration and content types:
39
+
40
+ ```ts
41
+ rakunBootstrap({
42
+ literals,
43
+ contentTypes: [Page, Post],
44
+ routes,
45
+ apiOperations,
46
+ mongo: {
47
+ MONGO_URI: process.env.MONGO_URI!,
48
+ ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
49
+ },
50
+ media,
51
+ logger: {
52
+ level: "info",
53
+ prettify: true,
54
+ },
55
+ syncRoutes: true,
56
+ });
57
+ ```
58
+
59
+ Options:
60
+
61
+ - `literals`: text/translation catalog.
62
+ - `contentTypes`: application content types.
63
+ - `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
64
+ - `routes`: configured routes used to resolve pages.
65
+ - `apiOperations`: custom API operations added to the Rakun operation registry.
66
+ - `mongo`: MongoDB connection. Required before serving Rakun requests.
67
+ - `media`: media adapter/configuration. Optional.
68
+ - `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
69
+ - `syncRoutes`: syncs configured routes during initialization. Enabled by default.
70
+
71
+ Content types can define lifecycle `hooks` and opt into manager-selected
72
+ `dynamicData` sources.
73
+
74
+ Routes can define fixed layout module slots. Rakun syncs those slots and the
75
+ manager lets users select an existing entry for each route:
76
+
77
+ ```ts
78
+ rakunBootstrap({
79
+ // ...
80
+ routes: [
81
+ {
82
+ key: "pages",
83
+ contentType: "Page",
84
+ field: "slug",
85
+ hasPage: true,
86
+ dynamic: false,
87
+ defaultBasePath: "",
88
+ layout: [
89
+ { key: "header", contentType: "Header" },
90
+ { type: "content" },
91
+ { key: "footer", contentType: "Footer" },
92
+ ],
93
+ },
94
+ ],
95
+ });
96
+ ```
97
+
98
+ The web page response includes an ordered `layout` array containing module slots
99
+ plus the content slot.
100
+
101
+ `ensureRakunInitialized()` prepares logger, MongoDB, media, and route syncing. It uses a singleton promise to avoid concurrent initialization; if initialization fails, the promise is cleared so the next call can retry.
102
+
103
+ `ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
104
+
105
+ ## Plugins
106
+
107
+ Trusted server plugins contribute to the same bootstrap registry without coupling
108
+ core to React:
109
+
110
+ ```ts
111
+ import { defineRakunPlugin, rakunBootstrap } from '@rakun-kit/core'
112
+
113
+ export const analyticsPlugin = defineRakunPlugin({
114
+ id: '@acme/rakun-analytics',
115
+ contentTypes: [AnalyticsEvent],
116
+ routes: analyticsRoutes,
117
+ apiOperations: analyticsOperations,
118
+ permissions: ['plugin.analytics.view'],
119
+ literals: {},
120
+ initialize: async ({ db }) => {
121
+ // Services and migrations are ready here. Keep initialization idempotent.
122
+ },
123
+ })
124
+
125
+ rakunBootstrap({
126
+ plugins: [analyticsPlugin],
127
+ contentTypes: [],
128
+ literals: {},
129
+ mongo,
130
+ })
131
+ ```
132
+
133
+ Plugin ids and contributed content types, routes, operations, literals, and
134
+ custom field editor ids must be unique. Rakun reports both owners on conflicts.
135
+ Visual manager and web facets are registered separately in their browser
136
+ runtimes.
137
+
138
+ Custom field factories can use `createPluginField`. Their serializable
139
+ `meta.editor` must match a field declaration in the server plugin and a React
140
+ editor registered by its manager facet.
141
+
142
+ ## Content Types
143
+
144
+ A `ContentType` defines a logical collection:
145
+
146
+ ```ts
147
+ const Post = new ContentType({
148
+ name: "Post",
149
+ comments: true,
150
+ menu: {
151
+ title: "Posts",
152
+ icon: "newspaper",
153
+ category: "Content",
154
+ },
155
+ fields: {
156
+ title: Fields.string().required(),
157
+ slug: Fields.string().type("Slug").required(),
158
+ body: Fields.string().type("RichText"),
159
+ published: Fields.boolean(),
160
+ },
161
+ uniques: [["slug"]],
162
+ listFields: ["title", "slug", "published"],
163
+ });
164
+ ```
165
+
166
+ Page-like content types can define ordered page modules with `iterator` outside
167
+ `fields`. Rakun persists this generated field as `_iterator`:
168
+
169
+ ```ts
170
+ const Page = new ContentType({
171
+ name: "Page",
172
+ fields: {
173
+ title: Fields.string().required(),
174
+ slug: Fields.string().type("Slug").required(),
175
+ },
176
+ iterator: [{ contentType: PageSection, type: "existing" }],
177
+ linkedIterator: true,
178
+ });
179
+ ```
180
+
181
+ Set `linkedIterator: true` when every document should use one shared module
182
+ structure. The manager stores a canonical iterator for the content type, while
183
+ dynamic bindings such as `Current document` continue to resolve against the
184
+ individual page being rendered. Documents are linked by default and can be
185
+ unlinked in the manager to keep a local copy of the iterator.
186
+
187
+ Main properties:
188
+
189
+ - `name`: stable type name. Also used as `_type`.
190
+ - `fields`: field map.
191
+ - `iterator`: page module entries. Generates the reserved `_iterator` field.
192
+ - `linkedIterator`: shares that iterator between documents while allowing
193
+ explicit per-document overrides.
194
+ - `menu`: manager metadata.
195
+ - `uniques`: unique field groups.
196
+ - `listFields`: preferred fields in lists and relations.
197
+ - `hideFromManager()`: hides the content type from manager content type lists.
198
+ - `apiOnly()`: applies `.apiOnly()` to every field in the content type.
199
+ - `managerOnly()`: applies `.managerOnly()` to every field in the content type.
200
+ - `comments: true` or `enableComments()`: enables manager document comments and user mentions for saved entries.
201
+ - `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
202
+ - Dynamic data bindings are available on manager-visible fields by default; use
203
+ field-level `.noDynamic()` to opt out.
204
+
205
+ When a content type has a configured route with `hasPage: true`, Rakun also
206
+ adds an optional reserved `_seo` relation automatically.
207
+
208
+ Hooks run around DB mutations and public output resolution:
209
+
210
+ ```ts
211
+ const User = new ContentType({
212
+ name: "User",
213
+ fields: {
214
+ email: Fields.string().type("Email").required(),
215
+ password: Fields.string().type("Password").required().managerOnly(),
216
+ },
217
+ }).withHooks({
218
+ beforeInsert: ({ data }) => ({
219
+ ...data,
220
+ password: hashPassword(String(data.password)),
221
+ }),
222
+ onGet: ({ data }) => ({
223
+ ...data,
224
+ displayName: String(data.email).split("@")[0],
225
+ }),
226
+ });
227
+ ```
228
+
229
+ Dynamic data turns a content type into a reusable layout. The manager can bind
230
+ manager-visible fields to another content item field, or to a generated `href`
231
+ when a source content type has a page route. Fields are dynamic by default; call
232
+ `.noDynamic()` on fields that should not accept bindings or be exported as
233
+ source paths. Source content types are hidden by default; set
234
+ `dynamicDataSource: true` on content types that should appear in the manager
235
+ source selector. Field bindings can also select `Current document` to read values
236
+ from the document being edited without marking its content type as a source.
237
+
238
+ Source field selectors are type-aware. A string target only offers string-like
239
+ source paths, number targets only offer numbers, and boolean targets only offer
240
+ booleans. Object-like source fields are traversed so nested leaf fields can be
241
+ selected, while reserved SEO metadata is omitted from dynamic data mappings.
242
+ The generated `href` source is only shown for content types that have a
243
+ configured route with `hasPage: true`.
244
+
245
+ ```ts
246
+ const Project = new ContentType({
247
+ name: "Project",
248
+ dynamicDataSource: true,
249
+ fields: {
250
+ title: Fields.string().required(),
251
+ slug: Fields.string().type("Slug").required(),
252
+ },
253
+ });
254
+
255
+ const Carousel = new ContentType({
256
+ name: "Carousel",
257
+ fields: {
258
+ title: Fields.string().required(),
259
+ internalNote: Fields.string().noDynamic(),
260
+ items: Fields.blocks([
261
+ {
262
+ name: "CarouselItem",
263
+ field: Fields.relation(CarouselItem, "new"),
264
+ },
265
+ ]),
266
+ },
267
+ });
268
+ ```
269
+
270
+ List bindings append dynamic items to manually stored items instead of replacing
271
+ the list. If the same stable item id appears in both sources, the dynamically
272
+ resolved item wins and the duplicate manual copy is skipped.
273
+
274
+ A list inside a nested module can also use a compatible relation or blocks array
275
+ from the current document. The manager exposes these sources as
276
+ `Current document · <field>`, infers the content type of each array item, and
277
+ lets each item be mapped with the same field mapping UI used for collections.
278
+ These bindings store a `currentDocument` source alongside the inferred item
279
+ content type:
280
+
281
+ ```ts
282
+ const headerBindings = {
283
+ lists: {
284
+ categories: {
285
+ contentType: LinkItem.name,
286
+ source: {
287
+ kind: "currentDocument",
288
+ contentType: Project.name,
289
+ path: "categories",
290
+ itemName: "Category",
291
+ },
292
+ itemName: "Category",
293
+ map: {
294
+ title: { contentType: LinkItem.name, path: "title" },
295
+ href: { contentType: LinkItem.name, path: "href" },
296
+ },
297
+ },
298
+ },
299
+ };
300
+ ```
301
+
265
302
  List query conditions can compare a source field with a value from the current
266
- document. The manager exposes this as `Current document`; programmatic bindings
267
- use `{ $current: "path.to.field" }`. For example, a category can query only the
268
- projects whose related category has the same slug:
269
-
270
- ```ts
271
- query: {
272
- filter: {
273
- "category.slug": { $current: "slug" },
274
- },
275
- options: { limit: 10 },
276
- }
277
- ```
278
-
279
- Current-document paths are checked against the content type's dynamic field
280
- rules before the query runs. `_id` is also available for relation queries.
281
-
303
+ document. The manager exposes this as `Current document`; programmatic bindings
304
+ use `{ $current: "path.to.field" }`. For example, a category can query only the
305
+ projects whose related category has the same slug:
306
+
307
+ ```ts
308
+ query: {
309
+ filter: {
310
+ "category.slug": { $current: "slug" },
311
+ },
312
+ options: { limit: 10 },
313
+ }
314
+ ```
315
+
316
+ Current-document paths are checked against the content type's dynamic field
317
+ rules before the query runs. `_id` is also available for relation queries.
318
+
282
319
  A list mapping can also collect an array through a reverse relation without
283
- persisting that relation on the source document. For example, a category gallery
284
- can create one item per `Category` and collect the images of its related
285
- `Project` documents:
286
-
287
- ```ts
288
- const Category = new ContentType({
289
- name: "Category",
290
- dynamicDataSource: true,
291
- fields: {
292
- title: Fields.string().required(),
293
- },
294
- });
295
-
296
- const Project = new ContentType({
297
- name: "Project",
298
- dynamicDataSource: true,
299
- fields: {
300
- category: Fields.relation(Category, "existing").required(),
301
- images: Fields.file().type("Image").multiple().required(),
302
- },
303
- });
304
-
305
- const galleryBindings = {
306
- lists: {
307
- items: {
308
- contentType: Category.name,
309
- itemName: "CategoriesGalleryItem",
310
- map: {
311
- title: { contentType: Category.name, path: "title" },
312
- images: {
313
- kind: "relatedCollection",
314
- contentType: Project.name,
315
- relation: "category",
316
- path: "images",
317
- limit: 10,
318
- },
319
- },
320
- },
321
- },
322
- };
323
- ```
324
-
325
- The related collection query matches `Project.category._id` against the current
326
- category, preserves project and image order, and flattens the selected array by
327
- one level. Its numeric limit applies to related projects and is capped at 100.
328
-
329
- Schema and validation methods:
330
-
331
- - `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
332
- - `getSchema()`: persisted schema.
333
- - `getPopulatedSchema()`: output with populated relations and `_id`.
334
- - `getOutputSchema()`: public API output.
335
- - `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
336
- - `validate`, `partialValidate`, `validateOutput`: Zod helpers.
337
-
338
- The registry lives in `lib/Registry`:
339
-
340
- - `registerContentType`, `registerInternalContentType`.
341
- - `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
342
- - `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
343
- - `getContentTypeByName`.
344
-
345
- ## Fields
346
-
347
- Main factory:
348
-
349
- ```ts
350
- Fields.string();
351
- Fields.number();
352
- Fields.boolean();
353
- Fields.date();
354
- Fields.select(["draft", "published"]);
355
- Fields.relation(Post);
356
- Fields.contentReference("Post");
357
- Fields.selfRelation();
358
- Fields.blocks([{ name: "title", field: Fields.string() }]);
359
- Fields.array(Fields.string());
360
- Fields.link();
361
- Fields.file();
362
- ```
363
-
364
- Common modifiers:
365
-
366
- - `.required()`: marks field as required.
367
- - `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
368
- - `.apiOnly()`: available for API/persistence, hidden from manager.
369
- - `.managerOnly()`: visible to manager, excluded from API output.
370
-
371
- Notable fields:
372
-
373
- - `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
374
- - `NumberField`: supports `.min()` and `.max()`.
375
- - `RelationField`: relation to another `ContentType`; accepts existing references or inline creation. `Fields.relation(Post, "existing")` restricts to existing records; `"new"` restricts to new records. `.multiple()` returns a homogeneous array of relations.
376
- - `ContentReferenceField`: reference by content type name.
377
- - `FileField`: integrates media and optimization options.
378
- - `Fields.blocks(...)`: heterogeneous ordered list. Each item stores a `name` and a `value`, and the value can match one of the named field shapes. Use it for block-like content where different item types can appear in the same list.
379
- - `Fields.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
380
- - `IteratorField`: repeatable structure based on content type entries.
381
-
382
- ## Derived Types
383
-
384
- `@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
385
-
386
- - `DataInput<T>`: accepted shape for create/update.
387
- - `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
388
- - `DataPopulated<T>`: shape with populated relations.
389
- - `DataFront<T>`: public output without `apiOnly` fields.
390
- - `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
391
- - `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
392
-
393
- ## ORM and Mongo
394
-
395
- `core/src/orm` implements `DBService` on top of MongoDB:
396
-
397
- ```ts
398
- const db = await getMongoService();
399
-
400
- const post = await db.create(Post, {
401
- _type: "Post",
402
- title: "Hello",
403
- slug: "hello",
404
- });
405
- ```
406
-
407
- Operations:
408
-
409
- - `get(contentType, id, fields?)`
410
- - `list(contentType, query)`
411
- - `create(contentType, data)`
412
- - `update(contentType, id, data)`
413
- - `updateMany(contentType, filter, data)`
414
- - `delete(contentType, filter)`
415
- - `find(contentType, filter, fields?)`
416
- - `clear(contentType)`
417
- - `findDependencies(contentType, id)`
418
- - `upsert(contentType, filter, data)`
419
- - `getAll(contentType, query?)`
420
-
421
- Connection:
422
-
423
- - `createMongoConnection(config)`: stores config.
424
- - `createMongoService(config)`: connects and creates handlers.
425
- - `getMongoService()`: returns the singleton or creates it from config.
426
- - `closeMongoService()`: closes the connection and clears the singleton.
427
-
428
- `MongoConfig`:
429
-
430
- ```ts
431
- type MongoConfig = {
432
- MONGO_URI: string;
433
- ENVIRONMENT?: "local" | "development" | "test" | "production";
434
- };
435
- ```
436
-
437
- In environments other than `test`, the connection creates indexes defined by `createIndexes`.
438
-
439
- ## API Operations
440
-
441
- `api/operations` defines typed contracts and handlers for endpoints:
442
-
443
- - Manager: CRUD, auth, MFA, media, literals, settings.
444
- - Manager comments: `manager.comments.list`, `manager.comments.create`, and `manager.users.mentions`.
445
- - Web: page resolution.
446
-
447
- Main helpers:
448
-
449
- - `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
450
- - `defineOperation`: combines a contract with its implementation.
451
- - `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
452
- - `GetClient`: derives a typed client from an operation map.
453
- - `createManagerOperationContracts`, `createWebOperationContracts`.
454
- - `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
455
- - `createRakunOperationDefinitions`: combines manager + web.
456
- - `createOperationManifest`: serializable metadata for clients.
457
- - `createOperationPath`: HTTP path from operation name.
458
- - `mergeOperationContracts`: combines maps.
459
-
460
- `@rakun-kit/core/manager` exposes the manifest and name-based types:
461
-
462
- ```ts
463
- import {
464
- managerOperationManifest,
465
- getManagerOperationMeta,
466
- type ManagerOperationInput,
467
- type ManagerOperationOutput,
468
- } from "@rakun-kit/core/manager";
469
- ```
470
-
471
- ### Custom Operations
472
-
473
- Applications can define operations in a separate object, pass that object to
474
- bootstrap, and reuse its type on the frontend:
475
-
476
- ```ts
477
- // server/api-operations.ts
478
- import { defineOperation } from "@rakun-kit/core";
479
- import { z } from "zod";
480
-
481
- export const apiOperations = {
482
- "demo.helloWorld": defineOperation<
483
- { text: string },
484
- { message: string },
485
- "query",
486
- "get",
487
- "public"
488
- >({
489
- access: "public",
490
- kind: "query",
491
- method: "get",
492
- description: "Return a hello world message with the provided text",
493
- input: z.object({
494
- text: z.string().default("world"),
495
- }),
496
- output: z.object({
497
- message: z.string(),
498
- }),
499
- resolve: ({ input }) => ({
500
- message: `Hello ${input.text}`,
501
- }),
502
- }),
503
- };
504
- ```
505
-
506
- ```ts
507
- // bootstrap
508
- import { apiOperations } from "./server/api-operations";
509
-
510
- rakunBootstrap({
511
- // ...
512
- apiOperations,
513
- });
514
- ```
515
-
516
- Operation names define their HTTP path: `demo.helloWorld` becomes
517
- `/demo/helloWorld`.
518
-
519
- Rules:
520
-
521
- - Names must not collide with built-in operations.
522
- - Operations prefixed with `manager.` are included in manager operation routers.
523
- - Operations prefixed with `web.` are included in web operation routers.
524
- - Operations with any other prefix are included when using the combined operation registry.
525
- - Use `access: "public"` for operations called from public web pages.
526
- - Use `access: "auth"` for operations that require a manager session.
527
-
528
- The manager route `manager.apiOperations` returns a JSON-serializable operation
529
- catalog for manager API docs/playgrounds. Input and output Zod schemas are
530
- converted to JSON Schema for display.
531
-
532
- ### Typed API Client
533
-
534
- `@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
535
-
536
- ```ts
537
- import {
538
- createRakunApiClient,
539
- type GetClient,
540
- } from "@rakun-kit/core/web";
541
- import type { apiOperations } from "./server/api-operations";
542
-
543
- type ApiClient = GetClient<typeof apiOperations>;
544
-
545
- const client: ApiClient = createRakunApiClient<typeof apiOperations>({
546
- baseUrl: "/api",
547
- });
548
-
549
- const result = await client.query("demo.helloWorld", {
550
- text: "Rakun",
551
- });
552
-
553
- result.message;
554
- ```
555
-
556
- The client exposes:
557
-
558
- - `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
559
- - `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
560
-
561
- The client derives input and output types from the Zod schemas in
562
- `apiOperations`.
563
-
564
- ## Request Context and Auth
565
-
566
- `createRequestContext(input)` normalizes headers, cookies, and response:
567
-
568
- ```ts
569
- const ctx = await createRequestContext({
570
- headers: req.headers,
571
- cookies,
572
- res: {
573
- setHeader: res.setHeader.bind(res),
574
- cookie: res.cookie.bind(res),
575
- },
576
- });
577
- ```
578
-
579
- The resulting context includes:
580
-
581
- - `req.headers`, `req.cookies`
582
- - `res.setHeader`, `res.cookie`
583
- - `user`: manager user or `null`
584
- - `getUser()`: returns the user or throws `AUTH_REQUIRED`
585
-
586
- Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
587
-
588
- ## Media
589
-
590
- Media uses a storage adapter:
591
-
592
- ```ts
593
- createMediaService({
594
- adapter,
595
- defaultAccess: "private",
596
- defaultGetExpiresInSeconds: 300,
597
- uploadUrl: "/api/rakun/manager/media/upload",
598
- });
599
- ```
600
-
601
- APIs:
602
-
603
- - `createMediaConnection(config)`: stores config without creating the service.
604
- - `createMediaService(config)`: creates the singleton with an adapter.
605
- - `getMediaService()`: returns the singleton or creates it from config.
606
- - `handleMediaBinaryUpload`: processes manager binary uploads.
607
-
608
- The service supports prepare/finalize upload, URL generation, folders, and image optimization depending on adapter/configuration.
609
-
610
- ## Literals and Translation
611
-
612
- Bootstrap receives `literals`. Related utilities:
613
-
614
- - `getTranslation`: resolves translatable values.
615
- - `translateObject`: translates objects with translatable fields.
616
- - `managerLiterals`: base manager texts.
617
- - Manager schemas for listing/upserting literals.
618
-
619
- Translatable values use this shape:
620
-
621
- ```ts
622
- {
623
- _tag: "Translatable",
624
- en: "Title",
625
- es: "Titulo"
626
- }
627
- ```
628
-
629
- ## Permissions
630
-
631
- `lib/Permissions` exposes:
632
-
633
- - `getPermissionList`: generates a permission list.
634
- - `hasPermissions`: validates user/role permissions.
635
- - `mapPermissions`: transforms permissions.
636
- - `Permission`: public type.
637
-
638
- Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
639
-
640
- ## Errors
641
-
642
- Application errors live in `lib/errors`:
643
-
644
- - `AppError`
645
- - `throwAppError`
646
- - `errors`
647
- - `instanceofAppErrorShape`
648
-
649
- Database errors live in `orm/dbService`:
650
-
651
- - `DbError`
652
- - `DbErrorUnknown`
653
- - `DbErrorNotFound`
654
- - `DbErrorInvalidData`
655
- - `DbErrorConflict`
656
- - `DbErrorSimulatedFailure`
657
-
658
- ## Runtime Flow
659
-
660
- 1. The app defines content types with `ContentType` and `Fields`.
661
- 2. The app calls `rakunBootstrap(options)`.
662
- 3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
663
- 4. `ensureRakunInitialized()` configures logger, MongoDB, media, and route syncing.
664
- 5. Each request creates a `RakunRequestContext`.
665
- 6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
666
-
667
- ## Tests and Development Notes
668
-
669
- The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
670
-
671
- Package build:
672
-
673
- ```sh
674
- bun run build --workspace @rakun-kit/core
675
- ```
676
-
677
- The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.
320
+ persisting that relation on the source document. For example, a category gallery
321
+ can create one item per `Category` and collect the images of its related
322
+ `Project` documents:
323
+
324
+ ```ts
325
+ const Category = new ContentType({
326
+ name: "Category",
327
+ dynamicDataSource: true,
328
+ fields: {
329
+ title: Fields.string().required(),
330
+ },
331
+ });
332
+
333
+ const Project = new ContentType({
334
+ name: "Project",
335
+ dynamicDataSource: true,
336
+ fields: {
337
+ category: Fields.relation(Category, "existing").required(),
338
+ images: Fields.file().type("Image").multiple().required(),
339
+ },
340
+ });
341
+
342
+ const galleryBindings = {
343
+ lists: {
344
+ items: {
345
+ contentType: Category.name,
346
+ itemName: "CategoriesGalleryItem",
347
+ map: {
348
+ title: { contentType: Category.name, path: "title" },
349
+ images: {
350
+ kind: "relatedCollection",
351
+ contentType: Project.name,
352
+ relation: "category",
353
+ path: "images",
354
+ limit: 10,
355
+ },
356
+ },
357
+ },
358
+ },
359
+ };
360
+ ```
361
+
362
+ The related collection query matches `Project.category._id` against the current
363
+ category, preserves project and image order, and flattens the selected array by
364
+ one level. Its numeric limit applies to related projects and is capped at 100.
365
+
366
+ Schema and validation methods:
367
+
368
+ - `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
369
+ - `getSchema()`: persisted schema.
370
+ - `getPopulatedSchema()`: output with populated relations and `_id`.
371
+ - `getOutputSchema()`: public API output.
372
+ - `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
373
+ - `validate`, `partialValidate`, `validateOutput`: Zod helpers.
374
+
375
+ The registry lives in `lib/Registry`:
376
+
377
+ - `registerContentType`, `registerInternalContentType`.
378
+ - `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
379
+ - `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
380
+ - `getContentTypeByName`.
381
+
382
+ ## Fields
383
+
384
+ Main factory:
385
+
386
+ ```ts
387
+ Fields.string();
388
+ Fields.number();
389
+ Fields.boolean();
390
+ Fields.date();
391
+ Fields.select(["draft", "published"]);
392
+ Fields.relation(Post);
393
+ Fields.contentReference("Post");
394
+ Fields.selfRelation();
395
+ Fields.blocks([{ name: "title", field: Fields.string() }]);
396
+ Fields.array(Fields.string());
397
+ Fields.link();
398
+ Fields.file();
399
+ ```
400
+
401
+ Common modifiers:
402
+
403
+ - `.required()`: marks field as required.
404
+ - `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
405
+ - `.apiOnly()`: available for API/persistence, hidden from manager.
406
+ - `.managerOnly()`: visible to manager, excluded from API output.
407
+
408
+ Notable fields:
409
+
410
+ - `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
411
+ - `NumberField`: supports `.min()` and `.max()`.
412
+ - `RelationField`: relation to another `ContentType`; accepts existing references or inline creation. `Fields.relation(Post, "existing")` restricts to existing records; `"new"` restricts to new records. `.multiple()` returns a homogeneous array of relations.
413
+ - `ContentReferenceField`: reference by content type name.
414
+ - `FileField`: integrates media and optimization options.
415
+ - `Fields.blocks(...)`: heterogeneous ordered list. Each item stores a `name` and a `value`, and the value can match one of the named field shapes. Use it for block-like content where different item types can appear in the same list.
416
+ - `Fields.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
417
+ - `IteratorField`: repeatable structure based on content type entries.
418
+
419
+ ## Derived Types
420
+
421
+ `@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
422
+
423
+ - `DataInput<T>`: accepted shape for create/update.
424
+ - `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
425
+ - `DataPopulated<T>`: shape with populated relations.
426
+ - `DataFront<T>`: public output without `apiOnly` fields.
427
+ - `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
428
+ - `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
429
+
430
+ ## ORM and Mongo
431
+
432
+ `core/src/orm` implements `DBService` on top of MongoDB:
433
+
434
+ ```ts
435
+ const db = await getMongoService();
436
+
437
+ const post = await db.create(Post, {
438
+ _type: "Post",
439
+ title: "Hello",
440
+ slug: "hello",
441
+ });
442
+ ```
443
+
444
+ Operations:
445
+
446
+ - `get(contentType, id, fields?)`
447
+ - `list(contentType, query)`
448
+ - `create(contentType, data)`
449
+ - `update(contentType, id, data)`
450
+ - `updateMany(contentType, filter, data)`
451
+ - `delete(contentType, filter)`
452
+ - `find(contentType, filter, fields?)`
453
+ - `clear(contentType)`
454
+ - `findDependencies(contentType, id)`
455
+ - `upsert(contentType, filter, data)`
456
+ - `getAll(contentType, query?)`
457
+
458
+ Connection:
459
+
460
+ - `createMongoConnection(config)`: stores config.
461
+ - `createMongoService(config)`: connects and creates handlers.
462
+ - `getMongoService()`: returns the singleton or creates it from config.
463
+ - `closeMongoService()`: closes the connection and clears the singleton.
464
+
465
+ `MongoConfig`:
466
+
467
+ ```ts
468
+ type MongoConfig = {
469
+ MONGO_URI: string;
470
+ ENVIRONMENT?: "local" | "development" | "test" | "production";
471
+ };
472
+ ```
473
+
474
+ In environments other than `test`, the connection creates indexes defined by `createIndexes`.
475
+
476
+ ## API Operations
477
+
478
+ `api/operations` defines typed contracts and handlers for endpoints:
479
+
480
+ - Manager: CRUD, auth, MFA, media, literals, settings.
481
+ - Manager comments: `manager.comments.list`, `manager.comments.create`, and `manager.users.mentions`.
482
+ - Web: page resolution.
483
+
484
+ Main helpers:
485
+
486
+ - `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
487
+ - `defineOperation`: combines a contract with its implementation.
488
+ - `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
489
+ - `GetClient`: derives a typed client from an operation map.
490
+ - `createManagerOperationContracts`, `createWebOperationContracts`.
491
+ - `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
492
+ - `createRakunOperationDefinitions`: combines manager + web.
493
+ - `createOperationManifest`: serializable metadata for clients.
494
+ - `createOperationPath`: HTTP path from operation name.
495
+ - `mergeOperationContracts`: combines maps.
496
+
497
+ `@rakun-kit/core/manager` exposes the manifest and name-based types:
498
+
499
+ ```ts
500
+ import {
501
+ managerOperationManifest,
502
+ getManagerOperationMeta,
503
+ type ManagerOperationInput,
504
+ type ManagerOperationOutput,
505
+ } from "@rakun-kit/core/manager";
506
+ ```
507
+
508
+ ### Custom Operations
509
+
510
+ Applications can define operations in a separate object, pass that object to
511
+ bootstrap, and reuse its type on the frontend:
512
+
513
+ ```ts
514
+ // server/api-operations.ts
515
+ import { defineOperation } from "@rakun-kit/core";
516
+ import { z } from "zod";
517
+
518
+ export const apiOperations = {
519
+ "demo.helloWorld": defineOperation<
520
+ { text: string },
521
+ { message: string },
522
+ "query",
523
+ "get",
524
+ "public"
525
+ >({
526
+ access: "public",
527
+ kind: "query",
528
+ method: "get",
529
+ description: "Return a hello world message with the provided text",
530
+ input: z.object({
531
+ text: z.string().default("world"),
532
+ }),
533
+ output: z.object({
534
+ message: z.string(),
535
+ }),
536
+ resolve: ({ input }) => ({
537
+ message: `Hello ${input.text}`,
538
+ }),
539
+ }),
540
+ };
541
+ ```
542
+
543
+ ```ts
544
+ // bootstrap
545
+ import { apiOperations } from "./server/api-operations";
546
+
547
+ rakunBootstrap({
548
+ // ...
549
+ apiOperations,
550
+ });
551
+ ```
552
+
553
+ Operation names define their HTTP path: `demo.helloWorld` becomes
554
+ `/demo/helloWorld`.
555
+
556
+ Rules:
557
+
558
+ - Names must not collide with built-in operations.
559
+ - Operations prefixed with `manager.` are included in manager operation routers.
560
+ - Operations prefixed with `web.` are included in web operation routers.
561
+ - Operations with any other prefix are included when using the combined operation registry.
562
+ - Use `access: "public"` for operations called from public web pages.
563
+ - Use `access: "auth"` for operations that require a manager session.
564
+
565
+ The manager route `manager.apiOperations` returns a JSON-serializable operation
566
+ catalog for manager API docs/playgrounds. Input and output Zod schemas are
567
+ converted to JSON Schema for display.
568
+
569
+ ### Typed API Client
570
+
571
+ `@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
572
+
573
+ ```ts
574
+ import {
575
+ createRakunApiClient,
576
+ type GetClient,
577
+ } from "@rakun-kit/core/web";
578
+ import type { apiOperations } from "./server/api-operations";
579
+
580
+ type ApiClient = GetClient<typeof apiOperations>;
581
+
582
+ const client: ApiClient = createRakunApiClient<typeof apiOperations>({
583
+ baseUrl: "/api",
584
+ });
585
+
586
+ const result = await client.query("demo.helloWorld", {
587
+ text: "Rakun",
588
+ });
589
+
590
+ result.message;
591
+ ```
592
+
593
+ The client exposes:
594
+
595
+ - `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
596
+ - `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
597
+
598
+ The client derives input and output types from the Zod schemas in
599
+ `apiOperations`.
600
+
601
+ ## Request Context and Auth
602
+
603
+ `createRequestContext(input)` normalizes headers, cookies, and response:
604
+
605
+ ```ts
606
+ const ctx = await createRequestContext({
607
+ headers: req.headers,
608
+ cookies,
609
+ res: {
610
+ setHeader: res.setHeader.bind(res),
611
+ cookie: res.cookie.bind(res),
612
+ },
613
+ });
614
+ ```
615
+
616
+ The resulting context includes:
617
+
618
+ - `req.headers`, `req.cookies`
619
+ - `res.setHeader`, `res.cookie`
620
+ - `user`: manager user or `null`
621
+ - `getUser()`: returns the user or throws `AUTH_REQUIRED`
622
+
623
+ Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
624
+
625
+ ## Media
626
+
627
+ Media uses a storage adapter:
628
+
629
+ ```ts
630
+ createMediaService({
631
+ adapter,
632
+ defaultAccess: "private",
633
+ defaultGetExpiresInSeconds: 300,
634
+ uploadUrl: "/api/rakun/manager/media/upload",
635
+ });
636
+ ```
637
+
638
+ APIs:
639
+
640
+ - `createMediaConnection(config)`: stores config without creating the service.
641
+ - `createMediaService(config)`: creates the singleton with an adapter.
642
+ - `getMediaService()`: returns the singleton or creates it from config.
643
+ - `handleMediaBinaryUpload`: processes manager binary uploads.
644
+
645
+ The service supports prepare/finalize upload, URL generation, folders, and image optimization depending on adapter/configuration.
646
+
647
+ ## Literals and Translation
648
+
649
+ Bootstrap receives `literals`. Related utilities:
650
+
651
+ - `getTranslation`: resolves translatable values.
652
+ - `translateObject`: translates objects with translatable fields.
653
+ - `managerLiterals`: base manager texts.
654
+ - Manager schemas for listing/upserting literals.
655
+
656
+ Translatable values use this shape:
657
+
658
+ ```ts
659
+ {
660
+ _tag: "Translatable",
661
+ en: "Title",
662
+ es: "Titulo"
663
+ }
664
+ ```
665
+
666
+ ## Permissions
667
+
668
+ `lib/Permissions` exposes:
669
+
670
+ - `getPermissionList`: generates a permission list.
671
+ - `hasPermissions`: validates user/role permissions.
672
+ - `mapPermissions`: transforms permissions.
673
+ - `Permission`: public type.
674
+
675
+ Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
676
+
677
+ ## Errors
678
+
679
+ Application errors live in `lib/errors`:
680
+
681
+ - `AppError`
682
+ - `throwAppError`
683
+ - `errors`
684
+ - `instanceofAppErrorShape`
685
+
686
+ Database errors live in `orm/dbService`:
687
+
688
+ - `DbError`
689
+ - `DbErrorUnknown`
690
+ - `DbErrorNotFound`
691
+ - `DbErrorInvalidData`
692
+ - `DbErrorConflict`
693
+ - `DbErrorSimulatedFailure`
694
+
695
+ ## Runtime Flow
696
+
697
+ 1. The app defines content types with `ContentType` and `Fields`.
698
+ 2. The app calls `rakunBootstrap(options)`.
699
+ 3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
700
+ 4. `ensureRakunInitialized()` configures logger, MongoDB, media, and route syncing.
701
+ 5. Each request creates a `RakunRequestContext`.
702
+ 6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
703
+
704
+ ## Tests and Development Notes
705
+
706
+ The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
707
+
708
+ Package build:
709
+
710
+ ```sh
711
+ bun run build --workspace @rakun-kit/core
712
+ ```
713
+
714
+ The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.