@rakun-kit/core 1.5.6 → 1.5.8
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.
- package/README.md +1071 -1067
- package/dist/cjs/api/routes/manager/media/deleteMediaStorage.js +11 -0
- package/dist/cjs/api/routes/manager/media/deleteMediaStorage.js.map +1 -1
- package/dist/cjs/api/routes/manager/media/finalizeUpload.js +4 -1
- package/dist/cjs/api/routes/manager/media/finalizeUpload.js.map +1 -1
- package/dist/cjs/api/routes/manager/media/reimportMedia.js +7 -7
- package/dist/cjs/api/routes/manager/media/reimportMedia.js.map +1 -1
- package/dist/cjs/api/routes/manager/media/resolveMediaRecordUrls.js +2 -0
- package/dist/cjs/api/routes/manager/media/resolveMediaRecordUrls.js.map +1 -1
- package/dist/cjs/api/routes/manager/media/uploadBinary.js +1 -18
- package/dist/cjs/api/routes/manager/media/uploadBinary.js.map +1 -1
- package/dist/cjs/api/utils/dynamicData.js +20 -5
- package/dist/cjs/api/utils/dynamicData.js.map +1 -1
- package/dist/cjs/api/utils/mediaUploadKey.js +19 -0
- package/dist/cjs/api/utils/mediaUploadKey.js.map +1 -0
- package/dist/cjs/api/utils/populates/populateRelations.js +10 -8
- package/dist/cjs/api/utils/populates/populateRelations.js.map +1 -1
- package/dist/cjs/auth/passwordResetMailTemplate.js +59 -59
- package/dist/cjs/client.js +3 -1
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/dynamicData.js +7 -1
- package/dist/cjs/lib/dynamicData.js.map +1 -1
- package/dist/cjs/lib/fields/File.js +2 -2
- package/dist/cjs/lib/fields/File.js.map +1 -1
- package/dist/cjs/media/imageOptimization.js +27 -16
- package/dist/cjs/media/imageOptimization.js.map +1 -1
- package/dist/cjs/schemas/manager/media/finalizeUpload.js +1 -0
- package/dist/cjs/schemas/manager/media/finalizeUpload.js.map +1 -1
- package/dist/docs/index.md +7 -3
- package/dist/esm/api/operations/index.d.ts +1 -0
- package/dist/esm/api/operations/index.d.ts.map +1 -1
- package/dist/esm/api/operations/manager-contract.d.ts +1 -0
- package/dist/esm/api/operations/manager-contract.d.ts.map +1 -1
- package/dist/esm/api/operations/manager.d.ts +1 -0
- package/dist/esm/api/operations/manager.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/deleteMediaStorage.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/deleteMediaStorage.js +11 -0
- package/dist/esm/api/routes/manager/media/deleteMediaStorage.js.map +1 -1
- package/dist/esm/api/routes/manager/media/finalizeUpload.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/finalizeUpload.js +4 -1
- package/dist/esm/api/routes/manager/media/finalizeUpload.js.map +1 -1
- package/dist/esm/api/routes/manager/media/reimportMedia.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/reimportMedia.js +7 -7
- package/dist/esm/api/routes/manager/media/reimportMedia.js.map +1 -1
- package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.js +2 -0
- package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.js.map +1 -1
- package/dist/esm/api/routes/manager/media/uploadBinary.d.ts.map +1 -1
- package/dist/esm/api/routes/manager/media/uploadBinary.js +1 -18
- package/dist/esm/api/routes/manager/media/uploadBinary.js.map +1 -1
- package/dist/esm/api/utils/dynamicData.d.ts.map +1 -1
- package/dist/esm/api/utils/dynamicData.js +21 -6
- package/dist/esm/api/utils/dynamicData.js.map +1 -1
- package/dist/esm/api/utils/mediaUploadKey.d.ts +3 -0
- package/dist/esm/api/utils/mediaUploadKey.d.ts.map +1 -0
- package/dist/esm/api/utils/mediaUploadKey.js +12 -0
- package/dist/esm/api/utils/mediaUploadKey.js.map +1 -0
- package/dist/esm/api/utils/populates/populateLinks.d.ts +2 -2
- package/dist/esm/api/utils/populates/populateLinks.d.ts.map +1 -1
- package/dist/esm/api/utils/populates/populateRelations.d.ts +2 -2
- package/dist/esm/api/utils/populates/populateRelations.d.ts.map +1 -1
- package/dist/esm/api/utils/populates/populateRelations.js +10 -8
- package/dist/esm/api/utils/populates/populateRelations.js.map +1 -1
- package/dist/esm/auth/passwordResetMailTemplate.js +59 -59
- package/dist/esm/client.d.ts +2 -2
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal-content-types/Backup.d.ts.map +1 -1
- package/dist/esm/internal-content-types/ContentReview.d.ts.map +1 -1
- package/dist/esm/internal-content-types/ContentReviewPolicy.d.ts.map +1 -1
- package/dist/esm/internal-content-types/ContentTemplate.d.ts.map +1 -1
- package/dist/esm/internal-content-types/ContentVersion.d.ts.map +1 -1
- package/dist/esm/internal-content-types/LoginIpBlock.d.ts.map +1 -1
- package/dist/esm/internal-content-types/MfaChallenge.d.ts.map +1 -1
- package/dist/esm/internal-content-types/Migration.d.ts.map +1 -1
- package/dist/esm/internal-content-types/RobotsRule.d.ts.map +1 -1
- package/dist/esm/internal-content-types/Route.d.ts.map +1 -1
- package/dist/esm/internal-content-types/RouteLayoutModule.d.ts.map +1 -1
- package/dist/esm/internal-content-types/SchemaState.d.ts.map +1 -1
- package/dist/esm/internal-content-types/WebAuthnCredential.d.ts.map +1 -1
- package/dist/esm/lib/dynamicData.d.ts +5 -0
- package/dist/esm/lib/dynamicData.d.ts.map +1 -1
- package/dist/esm/lib/dynamicData.js +6 -0
- package/dist/esm/lib/dynamicData.js.map +1 -1
- package/dist/esm/lib/fields/File.js +2 -2
- package/dist/esm/lib/fields/File.js.map +1 -1
- package/dist/esm/media/imageOptimization.d.ts +1 -3
- package/dist/esm/media/imageOptimization.d.ts.map +1 -1
- package/dist/esm/media/imageOptimization.js +27 -16
- package/dist/esm/media/imageOptimization.js.map +1 -1
- package/dist/esm/schemas/manager/media/finalizeUpload.d.ts +1 -0
- package/dist/esm/schemas/manager/media/finalizeUpload.d.ts.map +1 -1
- package/dist/esm/schemas/manager/media/finalizeUpload.js +1 -0
- package/dist/esm/schemas/manager/media/finalizeUpload.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,382 +1,382 @@
|
|
|
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
|
-
f,
|
|
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
|
-
- Mail: `createMailService`, `sendMail`, typed template registries, adapters, and types.
|
|
25
|
-
- Permissions, translations, errors, contracts, and public types.
|
|
26
|
-
- Basic internal content types: `Language`, `ManagerUser`, `Seo`.
|
|
27
|
-
|
|
28
|
-
Relevant subpaths:
|
|
29
|
-
|
|
30
|
-
- `@rakun-kit/core/manager`: manager operation metadata and types.
|
|
31
|
-
- `@rakun-kit/core/contracts`: shared manager/web payload schemas.
|
|
32
|
-
- `@rakun-kit/core/client`: client-facing types and utilities.
|
|
33
|
-
- `@rakun-kit/core/logger`: logger.
|
|
34
|
-
- `@rakun-kit/core/errors`: application errors.
|
|
35
|
-
- `@rakun-kit/core/types`: types derived from content types.
|
|
36
|
-
|
|
37
|
-
## Bootstrap
|
|
38
|
-
|
|
39
|
-
`rakunBootstrap(options)` registers global configuration and content types:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
rakunBootstrap({
|
|
43
|
-
literals,
|
|
44
|
-
contentTypes: [Page, Post],
|
|
45
|
-
routes,
|
|
46
|
-
apiOperations,
|
|
47
|
-
mongo: {
|
|
48
|
-
MONGO_URI: process.env.MONGO_URI!,
|
|
49
|
-
ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
|
|
50
|
-
},
|
|
51
|
-
media,
|
|
52
|
-
logger: {
|
|
53
|
-
level: "info",
|
|
54
|
-
prettify: true,
|
|
55
|
-
},
|
|
56
|
-
syncRoutes: true,
|
|
57
|
-
});
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Options:
|
|
61
|
-
|
|
62
|
-
- `literals`: text/translation catalog.
|
|
63
|
-
- `contentTypes`: application content types.
|
|
64
|
-
- `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
|
|
65
|
-
- `routes`: configured routes used to resolve pages.
|
|
66
|
-
- `apiOperations`: custom API operations added to the Rakun operation registry.
|
|
67
|
-
- `mongo`: MongoDB connection. Required before serving Rakun requests.
|
|
68
|
-
- `media`: media adapter/configuration. Optional.
|
|
69
|
-
- `mail`: outbound mail adapter and default sender configuration. Optional.
|
|
70
|
-
- `accountRecovery`: password-reset URL builder, expiry, and optional custom
|
|
71
|
-
mail template. Requires `mail`; core provides the default template.
|
|
72
|
-
- `login`: manager password-login toggle and external login adapters.
|
|
73
|
-
- `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
|
|
74
|
-
- `syncRoutes`: syncs configured routes during initialization. Enabled by default.
|
|
75
|
-
|
|
76
|
-
## External manager login
|
|
77
|
-
|
|
78
|
-
Configure external login methods in `rakunBootstrap`. The callback URL is the
|
|
79
|
-
manager's public `/login/callback` route.
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
import {
|
|
83
|
-
createGitHubLoginAdapter,
|
|
84
|
-
createGoogleLoginAdapter,
|
|
85
|
-
createMicrosoftLoginAdapter,
|
|
86
|
-
rakunBootstrap,
|
|
87
|
-
} from "@rakun-kit/core";
|
|
88
|
-
|
|
89
|
-
const callbackUrl = "https://cms.example.com/backend/login/callback";
|
|
90
|
-
|
|
91
|
-
rakunBootstrap({
|
|
92
|
-
// ...
|
|
93
|
-
login: {
|
|
94
|
-
password: true,
|
|
95
|
-
adapters: [
|
|
96
|
-
createGitHubLoginAdapter({
|
|
97
|
-
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
98
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
99
|
-
redirectUri: callbackUrl,
|
|
100
|
-
}),
|
|
101
|
-
createGoogleLoginAdapter({
|
|
102
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
103
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
104
|
-
redirectUri: callbackUrl,
|
|
105
|
-
}),
|
|
106
|
-
createMicrosoftLoginAdapter({
|
|
107
|
-
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
108
|
-
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
109
|
-
redirectUri: callbackUrl,
|
|
110
|
-
tenant: "common",
|
|
111
|
-
}),
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Register the exact callback URL in each provider console. External identities
|
|
118
|
-
must return a verified email matching an
|
|
119
|
-
existing manager user. Rakun does not create manager accounts automatically.
|
|
120
|
-
The flow uses an HttpOnly state cookie and PKCE and preserves the existing MFA
|
|
121
|
-
step. Set `password: false` only when at least one adapter is configured.
|
|
122
|
-
Custom providers can be implemented with `defineLoginAdapter`.
|
|
123
|
-
|
|
124
|
-
### Persistent password-login IP blocking
|
|
125
|
-
|
|
126
|
-
Password login enables persistent fail2ban-style IP blocking by default. Five
|
|
127
|
-
failed attempts block the IP indefinitely; a successful login before reaching
|
|
128
|
-
the limit clears its failed-attempt counter. Configure or disable it through
|
|
129
|
-
`login.fail2ban`:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
rakunBootstrap({
|
|
133
|
-
// ...
|
|
134
|
-
login: {
|
|
135
|
-
password: true,
|
|
136
|
-
fail2ban: {
|
|
137
|
-
maxAttempts: 5,
|
|
138
|
-
// Optional when the deployment uses a custom trusted proxy:
|
|
139
|
-
// resolveIp: (ctx) => ctx.req?.headers?.['your-trusted-ip-header'] as string,
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
})
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Users with the `auth.ipBlocks.manage` permission can inspect and remove blocks
|
|
146
|
-
under Manager Settings → Security. Admin roles receive this built-in permission
|
|
147
|
-
automatically. A block can also be removed directly from MongoDB by deleting
|
|
148
|
-
the matching document from the `LoginIpBlock` collection. Set `fail2ban: false`
|
|
149
|
-
to disable this behavior. External login adapters are not affected.
|
|
150
|
-
|
|
151
|
-
## Account recovery and MFA recovery
|
|
152
|
-
|
|
153
|
-
Password recovery is enabled when both `mail` and `accountRecovery` are
|
|
154
|
-
configured. The reset URL must point to the manager's public
|
|
155
|
-
`/reset-password` screen and preserve the token as the `token` query parameter:
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import { rakunBootstrap } from "@rakun-kit/core";
|
|
159
|
-
import { createResendMailServiceConfig } from "@rakun-kit/resend";
|
|
160
|
-
|
|
161
|
-
rakunBootstrap({
|
|
162
|
-
// ...
|
|
163
|
-
mail: createResendMailServiceConfig({
|
|
164
|
-
apiKey: process.env.RESEND_API_KEY!,
|
|
165
|
-
defaultFrom: process.env.RAKUN_MAIL_FROM!,
|
|
166
|
-
}),
|
|
167
|
-
accountRecovery: {
|
|
168
|
-
passwordReset: {
|
|
169
|
-
expiresInMs: 60 * 60 * 1000,
|
|
170
|
-
createUrl: (token) =>
|
|
171
|
-
`https://cms.example.com/backend/reset-password?token=${encodeURIComponent(token)}`,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Core uses its branded HTML and plain-text password-reset template by default.
|
|
178
|
-
Pass `accountRecovery.passwordReset.template` only when the application needs
|
|
179
|
-
to replace it with custom branding or copy.
|
|
180
|
-
|
|
181
|
-
Reset tokens are random, stored only as SHA-256 hashes, expire, and are consumed
|
|
182
|
-
once. A successful reset closes every existing session but never disables MFA.
|
|
183
|
-
The request endpoint always returns the same result for known and unknown email
|
|
184
|
-
addresses.
|
|
185
|
-
|
|
186
|
-
Enabling TOTP or the first WebAuthn device returns one-time recovery codes to
|
|
187
|
-
the manager UI. Only their hashes are persisted, each code is consumed after
|
|
188
|
-
one login, and users can replace the entire set from their account after
|
|
189
|
-
confirming their current password. MFA state cannot be disabled through generic
|
|
190
|
-
manager content operations. If a user loses all MFA methods and recovery codes,
|
|
191
|
-
recovery must be handled by an authorized person through a channel outside the
|
|
192
|
-
CMS.
|
|
193
|
-
|
|
194
|
-
Content types can define lifecycle `hooks` and opt into manager-selected
|
|
195
|
-
`dynamicData` sources.
|
|
196
|
-
|
|
197
|
-
Routes can define fixed layout module slots. Rakun syncs those slots and the
|
|
198
|
-
manager lets users select an existing entry for each route:
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
rakunBootstrap({
|
|
202
|
-
// ...
|
|
203
|
-
routes: [
|
|
204
|
-
{
|
|
205
|
-
key: "pages",
|
|
206
|
-
contentType: "Page",
|
|
207
|
-
field: "slug",
|
|
208
|
-
hasPage: true,
|
|
209
|
-
dynamic: false,
|
|
210
|
-
defaultBasePath: "",
|
|
211
|
-
layout: [
|
|
212
|
-
{ key: "header", contentType: "Header" },
|
|
213
|
-
{ type: "content" },
|
|
214
|
-
{ key: "footer", contentType: "Footer" },
|
|
215
|
-
],
|
|
216
|
-
},
|
|
217
|
-
],
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
The web page response includes an ordered `layout` array containing module slots
|
|
222
|
-
plus the content slot.
|
|
223
|
-
|
|
224
|
-
## Module picker
|
|
225
|
-
|
|
226
|
-
Content types used as iterator modules can customize their card in the manager's
|
|
227
|
-
add-module dialog. `modulePicker.preview` accepts an image URL available to the
|
|
228
|
-
manager browser; relative URLs are also supported:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
const Hero = new ContentType({
|
|
232
|
-
name: "Hero",
|
|
233
|
-
modulePicker: {
|
|
234
|
-
title: "Hero section",
|
|
235
|
-
description: "Large introduction with heading, copy, and CTA.",
|
|
236
|
-
category: "Marketing",
|
|
237
|
-
icon: "PanelTop",
|
|
238
|
-
preview: "/images/modules/hero.webp",
|
|
239
|
-
keywords: ["banner", "cover"],
|
|
240
|
-
},
|
|
241
|
-
fields: {
|
|
242
|
-
title: f.string().required(),
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
When `preview` is omitted or cannot be loaded, the picker displays a neutral
|
|
248
|
-
placeholder so module cards keep a consistent height.
|
|
249
|
-
|
|
250
|
-
`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.
|
|
251
|
-
|
|
252
|
-
`ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
|
|
253
|
-
|
|
254
|
-
English is built into `@rakun-kit/manager-react`. Install only the extra
|
|
255
|
-
manager UI locales an application needs:
|
|
256
|
-
|
|
257
|
-
```sh
|
|
258
|
-
bun add @rakun-kit/manager-locales
|
|
259
|
-
# or: npm install @rakun-kit/manager-locales
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Import the required language subpath and register it with `managerLanguages`;
|
|
263
|
-
the public `manager.uiLocales` operation returns configured packs to the
|
|
264
|
-
manager client:
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
|
|
268
|
-
|
|
269
|
-
rakunBootstrap({
|
|
270
|
-
// ...
|
|
271
|
-
managerLanguages: [esManagerLocalePack],
|
|
272
|
-
});
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
`managerLanguages` may also extend locales with arbitrary project keys. This is
|
|
276
|
-
useful for translatable content-type titles and categories without adding host
|
|
277
|
-
keys to the manager's static `ManagerMessageKey` union:
|
|
278
|
-
|
|
279
|
-
```ts
|
|
280
|
-
import { extendManagerLanguagePack } from "@rakun-kit/core/contracts";
|
|
281
|
-
import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
|
|
282
|
-
|
|
283
|
-
rakunBootstrap({
|
|
284
|
-
// ...
|
|
285
|
-
managerLanguages: [
|
|
286
|
-
{
|
|
287
|
-
code: "en",
|
|
288
|
-
name: "English",
|
|
289
|
-
messages: {
|
|
290
|
-
"field.title": "Title",
|
|
291
|
-
"layoutModule.header": "Header",
|
|
292
|
-
"project.contentTypes.article.menu": "Articles",
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
extendManagerLanguagePack(esManagerLocalePack, {
|
|
296
|
-
"field.title": "Título",
|
|
297
|
-
"layoutModule.header": "Cabecera",
|
|
298
|
-
"project.contentTypes.article.menu": "Artículos",
|
|
299
|
-
}),
|
|
300
|
-
],
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Content-type field labels automatically use `field.<fieldName>` from these
|
|
305
|
-
project messages. Missing translations fall back to a built-in manager label
|
|
306
|
-
when available and otherwise to the humanized field name.
|
|
307
|
-
|
|
308
|
-
Route layout module labels use `layoutModule.<layoutKey>`, so a slot configured
|
|
309
|
-
with `key: 'header'` resolves `layoutModule.header`.
|
|
310
|
-
|
|
311
|
-
## Plugins
|
|
312
|
-
|
|
313
|
-
Trusted server plugins contribute to the same bootstrap registry without coupling
|
|
314
|
-
core to React:
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
import { defineRakunPlugin, rakunBootstrap } from "@rakun-kit/core";
|
|
318
|
-
|
|
319
|
-
export const analyticsPlugin = defineRakunPlugin({
|
|
320
|
-
id: "@acme/rakun-analytics",
|
|
321
|
-
contentTypes: [AnalyticsEvent],
|
|
322
|
-
routes: analyticsRoutes,
|
|
323
|
-
apiOperations: analyticsOperations,
|
|
324
|
-
permissions: ["plugin.analytics.view"],
|
|
325
|
-
literals: {},
|
|
326
|
-
initialize: async ({ db }) => {
|
|
327
|
-
// Services and migrations are ready here. Keep initialization idempotent.
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
rakunBootstrap({
|
|
332
|
-
plugins: [analyticsPlugin],
|
|
333
|
-
contentTypes: [],
|
|
334
|
-
literals: {},
|
|
335
|
-
mongo,
|
|
336
|
-
});
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
Plugin ids and contributed content types, routes, operations, literals, and
|
|
340
|
-
custom field editor ids must be unique. Rakun reports both owners on conflicts.
|
|
341
|
-
Visual manager and web facets are registered separately in their browser
|
|
342
|
-
runtimes.
|
|
343
|
-
|
|
344
|
-
Custom field factories can use `createPluginField`. Their serializable
|
|
345
|
-
`meta.editor` must match a field declaration in the server plugin and a React
|
|
346
|
-
editor registered by its manager facet.
|
|
347
|
-
|
|
348
|
-
## Content Types
|
|
349
|
-
|
|
350
|
-
A `ContentType` defines a logical collection:
|
|
351
|
-
|
|
352
|
-
```ts
|
|
353
|
-
const Post = new ContentType({
|
|
354
|
-
name: "Post",
|
|
355
|
-
menu: {
|
|
356
|
-
title: "Posts",
|
|
357
|
-
icon: "newspaper",
|
|
358
|
-
category: "Content",
|
|
359
|
-
},
|
|
360
|
-
fields: {
|
|
361
|
-
title: f.string().required(),
|
|
362
|
-
slug: f.string().type("Slug").required(),
|
|
363
|
-
body: f.string().type("RichText"),
|
|
364
|
-
published: f.boolean(),
|
|
365
|
-
},
|
|
366
|
-
uniques: [["slug"]],
|
|
367
|
-
listFields: ["title", "slug", "published"],
|
|
368
|
-
});
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
Page-like content types can define ordered page modules with `iterator` outside
|
|
372
|
-
`fields`. Rakun persists this generated field as `_iterator`:
|
|
373
|
-
|
|
374
|
-
```ts
|
|
375
|
-
const Page = new ContentType({
|
|
376
|
-
name: "Page",
|
|
377
|
-
fields: {
|
|
378
|
-
title: f.string().required(),
|
|
379
|
-
slug: f.string().type("Slug").required(),
|
|
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
|
+
f,
|
|
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
|
+
- Mail: `createMailService`, `sendMail`, typed template registries, adapters, and types.
|
|
25
|
+
- Permissions, translations, errors, contracts, and public types.
|
|
26
|
+
- Basic internal content types: `Language`, `ManagerUser`, `Seo`.
|
|
27
|
+
|
|
28
|
+
Relevant subpaths:
|
|
29
|
+
|
|
30
|
+
- `@rakun-kit/core/manager`: manager operation metadata and types.
|
|
31
|
+
- `@rakun-kit/core/contracts`: shared manager/web payload schemas.
|
|
32
|
+
- `@rakun-kit/core/client`: client-facing types and utilities.
|
|
33
|
+
- `@rakun-kit/core/logger`: logger.
|
|
34
|
+
- `@rakun-kit/core/errors`: application errors.
|
|
35
|
+
- `@rakun-kit/core/types`: types derived from content types.
|
|
36
|
+
|
|
37
|
+
## Bootstrap
|
|
38
|
+
|
|
39
|
+
`rakunBootstrap(options)` registers global configuration and content types:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
rakunBootstrap({
|
|
43
|
+
literals,
|
|
44
|
+
contentTypes: [Page, Post],
|
|
45
|
+
routes,
|
|
46
|
+
apiOperations,
|
|
47
|
+
mongo: {
|
|
48
|
+
MONGO_URI: process.env.MONGO_URI!,
|
|
49
|
+
ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
|
|
50
|
+
},
|
|
51
|
+
media,
|
|
52
|
+
logger: {
|
|
53
|
+
level: "info",
|
|
54
|
+
prettify: true,
|
|
55
|
+
},
|
|
56
|
+
syncRoutes: true,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
|
|
62
|
+
- `literals`: text/translation catalog.
|
|
63
|
+
- `contentTypes`: application content types.
|
|
64
|
+
- `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
|
|
65
|
+
- `routes`: configured routes used to resolve pages.
|
|
66
|
+
- `apiOperations`: custom API operations added to the Rakun operation registry.
|
|
67
|
+
- `mongo`: MongoDB connection. Required before serving Rakun requests.
|
|
68
|
+
- `media`: media adapter/configuration. Optional.
|
|
69
|
+
- `mail`: outbound mail adapter and default sender configuration. Optional.
|
|
70
|
+
- `accountRecovery`: password-reset URL builder, expiry, and optional custom
|
|
71
|
+
mail template. Requires `mail`; core provides the default template.
|
|
72
|
+
- `login`: manager password-login toggle and external login adapters.
|
|
73
|
+
- `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
|
|
74
|
+
- `syncRoutes`: syncs configured routes during initialization. Enabled by default.
|
|
75
|
+
|
|
76
|
+
## External manager login
|
|
77
|
+
|
|
78
|
+
Configure external login methods in `rakunBootstrap`. The callback URL is the
|
|
79
|
+
manager's public `/login/callback` route.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {
|
|
83
|
+
createGitHubLoginAdapter,
|
|
84
|
+
createGoogleLoginAdapter,
|
|
85
|
+
createMicrosoftLoginAdapter,
|
|
86
|
+
rakunBootstrap,
|
|
87
|
+
} from "@rakun-kit/core";
|
|
88
|
+
|
|
89
|
+
const callbackUrl = "https://cms.example.com/backend/login/callback";
|
|
90
|
+
|
|
91
|
+
rakunBootstrap({
|
|
92
|
+
// ...
|
|
93
|
+
login: {
|
|
94
|
+
password: true,
|
|
95
|
+
adapters: [
|
|
96
|
+
createGitHubLoginAdapter({
|
|
97
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
98
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
99
|
+
redirectUri: callbackUrl,
|
|
100
|
+
}),
|
|
101
|
+
createGoogleLoginAdapter({
|
|
102
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
103
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
104
|
+
redirectUri: callbackUrl,
|
|
105
|
+
}),
|
|
106
|
+
createMicrosoftLoginAdapter({
|
|
107
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
108
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
109
|
+
redirectUri: callbackUrl,
|
|
110
|
+
tenant: "common",
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Register the exact callback URL in each provider console. External identities
|
|
118
|
+
must return a verified email matching an
|
|
119
|
+
existing manager user. Rakun does not create manager accounts automatically.
|
|
120
|
+
The flow uses an HttpOnly state cookie and PKCE and preserves the existing MFA
|
|
121
|
+
step. Set `password: false` only when at least one adapter is configured.
|
|
122
|
+
Custom providers can be implemented with `defineLoginAdapter`.
|
|
123
|
+
|
|
124
|
+
### Persistent password-login IP blocking
|
|
125
|
+
|
|
126
|
+
Password login enables persistent fail2ban-style IP blocking by default. Five
|
|
127
|
+
failed attempts block the IP indefinitely; a successful login before reaching
|
|
128
|
+
the limit clears its failed-attempt counter. Configure or disable it through
|
|
129
|
+
`login.fail2ban`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
rakunBootstrap({
|
|
133
|
+
// ...
|
|
134
|
+
login: {
|
|
135
|
+
password: true,
|
|
136
|
+
fail2ban: {
|
|
137
|
+
maxAttempts: 5,
|
|
138
|
+
// Optional when the deployment uses a custom trusted proxy:
|
|
139
|
+
// resolveIp: (ctx) => ctx.req?.headers?.['your-trusted-ip-header'] as string,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Users with the `auth.ipBlocks.manage` permission can inspect and remove blocks
|
|
146
|
+
under Manager Settings → Security. Admin roles receive this built-in permission
|
|
147
|
+
automatically. A block can also be removed directly from MongoDB by deleting
|
|
148
|
+
the matching document from the `LoginIpBlock` collection. Set `fail2ban: false`
|
|
149
|
+
to disable this behavior. External login adapters are not affected.
|
|
150
|
+
|
|
151
|
+
## Account recovery and MFA recovery
|
|
152
|
+
|
|
153
|
+
Password recovery is enabled when both `mail` and `accountRecovery` are
|
|
154
|
+
configured. The reset URL must point to the manager's public
|
|
155
|
+
`/reset-password` screen and preserve the token as the `token` query parameter:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { rakunBootstrap } from "@rakun-kit/core";
|
|
159
|
+
import { createResendMailServiceConfig } from "@rakun-kit/resend";
|
|
160
|
+
|
|
161
|
+
rakunBootstrap({
|
|
162
|
+
// ...
|
|
163
|
+
mail: createResendMailServiceConfig({
|
|
164
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
165
|
+
defaultFrom: process.env.RAKUN_MAIL_FROM!,
|
|
166
|
+
}),
|
|
167
|
+
accountRecovery: {
|
|
168
|
+
passwordReset: {
|
|
169
|
+
expiresInMs: 60 * 60 * 1000,
|
|
170
|
+
createUrl: (token) =>
|
|
171
|
+
`https://cms.example.com/backend/reset-password?token=${encodeURIComponent(token)}`,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Core uses its branded HTML and plain-text password-reset template by default.
|
|
178
|
+
Pass `accountRecovery.passwordReset.template` only when the application needs
|
|
179
|
+
to replace it with custom branding or copy.
|
|
180
|
+
|
|
181
|
+
Reset tokens are random, stored only as SHA-256 hashes, expire, and are consumed
|
|
182
|
+
once. A successful reset closes every existing session but never disables MFA.
|
|
183
|
+
The request endpoint always returns the same result for known and unknown email
|
|
184
|
+
addresses.
|
|
185
|
+
|
|
186
|
+
Enabling TOTP or the first WebAuthn device returns one-time recovery codes to
|
|
187
|
+
the manager UI. Only their hashes are persisted, each code is consumed after
|
|
188
|
+
one login, and users can replace the entire set from their account after
|
|
189
|
+
confirming their current password. MFA state cannot be disabled through generic
|
|
190
|
+
manager content operations. If a user loses all MFA methods and recovery codes,
|
|
191
|
+
recovery must be handled by an authorized person through a channel outside the
|
|
192
|
+
CMS.
|
|
193
|
+
|
|
194
|
+
Content types can define lifecycle `hooks` and opt into manager-selected
|
|
195
|
+
`dynamicData` sources.
|
|
196
|
+
|
|
197
|
+
Routes can define fixed layout module slots. Rakun syncs those slots and the
|
|
198
|
+
manager lets users select an existing entry for each route:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
rakunBootstrap({
|
|
202
|
+
// ...
|
|
203
|
+
routes: [
|
|
204
|
+
{
|
|
205
|
+
key: "pages",
|
|
206
|
+
contentType: "Page",
|
|
207
|
+
field: "slug",
|
|
208
|
+
hasPage: true,
|
|
209
|
+
dynamic: false,
|
|
210
|
+
defaultBasePath: "",
|
|
211
|
+
layout: [
|
|
212
|
+
{ key: "header", contentType: "Header" },
|
|
213
|
+
{ type: "content" },
|
|
214
|
+
{ key: "footer", contentType: "Footer" },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The web page response includes an ordered `layout` array containing module slots
|
|
222
|
+
plus the content slot.
|
|
223
|
+
|
|
224
|
+
## Module picker
|
|
225
|
+
|
|
226
|
+
Content types used as iterator modules can customize their card in the manager's
|
|
227
|
+
add-module dialog. `modulePicker.preview` accepts an image URL available to the
|
|
228
|
+
manager browser; relative URLs are also supported:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const Hero = new ContentType({
|
|
232
|
+
name: "Hero",
|
|
233
|
+
modulePicker: {
|
|
234
|
+
title: "Hero section",
|
|
235
|
+
description: "Large introduction with heading, copy, and CTA.",
|
|
236
|
+
category: "Marketing",
|
|
237
|
+
icon: "PanelTop",
|
|
238
|
+
preview: "/images/modules/hero.webp",
|
|
239
|
+
keywords: ["banner", "cover"],
|
|
240
|
+
},
|
|
241
|
+
fields: {
|
|
242
|
+
title: f.string().required(),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
When `preview` is omitted or cannot be loaded, the picker displays a neutral
|
|
248
|
+
placeholder so module cards keep a consistent height.
|
|
249
|
+
|
|
250
|
+
`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.
|
|
251
|
+
|
|
252
|
+
`ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
|
|
253
|
+
|
|
254
|
+
English is built into `@rakun-kit/manager-react`. Install only the extra
|
|
255
|
+
manager UI locales an application needs:
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
bun add @rakun-kit/manager-locales
|
|
259
|
+
# or: npm install @rakun-kit/manager-locales
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Import the required language subpath and register it with `managerLanguages`;
|
|
263
|
+
the public `manager.uiLocales` operation returns configured packs to the
|
|
264
|
+
manager client:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
|
|
268
|
+
|
|
269
|
+
rakunBootstrap({
|
|
270
|
+
// ...
|
|
271
|
+
managerLanguages: [esManagerLocalePack],
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`managerLanguages` may also extend locales with arbitrary project keys. This is
|
|
276
|
+
useful for translatable content-type titles and categories without adding host
|
|
277
|
+
keys to the manager's static `ManagerMessageKey` union:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { extendManagerLanguagePack } from "@rakun-kit/core/contracts";
|
|
281
|
+
import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
|
|
282
|
+
|
|
283
|
+
rakunBootstrap({
|
|
284
|
+
// ...
|
|
285
|
+
managerLanguages: [
|
|
286
|
+
{
|
|
287
|
+
code: "en",
|
|
288
|
+
name: "English",
|
|
289
|
+
messages: {
|
|
290
|
+
"field.title": "Title",
|
|
291
|
+
"layoutModule.header": "Header",
|
|
292
|
+
"project.contentTypes.article.menu": "Articles",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
extendManagerLanguagePack(esManagerLocalePack, {
|
|
296
|
+
"field.title": "Título",
|
|
297
|
+
"layoutModule.header": "Cabecera",
|
|
298
|
+
"project.contentTypes.article.menu": "Artículos",
|
|
299
|
+
}),
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Content-type field labels automatically use `field.<fieldName>` from these
|
|
305
|
+
project messages. Missing translations fall back to a built-in manager label
|
|
306
|
+
when available and otherwise to the humanized field name.
|
|
307
|
+
|
|
308
|
+
Route layout module labels use `layoutModule.<layoutKey>`, so a slot configured
|
|
309
|
+
with `key: 'header'` resolves `layoutModule.header`.
|
|
310
|
+
|
|
311
|
+
## Plugins
|
|
312
|
+
|
|
313
|
+
Trusted server plugins contribute to the same bootstrap registry without coupling
|
|
314
|
+
core to React:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { defineRakunPlugin, rakunBootstrap } from "@rakun-kit/core";
|
|
318
|
+
|
|
319
|
+
export const analyticsPlugin = defineRakunPlugin({
|
|
320
|
+
id: "@acme/rakun-analytics",
|
|
321
|
+
contentTypes: [AnalyticsEvent],
|
|
322
|
+
routes: analyticsRoutes,
|
|
323
|
+
apiOperations: analyticsOperations,
|
|
324
|
+
permissions: ["plugin.analytics.view"],
|
|
325
|
+
literals: {},
|
|
326
|
+
initialize: async ({ db }) => {
|
|
327
|
+
// Services and migrations are ready here. Keep initialization idempotent.
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
rakunBootstrap({
|
|
332
|
+
plugins: [analyticsPlugin],
|
|
333
|
+
contentTypes: [],
|
|
334
|
+
literals: {},
|
|
335
|
+
mongo,
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Plugin ids and contributed content types, routes, operations, literals, and
|
|
340
|
+
custom field editor ids must be unique. Rakun reports both owners on conflicts.
|
|
341
|
+
Visual manager and web facets are registered separately in their browser
|
|
342
|
+
runtimes.
|
|
343
|
+
|
|
344
|
+
Custom field factories can use `createPluginField`. Their serializable
|
|
345
|
+
`meta.editor` must match a field declaration in the server plugin and a React
|
|
346
|
+
editor registered by its manager facet.
|
|
347
|
+
|
|
348
|
+
## Content Types
|
|
349
|
+
|
|
350
|
+
A `ContentType` defines a logical collection:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const Post = new ContentType({
|
|
354
|
+
name: "Post",
|
|
355
|
+
menu: {
|
|
356
|
+
title: "Posts",
|
|
357
|
+
icon: "newspaper",
|
|
358
|
+
category: "Content",
|
|
359
|
+
},
|
|
360
|
+
fields: {
|
|
361
|
+
title: f.string().required(),
|
|
362
|
+
slug: f.string().type("Slug").required(),
|
|
363
|
+
body: f.string().type("RichText"),
|
|
364
|
+
published: f.boolean(),
|
|
365
|
+
},
|
|
366
|
+
uniques: [["slug"]],
|
|
367
|
+
listFields: ["title", "slug", "published"],
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Page-like content types can define ordered page modules with `iterator` outside
|
|
372
|
+
`fields`. Rakun persists this generated field as `_iterator`:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const Page = new ContentType({
|
|
376
|
+
name: "Page",
|
|
377
|
+
fields: {
|
|
378
|
+
title: f.string().required(),
|
|
379
|
+
slug: f.string().type("Slug").required(),
|
|
380
380
|
},
|
|
381
381
|
iterator: [
|
|
382
382
|
{ contentType: PageSection, type: "new" },
|
|
@@ -397,156 +397,160 @@ document's iterator modules. This supports compositions such as a shared hero,
|
|
|
397
397
|
a `LayoutWithInfo` wrapper containing the unique content, and a shared
|
|
398
398
|
newsletter. Route layout modules such as header and footer still wrap the
|
|
399
399
|
assembled result.
|
|
400
|
-
|
|
400
|
+
|
|
401
401
|
Iterator and template modules can also be made conditional from the manager. A
|
|
402
402
|
condition is stored on the module entry and evaluated against the current
|
|
403
403
|
document:
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
{
|
|
407
|
-
name: "Credits",
|
|
408
|
-
value: { /* module relation */ },
|
|
409
|
-
visibleWhen: {
|
|
410
|
-
field: "credits",
|
|
411
|
-
operator: "notEmpty",
|
|
412
|
-
},
|
|
413
|
-
}
|
|
414
|
-
```
|
|
415
|
-
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
{
|
|
407
|
+
name: "Credits",
|
|
408
|
+
value: { /* module relation */ },
|
|
409
|
+
visibleWhen: {
|
|
410
|
+
field: "credits",
|
|
411
|
+
operator: "notEmpty",
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
416
|
Supported operators are `notEmpty` and `empty`. Conditional modules remain in
|
|
417
417
|
their stored Content or Template composition but are omitted from web and
|
|
418
418
|
preview output when their condition does not match.
|
|
419
|
-
|
|
420
|
-
Main properties:
|
|
421
|
-
|
|
422
|
-
- `name`: stable type name. Also used as `_type`.
|
|
419
|
+
|
|
420
|
+
Main properties:
|
|
421
|
+
|
|
422
|
+
- `name`: stable type name. Also used as `_type`.
|
|
423
423
|
- `fields`: field map.
|
|
424
424
|
- `iterator`: page module entries. Generates the reserved `_iterator` field.
|
|
425
425
|
- `menu`: manager metadata.
|
|
426
|
-
- `uniques`: unique field groups.
|
|
427
|
-
- `listFields`: preferred fields in lists and relations.
|
|
428
|
-
- `hideFromManager()`: hides the content type from manager content type lists.
|
|
429
|
-
- `apiOnly()`: applies `.apiOnly()` to every field in the content type.
|
|
430
|
-
- `managerOnly()`: applies `.managerOnly()` to every field in the content type.
|
|
431
|
-
- Saved manager documents support comments and user mentions by default.
|
|
432
|
-
- `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
|
|
433
|
-
- Dynamic data bindings are available on manager-visible fields by default; use
|
|
434
|
-
field-level `.noDynamic()` to opt out.
|
|
435
|
-
|
|
426
|
+
- `uniques`: unique field groups.
|
|
427
|
+
- `listFields`: preferred fields in lists and relations.
|
|
428
|
+
- `hideFromManager()`: hides the content type from manager content type lists.
|
|
429
|
+
- `apiOnly()`: applies `.apiOnly()` to every field in the content type.
|
|
430
|
+
- `managerOnly()`: applies `.managerOnly()` to every field in the content type.
|
|
431
|
+
- Saved manager documents support comments and user mentions by default.
|
|
432
|
+
- `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
|
|
433
|
+
- Dynamic data bindings are available on manager-visible fields by default; use
|
|
434
|
+
field-level `.noDynamic()` to opt out.
|
|
435
|
+
|
|
436
436
|
When a content type has a configured route with `hasPage: true`, Rakun adds an
|
|
437
437
|
optional reserved `_seo` relation automatically. If that content type has an
|
|
438
438
|
`iterator`, Rakun also enables its shared Template editor automatically.
|
|
439
|
-
|
|
440
|
-
Hooks run around DB mutations and public output resolution:
|
|
441
|
-
|
|
442
|
-
```ts
|
|
443
|
-
const User = new ContentType({
|
|
444
|
-
name: "User",
|
|
445
|
-
fields: {
|
|
446
|
-
email: f.string().type("Email").required(),
|
|
447
|
-
password: f.string().type("Password").required().managerOnly(),
|
|
448
|
-
},
|
|
449
|
-
}).withHooks({
|
|
450
|
-
beforeInsert: ({ data }) => ({
|
|
451
|
-
...data,
|
|
452
|
-
password: hashPassword(String(data.password)),
|
|
453
|
-
}),
|
|
454
|
-
onGet: ({ data }) => ({
|
|
455
|
-
...data,
|
|
456
|
-
displayName: String(data.email).split("@")[0],
|
|
457
|
-
}),
|
|
458
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
Dynamic data turns a content type into a reusable layout. The manager can bind
|
|
462
|
-
manager-visible fields to another content item field, or to a generated `href`
|
|
463
|
-
when a source content type has a page route. Fields are dynamic by default; call
|
|
464
|
-
`.noDynamic()` on fields that should not accept bindings or be exported as
|
|
465
|
-
source paths. Source content types are hidden by default; set
|
|
466
|
-
`dynamicDataSource: true` on content types that should appear in the manager
|
|
467
|
-
source selector. Field bindings can also select `Current document` to read values
|
|
468
|
-
from the document being edited without marking its content type as a source.
|
|
469
|
-
|
|
470
|
-
Source field selectors are type-aware. A string target only offers string-like
|
|
471
|
-
source paths, number targets only offer numbers, and boolean targets only offer
|
|
472
|
-
booleans. Object-like source fields are traversed so nested leaf fields can be
|
|
473
|
-
selected, while reserved SEO metadata is omitted from dynamic data mappings.
|
|
474
|
-
The generated `href` source is only shown for content types that have a
|
|
475
|
-
configured route with `hasPage: true`.
|
|
476
|
-
|
|
477
|
-
```ts
|
|
478
|
-
const Project = new ContentType({
|
|
479
|
-
name: "Project",
|
|
480
|
-
dynamicDataSource: true,
|
|
481
|
-
fields: {
|
|
482
|
-
title: f.string().required(),
|
|
483
|
-
slug: f.string().type("Slug").required(),
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
const Carousel = new ContentType({
|
|
488
|
-
name: "Carousel",
|
|
489
|
-
fields: {
|
|
490
|
-
title: f.string().required(),
|
|
491
|
-
internalNote: f.string().noDynamic(),
|
|
492
|
-
items: f.blocks([
|
|
493
|
-
{
|
|
494
|
-
name: "CarouselItem",
|
|
495
|
-
field: f.relation(CarouselItem, "new"),
|
|
496
|
-
},
|
|
497
|
-
]),
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
List bindings append dynamic items to manually stored items instead of replacing
|
|
503
|
-
the list. If the same stable item id appears in both sources, the dynamically
|
|
504
|
-
resolved item wins and the duplicate manual copy is skipped.
|
|
505
|
-
|
|
506
|
-
A list inside a nested module can also use a compatible relation or blocks array
|
|
507
|
-
from the current document. The manager exposes these sources as
|
|
508
|
-
`Current document · <field>`, infers the content type of each array item, and
|
|
509
|
-
lets each item be mapped with the same field mapping UI used for collections.
|
|
510
|
-
These bindings store a `currentDocument` source alongside the inferred item
|
|
511
|
-
content type:
|
|
512
|
-
|
|
513
|
-
```ts
|
|
514
|
-
const headerBindings = {
|
|
515
|
-
lists: {
|
|
516
|
-
categories: {
|
|
517
|
-
contentType: LinkItem.name,
|
|
518
|
-
source: {
|
|
519
|
-
kind: "currentDocument",
|
|
520
|
-
contentType: Project.name,
|
|
521
|
-
path: "categories",
|
|
522
|
-
itemName: "Category",
|
|
523
|
-
},
|
|
524
|
-
itemName: "Category",
|
|
525
|
-
map: {
|
|
526
|
-
title: { contentType: LinkItem.name, path: "title" },
|
|
527
|
-
href: { contentType: LinkItem.name, path: "href" },
|
|
528
|
-
},
|
|
529
|
-
},
|
|
530
|
-
},
|
|
531
|
-
};
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
List query conditions can compare a source field with a value from the current
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
439
|
+
|
|
440
|
+
Hooks run around DB mutations and public output resolution:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
const User = new ContentType({
|
|
444
|
+
name: "User",
|
|
445
|
+
fields: {
|
|
446
|
+
email: f.string().type("Email").required(),
|
|
447
|
+
password: f.string().type("Password").required().managerOnly(),
|
|
448
|
+
},
|
|
449
|
+
}).withHooks({
|
|
450
|
+
beforeInsert: ({ data }) => ({
|
|
451
|
+
...data,
|
|
452
|
+
password: hashPassword(String(data.password)),
|
|
453
|
+
}),
|
|
454
|
+
onGet: ({ data }) => ({
|
|
455
|
+
...data,
|
|
456
|
+
displayName: String(data.email).split("@")[0],
|
|
457
|
+
}),
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Dynamic data turns a content type into a reusable layout. The manager can bind
|
|
462
|
+
manager-visible fields to another content item field, or to a generated `href`
|
|
463
|
+
when a source content type has a page route. Fields are dynamic by default; call
|
|
464
|
+
`.noDynamic()` on fields that should not accept bindings or be exported as
|
|
465
|
+
source paths. Source content types are hidden by default; set
|
|
466
|
+
`dynamicDataSource: true` on content types that should appear in the manager
|
|
467
|
+
source selector. Field bindings can also select `Current document` to read values
|
|
468
|
+
from the document being edited without marking its content type as a source.
|
|
469
|
+
|
|
470
|
+
Source field selectors are type-aware. A string target only offers string-like
|
|
471
|
+
source paths, number targets only offer numbers, and boolean targets only offer
|
|
472
|
+
booleans. Object-like source fields are traversed so nested leaf fields can be
|
|
473
|
+
selected, while reserved SEO metadata is omitted from dynamic data mappings.
|
|
474
|
+
The generated `href` source is only shown for content types that have a
|
|
475
|
+
configured route with `hasPage: true`.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const Project = new ContentType({
|
|
479
|
+
name: "Project",
|
|
480
|
+
dynamicDataSource: true,
|
|
481
|
+
fields: {
|
|
482
|
+
title: f.string().required(),
|
|
483
|
+
slug: f.string().type("Slug").required(),
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const Carousel = new ContentType({
|
|
488
|
+
name: "Carousel",
|
|
489
|
+
fields: {
|
|
490
|
+
title: f.string().required(),
|
|
491
|
+
internalNote: f.string().noDynamic(),
|
|
492
|
+
items: f.blocks([
|
|
493
|
+
{
|
|
494
|
+
name: "CarouselItem",
|
|
495
|
+
field: f.relation(CarouselItem, "new"),
|
|
496
|
+
},
|
|
497
|
+
]),
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
List bindings append dynamic items to manually stored items instead of replacing
|
|
503
|
+
the list. If the same stable item id appears in both sources, the dynamically
|
|
504
|
+
resolved item wins and the duplicate manual copy is skipped.
|
|
505
|
+
|
|
506
|
+
A list inside a nested module can also use a compatible relation or blocks array
|
|
507
|
+
from the current document. The manager exposes these sources as
|
|
508
|
+
`Current document · <field>`, infers the content type of each array item, and
|
|
509
|
+
lets each item be mapped with the same field mapping UI used for collections.
|
|
510
|
+
These bindings store a `currentDocument` source alongside the inferred item
|
|
511
|
+
content type:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const headerBindings = {
|
|
515
|
+
lists: {
|
|
516
|
+
categories: {
|
|
517
|
+
contentType: LinkItem.name,
|
|
518
|
+
source: {
|
|
519
|
+
kind: "currentDocument",
|
|
520
|
+
contentType: Project.name,
|
|
521
|
+
path: "categories",
|
|
522
|
+
itemName: "Category",
|
|
523
|
+
},
|
|
524
|
+
itemName: "Category",
|
|
525
|
+
map: {
|
|
526
|
+
title: { contentType: LinkItem.name, path: "title" },
|
|
527
|
+
href: { contentType: LinkItem.name, path: "href" },
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
List query conditions can compare a source field with a value from the current
|
|
535
|
+
context. At the top level this is the current document; inside a nested list it
|
|
536
|
+
is the parent source item and the manager labels it `Current item`. Programmatic
|
|
537
|
+
bindings use `{ $current: "path.to.field" }`. For example, a category can query
|
|
538
|
+
only the projects whose related category has the same slug:
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
query: {
|
|
542
|
+
filter: {
|
|
543
|
+
"category.slug": { $current: "slug" },
|
|
544
|
+
},
|
|
545
|
+
options: { limit: 10 },
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Nested conditions can still read the root document by selecting `Current
|
|
550
|
+
document`, which stores `{ $document: "path.to.field" }`. Current-item and
|
|
551
|
+
current-document paths are checked against their respective content type's
|
|
552
|
+
dynamic field rules before the query runs. `_id` is also available for relation
|
|
553
|
+
queries.
|
|
550
554
|
|
|
551
555
|
List mappings can be nested recursively when a mapped target field is itself a
|
|
552
556
|
`blocks` list. Use `kind: "list"` for that map entry and configure its source,
|
|
@@ -579,108 +583,108 @@ apply the same source opt-in, field compatibility, query validation, and
|
|
|
579
583
|
`.noDynamic()` rules at every level.
|
|
580
584
|
|
|
581
585
|
A list mapping can also collect an array through a reverse relation without
|
|
582
|
-
persisting that relation on the source document. For example, a category gallery
|
|
583
|
-
can create one item per `Category` and collect the images of its related
|
|
584
|
-
`Project` documents:
|
|
585
|
-
|
|
586
|
-
```ts
|
|
587
|
-
const Category = new ContentType({
|
|
588
|
-
name: "Category",
|
|
589
|
-
dynamicDataSource: true,
|
|
590
|
-
fields: {
|
|
591
|
-
title: f.string().required(),
|
|
592
|
-
},
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
const Project = new ContentType({
|
|
596
|
-
name: "Project",
|
|
597
|
-
dynamicDataSource: true,
|
|
598
|
-
fields: {
|
|
599
|
-
category: f.relation(Category, "existing").required(),
|
|
600
|
-
images: f.file().type("Image").multiple().required(),
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
const galleryBindings = {
|
|
605
|
-
lists: {
|
|
606
|
-
items: {
|
|
607
|
-
contentType: Category.name,
|
|
608
|
-
itemName: "CategoriesGalleryItem",
|
|
609
|
-
map: {
|
|
610
|
-
title: { contentType: Category.name, path: "title" },
|
|
611
|
-
images: {
|
|
612
|
-
kind: "relatedCollection",
|
|
613
|
-
contentType: Project.name,
|
|
614
|
-
relation: "category",
|
|
615
|
-
path: "images",
|
|
616
|
-
limit: 10,
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
The related collection query matches `Project.category._id` against the current
|
|
625
|
-
category, preserves project and image order, and flattens the selected array by
|
|
626
|
-
one level. Its numeric limit applies to related projects and is capped at 100.
|
|
627
|
-
|
|
628
|
-
Schema and validation methods:
|
|
629
|
-
|
|
630
|
-
- `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
|
|
631
|
-
- `getSchema()`: persisted schema.
|
|
632
|
-
- `getPopulatedSchema()`: output with populated relations and `_id`.
|
|
633
|
-
- `getOutputSchema()`: public API output.
|
|
634
|
-
- `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
|
|
635
|
-
- `validate`, `partialValidate`, `validateOutput`: Zod helpers.
|
|
636
|
-
|
|
637
|
-
The registry lives in `lib/Registry`:
|
|
638
|
-
|
|
639
|
-
- `registerContentType`, `registerInternalContentType`.
|
|
640
|
-
- `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
|
|
641
|
-
- `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
|
|
642
|
-
- `getContentTypeByName`.
|
|
643
|
-
|
|
644
|
-
## Fields
|
|
645
|
-
|
|
646
|
-
Use the concise `f` namespace for field factories. `Fields` remains exported as
|
|
647
|
-
a backward-compatible alias and references the same object.
|
|
648
|
-
|
|
649
|
-
Main factories:
|
|
650
|
-
|
|
651
|
-
```ts
|
|
652
|
-
f.string();
|
|
653
|
-
f.number();
|
|
654
|
-
f.boolean();
|
|
655
|
-
f.date();
|
|
656
|
-
f.select(["draft", "published"]);
|
|
657
|
-
f.relation(Post);
|
|
658
|
-
f.contentReference("Post");
|
|
659
|
-
f.selfRelation();
|
|
660
|
-
f.blocks([{ name: "title", field: f.string() }]);
|
|
661
|
-
f.array(f.string());
|
|
586
|
+
persisting that relation on the source document. For example, a category gallery
|
|
587
|
+
can create one item per `Category` and collect the images of its related
|
|
588
|
+
`Project` documents:
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
const Category = new ContentType({
|
|
592
|
+
name: "Category",
|
|
593
|
+
dynamicDataSource: true,
|
|
594
|
+
fields: {
|
|
595
|
+
title: f.string().required(),
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const Project = new ContentType({
|
|
600
|
+
name: "Project",
|
|
601
|
+
dynamicDataSource: true,
|
|
602
|
+
fields: {
|
|
603
|
+
category: f.relation(Category, "existing").required(),
|
|
604
|
+
images: f.file().type("Image").multiple().required(),
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const galleryBindings = {
|
|
609
|
+
lists: {
|
|
610
|
+
items: {
|
|
611
|
+
contentType: Category.name,
|
|
612
|
+
itemName: "CategoriesGalleryItem",
|
|
613
|
+
map: {
|
|
614
|
+
title: { contentType: Category.name, path: "title" },
|
|
615
|
+
images: {
|
|
616
|
+
kind: "relatedCollection",
|
|
617
|
+
contentType: Project.name,
|
|
618
|
+
relation: "category",
|
|
619
|
+
path: "images",
|
|
620
|
+
limit: 10,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
The related collection query matches `Project.category._id` against the current
|
|
629
|
+
category, preserves project and image order, and flattens the selected array by
|
|
630
|
+
one level. Its numeric limit applies to related projects and is capped at 100.
|
|
631
|
+
|
|
632
|
+
Schema and validation methods:
|
|
633
|
+
|
|
634
|
+
- `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
|
|
635
|
+
- `getSchema()`: persisted schema.
|
|
636
|
+
- `getPopulatedSchema()`: output with populated relations and `_id`.
|
|
637
|
+
- `getOutputSchema()`: public API output.
|
|
638
|
+
- `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
|
|
639
|
+
- `validate`, `partialValidate`, `validateOutput`: Zod helpers.
|
|
640
|
+
|
|
641
|
+
The registry lives in `lib/Registry`:
|
|
642
|
+
|
|
643
|
+
- `registerContentType`, `registerInternalContentType`.
|
|
644
|
+
- `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
|
|
645
|
+
- `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
|
|
646
|
+
- `getContentTypeByName`.
|
|
647
|
+
|
|
648
|
+
## Fields
|
|
649
|
+
|
|
650
|
+
Use the concise `f` namespace for field factories. `Fields` remains exported as
|
|
651
|
+
a backward-compatible alias and references the same object.
|
|
652
|
+
|
|
653
|
+
Main factories:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
f.string();
|
|
657
|
+
f.number();
|
|
658
|
+
f.boolean();
|
|
659
|
+
f.date();
|
|
660
|
+
f.select(["draft", "published"]);
|
|
661
|
+
f.relation(Post);
|
|
662
|
+
f.contentReference("Post");
|
|
663
|
+
f.selfRelation();
|
|
664
|
+
f.blocks([{ name: "title", field: f.string() }]);
|
|
665
|
+
f.array(f.string());
|
|
662
666
|
f.link();
|
|
663
667
|
f.file();
|
|
664
668
|
f.breadcrums();
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
Common modifiers:
|
|
668
|
-
|
|
669
|
-
- `.required()`: marks field as required.
|
|
670
|
-
- `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
|
|
671
|
-
- `.apiOnly()`: available for API/persistence, hidden from manager.
|
|
672
|
-
- `.managerOnly()`: visible to manager, excluded from API output.
|
|
673
|
-
|
|
674
|
-
Notable fields:
|
|
675
|
-
|
|
676
|
-
- `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Common modifiers:
|
|
672
|
+
|
|
673
|
+
- `.required()`: marks field as required.
|
|
674
|
+
- `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
|
|
675
|
+
- `.apiOnly()`: available for API/persistence, hidden from manager.
|
|
676
|
+
- `.managerOnly()`: visible to manager, excluded from API output.
|
|
677
|
+
|
|
678
|
+
Notable fields:
|
|
679
|
+
|
|
680
|
+
- `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
|
|
677
681
|
- `NumberField`: supports `.min()` and `.max()`.
|
|
678
682
|
- `DateField`: supports `.type("Date")`, `.type("DateTime")` and
|
|
679
683
|
`.type("Time")`. Date and datetime writes accept JavaScript `Date` values or
|
|
680
684
|
their ISO datetime JSON representation and normalize them to `Date`; time
|
|
681
685
|
values remain ISO time strings.
|
|
682
686
|
- `RelationField`: relation to another `ContentType`; accepts existing references or inline creation. `f.relation(Post, "existing")` restricts to existing records; `"new"` restricts to new records. `.multiple()` returns a homogeneous array of relations.
|
|
683
|
-
- `ContentReferenceField`: reference by content type name.
|
|
687
|
+
- `ContentReferenceField`: reference by content type name.
|
|
684
688
|
- `LinkField`: the manager stores a direct `{ href, title }` value or an
|
|
685
689
|
internal `{ routeId, contentTypeId, title }` reference. Titled links resolve
|
|
686
690
|
to `{ href, title }` in web output, with internal `href` values localized by
|
|
@@ -693,462 +697,462 @@ Notable fields:
|
|
|
693
697
|
returns `null` when the content is resolved outside a routable page and is
|
|
694
698
|
never accepted as input, persisted, or rendered in the manager.
|
|
695
699
|
- `FileField`: integrates media and optimization options.
|
|
696
|
-
- `f.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.
|
|
697
|
-
- `f.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
|
|
698
|
-
- `IteratorField`: repeatable structure based on content type entries.
|
|
699
|
-
|
|
700
|
-
## Derived Types
|
|
701
|
-
|
|
702
|
-
`@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
|
|
703
|
-
|
|
704
|
-
- `DataInput<T>`: accepted shape for create/update.
|
|
705
|
-
- `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
|
|
706
|
-
- `DataPopulated<T>`: shape with populated relations.
|
|
707
|
-
- `DataFront<T>`: public output without `apiOnly` fields.
|
|
708
|
-
- `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
|
|
709
|
-
- `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
|
|
710
|
-
|
|
711
|
-
## ORM and Mongo
|
|
712
|
-
|
|
713
|
-
`core/src/orm` implements `DBService` on top of MongoDB:
|
|
714
|
-
|
|
715
|
-
```ts
|
|
716
|
-
const db = await getMongoService();
|
|
717
|
-
|
|
718
|
-
const post = await db.create(Post, {
|
|
719
|
-
_type: "Post",
|
|
720
|
-
title: "Hello",
|
|
721
|
-
slug: "hello",
|
|
722
|
-
});
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
Operations:
|
|
726
|
-
|
|
727
|
-
- `get(contentType, id, fields?)`
|
|
728
|
-
- `list(contentType, query)`
|
|
729
|
-
- `create(contentType, data)`
|
|
730
|
-
- `update(contentType, id, data)`
|
|
731
|
-
- `updateMany(contentType, filter, data)`
|
|
732
|
-
- `delete(contentType, filter)`
|
|
733
|
-
- `find(contentType, filter, fields?)`
|
|
734
|
-
- `clear(contentType)`
|
|
735
|
-
- `findDependencies(contentType, id)`
|
|
736
|
-
- `upsert(contentType, filter, data)`
|
|
737
|
-
- `getAll(contentType, query?)`
|
|
738
|
-
|
|
739
|
-
Connection:
|
|
740
|
-
|
|
741
|
-
- `createMongoConnection(config)`: stores config.
|
|
742
|
-
- `createMongoService(config)`: connects and creates handlers.
|
|
743
|
-
- `getMongoService()`: returns the singleton or creates it from config.
|
|
744
|
-
- `closeMongoService()`: closes the connection and clears the singleton.
|
|
745
|
-
|
|
746
|
-
`MongoConfig`:
|
|
747
|
-
|
|
748
|
-
```ts
|
|
749
|
-
type MongoConfig = {
|
|
750
|
-
MONGO_URI: string;
|
|
751
|
-
ENVIRONMENT?: "local" | "development" | "test" | "production";
|
|
752
|
-
};
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
In environments other than `test`, the connection creates indexes defined by `createIndexes`.
|
|
756
|
-
|
|
757
|
-
## API Operations
|
|
758
|
-
|
|
759
|
-
`api/operations` defines typed contracts and handlers for endpoints:
|
|
760
|
-
|
|
761
|
-
- Manager: CRUD, auth, MFA, media, literals, settings.
|
|
762
|
-
- Manager comments and mentions: `manager.comments.list`,
|
|
763
|
-
`manager.comments.create`, `manager.comments.toggleReaction`,
|
|
764
|
-
`manager.comments.markRead`, `manager.comments.unreadCount`,
|
|
765
|
-
`manager.users.mentions`,
|
|
766
|
-
`manager.notifications.list`, and `manager.notifications.markRead`.
|
|
767
|
-
- Web: page resolution.
|
|
768
|
-
|
|
769
|
-
Main helpers:
|
|
770
|
-
|
|
771
|
-
- `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
|
|
772
|
-
- `defineOperation`: combines a contract with its implementation.
|
|
773
|
-
- `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
|
|
774
|
-
- `GetClient`: derives a typed client from an operation map.
|
|
775
|
-
- `createManagerOperationContracts`, `createWebOperationContracts`.
|
|
776
|
-
- `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
|
|
777
|
-
- `createRakunOperationDefinitions`: combines manager + web.
|
|
778
|
-
- `createOperationManifest`: serializable metadata for clients.
|
|
779
|
-
- `createOperationPath`: HTTP path from operation name.
|
|
780
|
-
- `mergeOperationContracts`: combines maps.
|
|
781
|
-
|
|
782
|
-
`@rakun-kit/core/manager` exposes the manifest and name-based types:
|
|
783
|
-
|
|
784
|
-
```ts
|
|
785
|
-
import {
|
|
786
|
-
managerOperationManifest,
|
|
787
|
-
getManagerOperationMeta,
|
|
788
|
-
type ManagerOperationInput,
|
|
789
|
-
type ManagerOperationOutput,
|
|
790
|
-
} from "@rakun-kit/core/manager";
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
### Custom Operations
|
|
794
|
-
|
|
795
|
-
Applications can define operations in a separate object, pass that object to
|
|
796
|
-
bootstrap, and reuse its type on the frontend:
|
|
797
|
-
|
|
798
|
-
```ts
|
|
799
|
-
// server/api-operations.ts
|
|
800
|
-
import { defineOperation } from "@rakun-kit/core";
|
|
801
|
-
import { z } from "zod";
|
|
802
|
-
|
|
803
|
-
export const apiOperations = {
|
|
804
|
-
"demo.helloWorld": defineOperation<
|
|
805
|
-
{ text: string },
|
|
806
|
-
{ message: string },
|
|
807
|
-
"query",
|
|
808
|
-
"get",
|
|
809
|
-
"public"
|
|
810
|
-
>({
|
|
811
|
-
access: "public",
|
|
812
|
-
kind: "query",
|
|
813
|
-
method: "get",
|
|
814
|
-
description: "Return a hello world message with the provided text",
|
|
815
|
-
input: z.object({
|
|
816
|
-
text: z.string().default("world"),
|
|
817
|
-
}),
|
|
818
|
-
output: z.object({
|
|
819
|
-
message: z.string(),
|
|
820
|
-
}),
|
|
821
|
-
resolve: ({ input }) => ({
|
|
822
|
-
message: `Hello ${input.text}`,
|
|
823
|
-
}),
|
|
824
|
-
}),
|
|
825
|
-
};
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
```ts
|
|
829
|
-
// bootstrap
|
|
830
|
-
import { apiOperations } from "./server/api-operations";
|
|
831
|
-
|
|
832
|
-
rakunBootstrap({
|
|
833
|
-
// ...
|
|
834
|
-
apiOperations,
|
|
835
|
-
});
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
Operation names define their HTTP path: `demo.helloWorld` becomes
|
|
839
|
-
`/demo/helloWorld`.
|
|
840
|
-
|
|
841
|
-
Rules:
|
|
842
|
-
|
|
843
|
-
- Names must not collide with built-in operations.
|
|
844
|
-
- Operations prefixed with `manager.` are included in manager operation routers.
|
|
845
|
-
- Operations prefixed with `web.` are included in web operation routers.
|
|
846
|
-
- Operations with any other prefix are included when using the combined operation registry.
|
|
847
|
-
- Use `access: "public"` for operations called from public web pages.
|
|
848
|
-
- Use `access: "auth"` for operations that require a manager session.
|
|
849
|
-
|
|
850
|
-
The manager route `manager.apiOperations` returns a JSON-serializable operation
|
|
851
|
-
catalog for manager API docs/playgrounds. Input and output Zod schemas are
|
|
852
|
-
converted to JSON Schema for display.
|
|
853
|
-
|
|
854
|
-
### Typed API Client
|
|
855
|
-
|
|
856
|
-
`@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
|
|
857
|
-
|
|
858
|
-
```ts
|
|
859
|
-
import { createRakunApiClient, type GetClient } from "@rakun-kit/core/web";
|
|
860
|
-
import type { apiOperations } from "./server/api-operations";
|
|
861
|
-
|
|
862
|
-
type ApiClient = GetClient<typeof apiOperations>;
|
|
863
|
-
|
|
864
|
-
const client: ApiClient = createRakunApiClient<typeof apiOperations>({
|
|
865
|
-
baseUrl: "/api",
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
const result = await client.query("demo.helloWorld", {
|
|
869
|
-
text: "Rakun",
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
result.message;
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
The client exposes:
|
|
876
|
-
|
|
877
|
-
- `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
|
|
878
|
-
- `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
|
|
879
|
-
|
|
880
|
-
The client derives input and output types from the Zod schemas in
|
|
881
|
-
`apiOperations`.
|
|
882
|
-
|
|
883
|
-
## Request Context and Auth
|
|
884
|
-
|
|
885
|
-
`createRequestContext(input)` normalizes headers, cookies, and response:
|
|
886
|
-
|
|
887
|
-
```ts
|
|
888
|
-
const ctx = await createRequestContext({
|
|
889
|
-
headers: req.headers,
|
|
890
|
-
cookies,
|
|
891
|
-
res: {
|
|
892
|
-
setHeader: res.setHeader.bind(res),
|
|
893
|
-
cookie: res.cookie.bind(res),
|
|
894
|
-
},
|
|
895
|
-
});
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
The resulting context includes:
|
|
899
|
-
|
|
900
|
-
- `req.headers`, `req.cookies`
|
|
901
|
-
- `res.setHeader`, `res.cookie`
|
|
902
|
-
- `user`: manager user or `null`
|
|
903
|
-
- `getUser()`: returns the user or throws `AUTH_REQUIRED`
|
|
904
|
-
|
|
905
|
-
Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
|
|
906
|
-
|
|
907
|
-
## Media
|
|
908
|
-
|
|
909
|
-
Media uses a storage adapter:
|
|
910
|
-
|
|
911
|
-
```ts
|
|
912
|
-
createMediaService({
|
|
913
|
-
adapter,
|
|
914
|
-
defaultAccess: "private",
|
|
915
|
-
defaultGetExpiresInSeconds: 300,
|
|
916
|
-
uploadUrl: "/api/rakun/manager/media/upload",
|
|
917
|
-
});
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
APIs:
|
|
921
|
-
|
|
922
|
-
- `createMediaConnection(config)`: stores config without creating the service.
|
|
923
|
-
- `createMediaService(config)`: creates the singleton with an adapter.
|
|
924
|
-
- `getMediaService()`: returns the singleton or creates it from config.
|
|
925
|
-
- `handleMediaBinaryUpload`: processes manager binary uploads.
|
|
926
|
-
|
|
700
|
+
- `f.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.
|
|
701
|
+
- `f.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
|
|
702
|
+
- `IteratorField`: repeatable structure based on content type entries.
|
|
703
|
+
|
|
704
|
+
## Derived Types
|
|
705
|
+
|
|
706
|
+
`@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
|
|
707
|
+
|
|
708
|
+
- `DataInput<T>`: accepted shape for create/update.
|
|
709
|
+
- `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
|
|
710
|
+
- `DataPopulated<T>`: shape with populated relations.
|
|
711
|
+
- `DataFront<T>`: public output without `apiOnly` fields.
|
|
712
|
+
- `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
|
|
713
|
+
- `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
|
|
714
|
+
|
|
715
|
+
## ORM and Mongo
|
|
716
|
+
|
|
717
|
+
`core/src/orm` implements `DBService` on top of MongoDB:
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
const db = await getMongoService();
|
|
721
|
+
|
|
722
|
+
const post = await db.create(Post, {
|
|
723
|
+
_type: "Post",
|
|
724
|
+
title: "Hello",
|
|
725
|
+
slug: "hello",
|
|
726
|
+
});
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Operations:
|
|
730
|
+
|
|
731
|
+
- `get(contentType, id, fields?)`
|
|
732
|
+
- `list(contentType, query)`
|
|
733
|
+
- `create(contentType, data)`
|
|
734
|
+
- `update(contentType, id, data)`
|
|
735
|
+
- `updateMany(contentType, filter, data)`
|
|
736
|
+
- `delete(contentType, filter)`
|
|
737
|
+
- `find(contentType, filter, fields?)`
|
|
738
|
+
- `clear(contentType)`
|
|
739
|
+
- `findDependencies(contentType, id)`
|
|
740
|
+
- `upsert(contentType, filter, data)`
|
|
741
|
+
- `getAll(contentType, query?)`
|
|
742
|
+
|
|
743
|
+
Connection:
|
|
744
|
+
|
|
745
|
+
- `createMongoConnection(config)`: stores config.
|
|
746
|
+
- `createMongoService(config)`: connects and creates handlers.
|
|
747
|
+
- `getMongoService()`: returns the singleton or creates it from config.
|
|
748
|
+
- `closeMongoService()`: closes the connection and clears the singleton.
|
|
749
|
+
|
|
750
|
+
`MongoConfig`:
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
type MongoConfig = {
|
|
754
|
+
MONGO_URI: string;
|
|
755
|
+
ENVIRONMENT?: "local" | "development" | "test" | "production";
|
|
756
|
+
};
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
In environments other than `test`, the connection creates indexes defined by `createIndexes`.
|
|
760
|
+
|
|
761
|
+
## API Operations
|
|
762
|
+
|
|
763
|
+
`api/operations` defines typed contracts and handlers for endpoints:
|
|
764
|
+
|
|
765
|
+
- Manager: CRUD, auth, MFA, media, literals, settings.
|
|
766
|
+
- Manager comments and mentions: `manager.comments.list`,
|
|
767
|
+
`manager.comments.create`, `manager.comments.toggleReaction`,
|
|
768
|
+
`manager.comments.markRead`, `manager.comments.unreadCount`,
|
|
769
|
+
`manager.users.mentions`,
|
|
770
|
+
`manager.notifications.list`, and `manager.notifications.markRead`.
|
|
771
|
+
- Web: page resolution.
|
|
772
|
+
|
|
773
|
+
Main helpers:
|
|
774
|
+
|
|
775
|
+
- `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
|
|
776
|
+
- `defineOperation`: combines a contract with its implementation.
|
|
777
|
+
- `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
|
|
778
|
+
- `GetClient`: derives a typed client from an operation map.
|
|
779
|
+
- `createManagerOperationContracts`, `createWebOperationContracts`.
|
|
780
|
+
- `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
|
|
781
|
+
- `createRakunOperationDefinitions`: combines manager + web.
|
|
782
|
+
- `createOperationManifest`: serializable metadata for clients.
|
|
783
|
+
- `createOperationPath`: HTTP path from operation name.
|
|
784
|
+
- `mergeOperationContracts`: combines maps.
|
|
785
|
+
|
|
786
|
+
`@rakun-kit/core/manager` exposes the manifest and name-based types:
|
|
787
|
+
|
|
788
|
+
```ts
|
|
789
|
+
import {
|
|
790
|
+
managerOperationManifest,
|
|
791
|
+
getManagerOperationMeta,
|
|
792
|
+
type ManagerOperationInput,
|
|
793
|
+
type ManagerOperationOutput,
|
|
794
|
+
} from "@rakun-kit/core/manager";
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### Custom Operations
|
|
798
|
+
|
|
799
|
+
Applications can define operations in a separate object, pass that object to
|
|
800
|
+
bootstrap, and reuse its type on the frontend:
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
// server/api-operations.ts
|
|
804
|
+
import { defineOperation } from "@rakun-kit/core";
|
|
805
|
+
import { z } from "zod";
|
|
806
|
+
|
|
807
|
+
export const apiOperations = {
|
|
808
|
+
"demo.helloWorld": defineOperation<
|
|
809
|
+
{ text: string },
|
|
810
|
+
{ message: string },
|
|
811
|
+
"query",
|
|
812
|
+
"get",
|
|
813
|
+
"public"
|
|
814
|
+
>({
|
|
815
|
+
access: "public",
|
|
816
|
+
kind: "query",
|
|
817
|
+
method: "get",
|
|
818
|
+
description: "Return a hello world message with the provided text",
|
|
819
|
+
input: z.object({
|
|
820
|
+
text: z.string().default("world"),
|
|
821
|
+
}),
|
|
822
|
+
output: z.object({
|
|
823
|
+
message: z.string(),
|
|
824
|
+
}),
|
|
825
|
+
resolve: ({ input }) => ({
|
|
826
|
+
message: `Hello ${input.text}`,
|
|
827
|
+
}),
|
|
828
|
+
}),
|
|
829
|
+
};
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
// bootstrap
|
|
834
|
+
import { apiOperations } from "./server/api-operations";
|
|
835
|
+
|
|
836
|
+
rakunBootstrap({
|
|
837
|
+
// ...
|
|
838
|
+
apiOperations,
|
|
839
|
+
});
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
Operation names define their HTTP path: `demo.helloWorld` becomes
|
|
843
|
+
`/demo/helloWorld`.
|
|
844
|
+
|
|
845
|
+
Rules:
|
|
846
|
+
|
|
847
|
+
- Names must not collide with built-in operations.
|
|
848
|
+
- Operations prefixed with `manager.` are included in manager operation routers.
|
|
849
|
+
- Operations prefixed with `web.` are included in web operation routers.
|
|
850
|
+
- Operations with any other prefix are included when using the combined operation registry.
|
|
851
|
+
- Use `access: "public"` for operations called from public web pages.
|
|
852
|
+
- Use `access: "auth"` for operations that require a manager session.
|
|
853
|
+
|
|
854
|
+
The manager route `manager.apiOperations` returns a JSON-serializable operation
|
|
855
|
+
catalog for manager API docs/playgrounds. Input and output Zod schemas are
|
|
856
|
+
converted to JSON Schema for display.
|
|
857
|
+
|
|
858
|
+
### Typed API Client
|
|
859
|
+
|
|
860
|
+
`@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
|
|
861
|
+
|
|
862
|
+
```ts
|
|
863
|
+
import { createRakunApiClient, type GetClient } from "@rakun-kit/core/web";
|
|
864
|
+
import type { apiOperations } from "./server/api-operations";
|
|
865
|
+
|
|
866
|
+
type ApiClient = GetClient<typeof apiOperations>;
|
|
867
|
+
|
|
868
|
+
const client: ApiClient = createRakunApiClient<typeof apiOperations>({
|
|
869
|
+
baseUrl: "/api",
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const result = await client.query("demo.helloWorld", {
|
|
873
|
+
text: "Rakun",
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
result.message;
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
The client exposes:
|
|
880
|
+
|
|
881
|
+
- `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
|
|
882
|
+
- `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
|
|
883
|
+
|
|
884
|
+
The client derives input and output types from the Zod schemas in
|
|
885
|
+
`apiOperations`.
|
|
886
|
+
|
|
887
|
+
## Request Context and Auth
|
|
888
|
+
|
|
889
|
+
`createRequestContext(input)` normalizes headers, cookies, and response:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
const ctx = await createRequestContext({
|
|
893
|
+
headers: req.headers,
|
|
894
|
+
cookies,
|
|
895
|
+
res: {
|
|
896
|
+
setHeader: res.setHeader.bind(res),
|
|
897
|
+
cookie: res.cookie.bind(res),
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
The resulting context includes:
|
|
903
|
+
|
|
904
|
+
- `req.headers`, `req.cookies`
|
|
905
|
+
- `res.setHeader`, `res.cookie`
|
|
906
|
+
- `user`: manager user or `null`
|
|
907
|
+
- `getUser()`: returns the user or throws `AUTH_REQUIRED`
|
|
908
|
+
|
|
909
|
+
Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
|
|
910
|
+
|
|
911
|
+
## Media
|
|
912
|
+
|
|
913
|
+
Media uses a storage adapter:
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
createMediaService({
|
|
917
|
+
adapter,
|
|
918
|
+
defaultAccess: "private",
|
|
919
|
+
defaultGetExpiresInSeconds: 300,
|
|
920
|
+
uploadUrl: "/api/rakun/manager/media/upload",
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
APIs:
|
|
925
|
+
|
|
926
|
+
- `createMediaConnection(config)`: stores config without creating the service.
|
|
927
|
+
- `createMediaService(config)`: creates the singleton with an adapter.
|
|
928
|
+
- `getMediaService()`: returns the singleton or creates it from config.
|
|
929
|
+
- `handleMediaBinaryUpload`: processes manager binary uploads.
|
|
930
|
+
|
|
927
931
|
The service supports prepare/finalize upload, URL generation, folders, and image optimization depending on adapter/configuration. Manager uploads encode original file names for transport, so Unicode names (including accents, non-Latin scripts, and emoji) are preserved without placing invalid characters in HTTP headers.
|
|
928
932
|
|
|
929
|
-
Existing images can be reimported from the media manager with the selected optimization settings. The replacement uses new storage keys and updates the existing `Media` record only after every requested variant has been written, preserving its ID and content references.
|
|
930
|
-
|
|
931
|
-
## Persistent Event Log
|
|
932
|
-
|
|
933
|
-
Rakun keeps business/audit events separate from its technical console logger.
|
|
934
|
-
The event log keeps events immutable and defaults to a MongoDB collection
|
|
935
|
-
with indexes for time, type, category, outcome, severity, correlation and tags:
|
|
936
|
-
|
|
937
|
-
```ts
|
|
938
|
-
import { recordEvent, queryEvents } from "@rakun-kit/core";
|
|
939
|
-
|
|
940
|
-
await recordEvent({
|
|
941
|
-
type: "content.article.published",
|
|
942
|
-
category: "content",
|
|
943
|
-
outcome: "success",
|
|
944
|
-
actor: { type: "manager-user", id: userId },
|
|
945
|
-
resource: { type: "Article", id: articleId },
|
|
946
|
-
correlationId: requestId,
|
|
947
|
-
tags: ["editorial"],
|
|
948
|
-
data: {
|
|
949
|
-
locale: "es",
|
|
950
|
-
changedFields: 3,
|
|
951
|
-
},
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
const page = await queryEvents({
|
|
955
|
-
categories: ["content"],
|
|
956
|
-
outcomes: ["success"],
|
|
957
|
-
from: new Date("2026-01-01T00:00:00.000Z"),
|
|
958
|
-
limit: 50,
|
|
959
|
-
});
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
`data` accepts nested JSON values. Event queries use cursor pagination and can
|
|
963
|
-
filter by types, categories, severities, outcomes, sources, correlation,
|
|
964
|
-
required tags and a date range. A custom persistence implementation can be
|
|
965
|
-
plugged in globally:
|
|
966
|
-
|
|
967
|
-
```ts
|
|
968
|
-
import type { EventLogAdapter } from "@rakun-kit/core";
|
|
969
|
-
|
|
970
|
-
const adapter: EventLogAdapter = {
|
|
971
|
-
async append(event) {
|
|
972
|
-
return customStore.append(event);
|
|
973
|
-
},
|
|
974
|
-
async query(filters) {
|
|
975
|
-
return customStore.query(filters);
|
|
976
|
-
},
|
|
977
|
-
async deleteBefore(before) {
|
|
978
|
-
return customStore.deleteBefore(before);
|
|
979
|
-
},
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
rakunBootstrap({
|
|
983
|
-
// ...
|
|
984
|
-
eventLog: { adapter },
|
|
985
|
-
});
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
Plugins receive the resolved `eventLog` service in their initialization
|
|
989
|
-
context. Reading a shared event stream should be protected with the built-in
|
|
990
|
-
`system.eventLog.read` permission. The built-in `manager.logs.list` operation and the
|
|
991
|
-
manager Settings → Logs screen both enforce it.
|
|
992
|
-
|
|
993
|
-
Retention cleanup is available from the manager Logs screen to users with the
|
|
994
|
-
built-in `system.eventLog.manage` permission. It deletes events strictly older
|
|
995
|
-
than the selected cutoff. Custom adapters must implement `deleteBefore` to
|
|
996
|
-
support this action. The cleanup mutation itself is logged after deletion.
|
|
997
|
-
|
|
998
|
-
Failed API operations are persisted automatically as `api.operation.failed`
|
|
999
|
-
events. This includes expected 4xx application errors and unexpected 5xx
|
|
1000
|
-
failures across the core operation wrapper and the Express, Next.js, and tRPC
|
|
1001
|
-
adapters. Events include the operation, status, method, kind, correlation ID,
|
|
1002
|
-
and authenticated actor when available. Request payloads, application error
|
|
1003
|
-
causes, and raw internal error messages are not persisted.
|
|
1004
|
-
|
|
1005
|
-
Every successful API mutation is also persisted with an operation-specific
|
|
1006
|
-
event type such as `manager.create.succeeded` or
|
|
1007
|
-
`manager.backups.restore.succeeded`. Successful queries are not recorded by
|
|
1008
|
-
default, which keeps the event stream focused on state changes. Mutation
|
|
1009
|
-
events contain operation metadata and actor/correlation context, but never
|
|
1010
|
-
copy the input payload or result.
|
|
1011
|
-
|
|
1012
|
-
## Mail
|
|
1013
|
-
|
|
1014
|
-
Mail providers receive normalized, already-rendered messages through
|
|
1015
|
-
`MailAdapter`, so neither `core` nor an adapter depends on React or a template
|
|
1016
|
-
engine:
|
|
1017
|
-
|
|
1018
|
-
```ts
|
|
1019
|
-
import type { MailAdapter } from "@rakun-kit/core";
|
|
1020
|
-
|
|
1021
|
-
const adapter: MailAdapter = {
|
|
1022
|
-
async send(message) {
|
|
1023
|
-
const result = await provider.send(message);
|
|
1024
|
-
return { id: result.id };
|
|
1025
|
-
},
|
|
1026
|
-
};
|
|
1027
|
-
|
|
1028
|
-
rakunBootstrap({
|
|
1029
|
-
// ...
|
|
1030
|
-
mail: {
|
|
1031
|
-
adapter,
|
|
1032
|
-
defaultFrom: "hello@example.com",
|
|
1033
|
-
defaultReplyTo: "support@example.com",
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
Send rendered content directly:
|
|
1039
|
-
|
|
1040
|
-
```ts
|
|
1041
|
-
import { sendMail } from "@rakun-kit/core";
|
|
1042
|
-
|
|
1043
|
-
await sendMail({
|
|
1044
|
-
to: "ada@example.com",
|
|
1045
|
-
subject: "Welcome",
|
|
1046
|
-
html: "<p>Hello Ada</p>",
|
|
1047
|
-
text: "Hello Ada",
|
|
1048
|
-
});
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
Or create a typed application template registry:
|
|
1052
|
-
|
|
1053
|
-
```ts
|
|
1054
|
-
import { createMailSender, defineMailTemplate } from "@rakun-kit/core";
|
|
1055
|
-
|
|
1056
|
-
const mail = createMailSender({
|
|
1057
|
-
templates: {
|
|
1058
|
-
welcome: defineMailTemplate<{ name: string }>({
|
|
1059
|
-
subject: ({ name }) => `Welcome, ${name}`,
|
|
1060
|
-
render: ({ name }) => ({
|
|
1061
|
-
html: `<p>Hello ${name}</p>`,
|
|
1062
|
-
text: `Hello ${name}`,
|
|
1063
|
-
}),
|
|
1064
|
-
}),
|
|
1065
|
-
},
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
await mail.send({
|
|
1069
|
-
template: "welcome",
|
|
1070
|
-
props: { name: "Ada" },
|
|
1071
|
-
to: "ada@example.com",
|
|
1072
|
-
});
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
The common contract supports To/CC/BCC/Reply-To, custom headers and in-memory
|
|
1076
|
-
`Uint8Array` attachments. Sending is immediate; queues, retries and delivery
|
|
1077
|
-
events belong to application infrastructure.
|
|
1078
|
-
|
|
1079
|
-
Every mail sent through a bootstrapped Rakun mail service creates append-only
|
|
1080
|
-
`mail.send.attempted` and `mail.send.succeeded` or `mail.send.failed` events.
|
|
1081
|
-
They share a correlation id and include only operational counts, provider,
|
|
1082
|
-
template and duration. Recipient addresses, subject, HTML/text, headers,
|
|
1083
|
-
attachment names/content, credentials and raw provider errors are never copied
|
|
1084
|
-
to the persistent event log. If the initial attempt event cannot be persisted,
|
|
1085
|
-
the provider is not called.
|
|
1086
|
-
|
|
1087
|
-
## Literals and Translation
|
|
1088
|
-
|
|
1089
|
-
Bootstrap receives `literals`. Related utilities:
|
|
1090
|
-
|
|
1091
|
-
- `getTranslation`: resolves translatable values.
|
|
1092
|
-
- `translateObject`: translates objects with translatable fields.
|
|
1093
|
-
- Manager schemas for listing/upserting website literals (not manager UI chrome).
|
|
1094
|
-
|
|
1095
|
-
Translatable values use this shape:
|
|
1096
|
-
|
|
1097
|
-
```ts
|
|
1098
|
-
{
|
|
1099
|
-
_tag: "Translatable",
|
|
1100
|
-
en: "Title",
|
|
1101
|
-
es: "Titulo"
|
|
1102
|
-
}
|
|
1103
|
-
```
|
|
1104
|
-
|
|
1105
|
-
## Permissions
|
|
1106
|
-
|
|
1107
|
-
`lib/Permissions` exposes:
|
|
1108
|
-
|
|
1109
|
-
- `getPermissionList`: generates a permission list.
|
|
1110
|
-
- `hasPermissions`: validates user/role permissions.
|
|
1111
|
-
- `mapPermissions`: transforms permissions.
|
|
1112
|
-
- `Permission`: public type.
|
|
1113
|
-
|
|
1114
|
-
Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
|
|
1115
|
-
|
|
1116
|
-
## Errors
|
|
1117
|
-
|
|
1118
|
-
Application errors live in `lib/errors`:
|
|
1119
|
-
|
|
1120
|
-
- `AppError`
|
|
1121
|
-
- `throwAppError`
|
|
1122
|
-
- `errors`
|
|
1123
|
-
- `instanceofAppErrorShape`
|
|
1124
|
-
|
|
1125
|
-
Database errors live in `orm/dbService`:
|
|
1126
|
-
|
|
1127
|
-
- `DbError`
|
|
1128
|
-
- `DbErrorUnknown`
|
|
1129
|
-
- `DbErrorNotFound`
|
|
1130
|
-
- `DbErrorInvalidData`
|
|
1131
|
-
- `DbErrorConflict`
|
|
1132
|
-
- `DbErrorSimulatedFailure`
|
|
1133
|
-
|
|
1134
|
-
## Runtime Flow
|
|
1135
|
-
|
|
1136
|
-
1. The app defines content types with `ContentType` and `f`.
|
|
1137
|
-
2. The app calls `rakunBootstrap(options)`.
|
|
1138
|
-
3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
|
|
1139
|
-
4. `ensureRakunInitialized()` configures logger, MongoDB, the persistent event
|
|
1140
|
-
log, media, mail, and route syncing.
|
|
1141
|
-
5. Each request creates a `RakunRequestContext`.
|
|
1142
|
-
6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
|
|
1143
|
-
|
|
1144
|
-
## Tests and Development Notes
|
|
1145
|
-
|
|
1146
|
-
The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
|
|
1147
|
-
|
|
1148
|
-
Package build:
|
|
1149
|
-
|
|
1150
|
-
```sh
|
|
1151
|
-
bun run build --workspace @rakun-kit/core
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.
|
|
933
|
+
Existing images can be reimported from the media manager with the selected optimization settings. The replacement uses new storage keys and updates the existing `Media` record only after every requested variant has been written, preserving its ID and content references. Preview generation stores a compact `data:image/...` string on `previewUrl` for LQIP use in `@rakun-kit/react` `Image`, instead of writing a separate preview object to storage.
|
|
934
|
+
|
|
935
|
+
## Persistent Event Log
|
|
936
|
+
|
|
937
|
+
Rakun keeps business/audit events separate from its technical console logger.
|
|
938
|
+
The event log keeps events immutable and defaults to a MongoDB collection
|
|
939
|
+
with indexes for time, type, category, outcome, severity, correlation and tags:
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
import { recordEvent, queryEvents } from "@rakun-kit/core";
|
|
943
|
+
|
|
944
|
+
await recordEvent({
|
|
945
|
+
type: "content.article.published",
|
|
946
|
+
category: "content",
|
|
947
|
+
outcome: "success",
|
|
948
|
+
actor: { type: "manager-user", id: userId },
|
|
949
|
+
resource: { type: "Article", id: articleId },
|
|
950
|
+
correlationId: requestId,
|
|
951
|
+
tags: ["editorial"],
|
|
952
|
+
data: {
|
|
953
|
+
locale: "es",
|
|
954
|
+
changedFields: 3,
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const page = await queryEvents({
|
|
959
|
+
categories: ["content"],
|
|
960
|
+
outcomes: ["success"],
|
|
961
|
+
from: new Date("2026-01-01T00:00:00.000Z"),
|
|
962
|
+
limit: 50,
|
|
963
|
+
});
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
`data` accepts nested JSON values. Event queries use cursor pagination and can
|
|
967
|
+
filter by types, categories, severities, outcomes, sources, correlation,
|
|
968
|
+
required tags and a date range. A custom persistence implementation can be
|
|
969
|
+
plugged in globally:
|
|
970
|
+
|
|
971
|
+
```ts
|
|
972
|
+
import type { EventLogAdapter } from "@rakun-kit/core";
|
|
973
|
+
|
|
974
|
+
const adapter: EventLogAdapter = {
|
|
975
|
+
async append(event) {
|
|
976
|
+
return customStore.append(event);
|
|
977
|
+
},
|
|
978
|
+
async query(filters) {
|
|
979
|
+
return customStore.query(filters);
|
|
980
|
+
},
|
|
981
|
+
async deleteBefore(before) {
|
|
982
|
+
return customStore.deleteBefore(before);
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
rakunBootstrap({
|
|
987
|
+
// ...
|
|
988
|
+
eventLog: { adapter },
|
|
989
|
+
});
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
Plugins receive the resolved `eventLog` service in their initialization
|
|
993
|
+
context. Reading a shared event stream should be protected with the built-in
|
|
994
|
+
`system.eventLog.read` permission. The built-in `manager.logs.list` operation and the
|
|
995
|
+
manager Settings → Logs screen both enforce it.
|
|
996
|
+
|
|
997
|
+
Retention cleanup is available from the manager Logs screen to users with the
|
|
998
|
+
built-in `system.eventLog.manage` permission. It deletes events strictly older
|
|
999
|
+
than the selected cutoff. Custom adapters must implement `deleteBefore` to
|
|
1000
|
+
support this action. The cleanup mutation itself is logged after deletion.
|
|
1001
|
+
|
|
1002
|
+
Failed API operations are persisted automatically as `api.operation.failed`
|
|
1003
|
+
events. This includes expected 4xx application errors and unexpected 5xx
|
|
1004
|
+
failures across the core operation wrapper and the Express, Next.js, and tRPC
|
|
1005
|
+
adapters. Events include the operation, status, method, kind, correlation ID,
|
|
1006
|
+
and authenticated actor when available. Request payloads, application error
|
|
1007
|
+
causes, and raw internal error messages are not persisted.
|
|
1008
|
+
|
|
1009
|
+
Every successful API mutation is also persisted with an operation-specific
|
|
1010
|
+
event type such as `manager.create.succeeded` or
|
|
1011
|
+
`manager.backups.restore.succeeded`. Successful queries are not recorded by
|
|
1012
|
+
default, which keeps the event stream focused on state changes. Mutation
|
|
1013
|
+
events contain operation metadata and actor/correlation context, but never
|
|
1014
|
+
copy the input payload or result.
|
|
1015
|
+
|
|
1016
|
+
## Mail
|
|
1017
|
+
|
|
1018
|
+
Mail providers receive normalized, already-rendered messages through
|
|
1019
|
+
`MailAdapter`, so neither `core` nor an adapter depends on React or a template
|
|
1020
|
+
engine:
|
|
1021
|
+
|
|
1022
|
+
```ts
|
|
1023
|
+
import type { MailAdapter } from "@rakun-kit/core";
|
|
1024
|
+
|
|
1025
|
+
const adapter: MailAdapter = {
|
|
1026
|
+
async send(message) {
|
|
1027
|
+
const result = await provider.send(message);
|
|
1028
|
+
return { id: result.id };
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
rakunBootstrap({
|
|
1033
|
+
// ...
|
|
1034
|
+
mail: {
|
|
1035
|
+
adapter,
|
|
1036
|
+
defaultFrom: "hello@example.com",
|
|
1037
|
+
defaultReplyTo: "support@example.com",
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
Send rendered content directly:
|
|
1043
|
+
|
|
1044
|
+
```ts
|
|
1045
|
+
import { sendMail } from "@rakun-kit/core";
|
|
1046
|
+
|
|
1047
|
+
await sendMail({
|
|
1048
|
+
to: "ada@example.com",
|
|
1049
|
+
subject: "Welcome",
|
|
1050
|
+
html: "<p>Hello Ada</p>",
|
|
1051
|
+
text: "Hello Ada",
|
|
1052
|
+
});
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
Or create a typed application template registry:
|
|
1056
|
+
|
|
1057
|
+
```ts
|
|
1058
|
+
import { createMailSender, defineMailTemplate } from "@rakun-kit/core";
|
|
1059
|
+
|
|
1060
|
+
const mail = createMailSender({
|
|
1061
|
+
templates: {
|
|
1062
|
+
welcome: defineMailTemplate<{ name: string }>({
|
|
1063
|
+
subject: ({ name }) => `Welcome, ${name}`,
|
|
1064
|
+
render: ({ name }) => ({
|
|
1065
|
+
html: `<p>Hello ${name}</p>`,
|
|
1066
|
+
text: `Hello ${name}`,
|
|
1067
|
+
}),
|
|
1068
|
+
}),
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
await mail.send({
|
|
1073
|
+
template: "welcome",
|
|
1074
|
+
props: { name: "Ada" },
|
|
1075
|
+
to: "ada@example.com",
|
|
1076
|
+
});
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
The common contract supports To/CC/BCC/Reply-To, custom headers and in-memory
|
|
1080
|
+
`Uint8Array` attachments. Sending is immediate; queues, retries and delivery
|
|
1081
|
+
events belong to application infrastructure.
|
|
1082
|
+
|
|
1083
|
+
Every mail sent through a bootstrapped Rakun mail service creates append-only
|
|
1084
|
+
`mail.send.attempted` and `mail.send.succeeded` or `mail.send.failed` events.
|
|
1085
|
+
They share a correlation id and include only operational counts, provider,
|
|
1086
|
+
template and duration. Recipient addresses, subject, HTML/text, headers,
|
|
1087
|
+
attachment names/content, credentials and raw provider errors are never copied
|
|
1088
|
+
to the persistent event log. If the initial attempt event cannot be persisted,
|
|
1089
|
+
the provider is not called.
|
|
1090
|
+
|
|
1091
|
+
## Literals and Translation
|
|
1092
|
+
|
|
1093
|
+
Bootstrap receives `literals`. Related utilities:
|
|
1094
|
+
|
|
1095
|
+
- `getTranslation`: resolves translatable values.
|
|
1096
|
+
- `translateObject`: translates objects with translatable fields.
|
|
1097
|
+
- Manager schemas for listing/upserting website literals (not manager UI chrome).
|
|
1098
|
+
|
|
1099
|
+
Translatable values use this shape:
|
|
1100
|
+
|
|
1101
|
+
```ts
|
|
1102
|
+
{
|
|
1103
|
+
_tag: "Translatable",
|
|
1104
|
+
en: "Title",
|
|
1105
|
+
es: "Titulo"
|
|
1106
|
+
}
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
## Permissions
|
|
1110
|
+
|
|
1111
|
+
`lib/Permissions` exposes:
|
|
1112
|
+
|
|
1113
|
+
- `getPermissionList`: generates a permission list.
|
|
1114
|
+
- `hasPermissions`: validates user/role permissions.
|
|
1115
|
+
- `mapPermissions`: transforms permissions.
|
|
1116
|
+
- `Permission`: public type.
|
|
1117
|
+
|
|
1118
|
+
Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
|
|
1119
|
+
|
|
1120
|
+
## Errors
|
|
1121
|
+
|
|
1122
|
+
Application errors live in `lib/errors`:
|
|
1123
|
+
|
|
1124
|
+
- `AppError`
|
|
1125
|
+
- `throwAppError`
|
|
1126
|
+
- `errors`
|
|
1127
|
+
- `instanceofAppErrorShape`
|
|
1128
|
+
|
|
1129
|
+
Database errors live in `orm/dbService`:
|
|
1130
|
+
|
|
1131
|
+
- `DbError`
|
|
1132
|
+
- `DbErrorUnknown`
|
|
1133
|
+
- `DbErrorNotFound`
|
|
1134
|
+
- `DbErrorInvalidData`
|
|
1135
|
+
- `DbErrorConflict`
|
|
1136
|
+
- `DbErrorSimulatedFailure`
|
|
1137
|
+
|
|
1138
|
+
## Runtime Flow
|
|
1139
|
+
|
|
1140
|
+
1. The app defines content types with `ContentType` and `f`.
|
|
1141
|
+
2. The app calls `rakunBootstrap(options)`.
|
|
1142
|
+
3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
|
|
1143
|
+
4. `ensureRakunInitialized()` configures logger, MongoDB, the persistent event
|
|
1144
|
+
log, media, mail, and route syncing.
|
|
1145
|
+
5. Each request creates a `RakunRequestContext`.
|
|
1146
|
+
6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
|
|
1147
|
+
|
|
1148
|
+
## Tests and Development Notes
|
|
1149
|
+
|
|
1150
|
+
The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
|
|
1151
|
+
|
|
1152
|
+
Package build:
|
|
1153
|
+
|
|
1154
|
+
```sh
|
|
1155
|
+
bun run build --workspace @rakun-kit/core
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.
|