howone 0.1.11 → 0.1.12

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.
@@ -0,0 +1,299 @@
1
+ # Raw HTTP
2
+
3
+ ## When to Use
4
+
5
+ Use `client.raw` when you need to call a custom backend endpoint that is **not** covered by `client.entities` or `client.ai`. The raw client is an Axios-based HTTP client that automatically attaches the HowOne auth token and project ID headers.
6
+
7
+ **Do not use `client.raw` to re-implement entity operations or AI workflows** — use the typed SDK methods instead.
8
+
9
+ ---
10
+
11
+ ## The RawHttpClient Interface
12
+
13
+ ```ts
14
+ type RawHttpClient = {
15
+ instance: AxiosInstance
16
+
17
+ // All methods return Promise<AxiosResponse>
18
+ request(config: RequestConfig): Promise<AxiosResponse>
19
+ get(config: RequestConfig): Promise<AxiosResponse>
20
+ post(config: RequestConfig): Promise<AxiosResponse>
21
+ put(config: RequestConfig): Promise<AxiosResponse>
22
+ patch(config: RequestConfig): Promise<AxiosResponse>
23
+ delete(config: RequestConfig): Promise<AxiosResponse>
24
+
25
+ // Cancel an in-flight request by URL
26
+ cancelRequest(url: string): void
27
+
28
+ // Cancel all in-flight requests
29
+ cancelAllRequests(): void
30
+ }
31
+ ```
32
+
33
+ ### RequestConfig
34
+
35
+ `RequestConfig` extends `AxiosRequestConfig` with optional interceptors:
36
+
37
+ ```ts
38
+ type RequestConfig<T = AxiosResponse> = AxiosRequestConfig & {
39
+ interceptors?: {
40
+ requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
41
+ requestInterceptorCatch?: (error: any) => any
42
+ responseInterceptor?: (res: T) => T
43
+ responseInterceptorCatch?: (error: any) => any
44
+ }
45
+ showLoading?: boolean
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Basic Usage
52
+
53
+ ```ts
54
+ import howone from '@/lib/sdk'
55
+
56
+ // GET
57
+ const response = await howone.raw.get({
58
+ url: '/api/custom/stats',
59
+ })
60
+ const data = response.data // untyped AxiosResponse.data
61
+
62
+ // POST with body
63
+ const response = await howone.raw.post({
64
+ url: '/api/custom/send-notification',
65
+ data: {
66
+ userId: '123',
67
+ message: 'Hello!',
68
+ },
69
+ })
70
+
71
+ // PUT
72
+ const response = await howone.raw.put({
73
+ url: `/api/custom/profile/${userId}`,
74
+ data: { displayName: 'Alice' },
75
+ })
76
+
77
+ // PATCH
78
+ const response = await howone.raw.patch({
79
+ url: `/api/custom/settings`,
80
+ data: { theme: 'dark' },
81
+ })
82
+
83
+ // DELETE
84
+ const response = await howone.raw.delete({
85
+ url: `/api/custom/sessions/${sessionId}`,
86
+ })
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Typed Responses
92
+
93
+ Wrap with generics for type safety:
94
+
95
+ ```ts
96
+ type StatsResponse = {
97
+ totalUsers: number
98
+ activeToday: number
99
+ storageUsed: number
100
+ }
101
+
102
+ const response = await howone.raw.get<StatsResponse>({
103
+ url: '/api/custom/stats',
104
+ })
105
+
106
+ const stats = response.data // typed as StatsResponse
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Query Parameters
112
+
113
+ ```ts
114
+ // Pass query params via the `params` field (Axios serializes them automatically)
115
+ const response = await howone.raw.get({
116
+ url: '/api/custom/search',
117
+ params: {
118
+ q: 'dragons',
119
+ page: 1,
120
+ limit: 20,
121
+ sort: 'createdAt',
122
+ order: 'desc',
123
+ },
124
+ })
125
+ // Calls: GET /api/custom/search?q=dragons&page=1&limit=20&sort=createdAt&order=desc
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Custom Headers
131
+
132
+ ```ts
133
+ const response = await howone.raw.post({
134
+ url: '/api/custom/webhook',
135
+ data: payload,
136
+ headers: {
137
+ 'X-Webhook-Secret': import.meta.env.VITE_WEBHOOK_SECRET,
138
+ 'X-Request-Source': 'app',
139
+ },
140
+ })
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Request Cancellation
146
+
147
+ ```ts
148
+ // Cancel a specific request by URL
149
+ howone.raw.cancelRequest('/api/custom/long-running')
150
+
151
+ // Cancel all in-flight requests (e.g. on page unmount)
152
+ howone.raw.cancelAllRequests()
153
+
154
+ // Pattern: cancel on component unmount
155
+ import { useEffect } from 'react'
156
+ import howone from '@/lib/sdk'
157
+
158
+ function DataComponent() {
159
+ useEffect(() => {
160
+ howone.raw.get({ url: '/api/custom/data' })
161
+ .then(res => setData(res.data))
162
+
163
+ return () => {
164
+ howone.raw.cancelRequest('/api/custom/data')
165
+ }
166
+ }, [])
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Per-Request Interceptors
173
+
174
+ For one-off request/response transforms without modifying the global client:
175
+
176
+ ```ts
177
+ const response = await howone.raw.post({
178
+ url: '/api/custom/transform',
179
+ data: payload,
180
+ interceptors: {
181
+ requestInterceptor: (config) => {
182
+ // Modify request config (e.g. add timestamp)
183
+ config.headers['X-Timestamp'] = Date.now().toString()
184
+ return config
185
+ },
186
+ responseInterceptor: (res) => {
187
+ // Log response time or transform data
188
+ console.log('Response status:', res.status)
189
+ return res
190
+ },
191
+ responseInterceptorCatch: (error) => {
192
+ // Handle specific error codes
193
+ if (error.response?.status === 503) {
194
+ console.error('Service temporarily unavailable')
195
+ }
196
+ return Promise.reject(error)
197
+ },
198
+ },
199
+ })
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Direct Axios Instance Access
205
+
206
+ For maximum control (multipart forms, streaming responses, etc.):
207
+
208
+ ```ts
209
+ const instance = howone.raw.instance // Axios instance
210
+
211
+ // Multipart form data
212
+ const formData = new FormData()
213
+ formData.append('report', file)
214
+ formData.append('meta', JSON.stringify({ type: 'monthly' }))
215
+
216
+ const response = await instance.post('/api/custom/reports', formData, {
217
+ headers: { 'Content-Type': 'multipart/form-data' },
218
+ onUploadProgress: (e) => {
219
+ const percent = Math.round((e.loaded * 100) / (e.total ?? 1))
220
+ setProgress(percent)
221
+ },
222
+ })
223
+ ```
224
+
225
+ ---
226
+
227
+ ## React Pattern: Data Fetching with Raw HTTP
228
+
229
+ ```tsx
230
+ import { useEffect, useState } from 'react'
231
+ import howone from '@/lib/sdk'
232
+
233
+ type AnalyticsData = {
234
+ views: number
235
+ clicks: number
236
+ conversions: number
237
+ period: string
238
+ }
239
+
240
+ function Analytics({ projectId }: { projectId: string }) {
241
+ const [data, setData] = useState<AnalyticsData | null>(null)
242
+ const [loading, setLoading] = useState(true)
243
+ const [error, setError] = useState<string | null>(null)
244
+
245
+ useEffect(() => {
246
+ let cancelled = false
247
+
248
+ howone.raw.get<AnalyticsData>({
249
+ url: '/api/custom/analytics',
250
+ params: { projectId, period: '30d' },
251
+ })
252
+ .then(res => { if (!cancelled) setData(res.data) })
253
+ .catch(err => { if (!cancelled) setError(err.message) })
254
+ .finally(() => { if (!cancelled) setLoading(false) })
255
+
256
+ return () => {
257
+ cancelled = true
258
+ howone.raw.cancelRequest('/api/custom/analytics')
259
+ }
260
+ }, [projectId])
261
+
262
+ if (loading) return <div>Loading analytics...</div>
263
+ if (error) return <div>Error: {error}</div>
264
+ if (!data) return null
265
+
266
+ return (
267
+ <div>
268
+ <p>Views: {data.views}</p>
269
+ <p>Clicks: {data.clicks}</p>
270
+ <p>Conversions: {data.conversions}</p>
271
+ </div>
272
+ )
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## client.raw vs client.entities
279
+
280
+ | Use Case | Recommended API |
281
+ |---|---|
282
+ | CRUD on a HowOne entity | `howone.entities.<Entity>.*` |
283
+ | Querying with pagination/filter/sort | `howone.entities.<Entity>.query()` |
284
+ | Running an AI workflow | `howone.ai.<action>.run()` |
285
+ | Calling a custom backend route | `howone.raw.get/post/...` |
286
+ | Sending webhooks or notifications | `howone.raw.post()` |
287
+ | Fetching analytics or aggregated data not in entities | `howone.raw.get()` |
288
+ | Uploading files | `howone.upload.*` |
289
+
290
+ ---
291
+
292
+ ## Common Mistakes
293
+
294
+ | Mistake | Correct Pattern |
295
+ |---|---|
296
+ | `howone.raw.get({ url: '/entities/Story' })` to read entities | Use `howone.entities.Story.query()` |
297
+ | Not handling Axios errors (`.response.status`) | Wrap in try/catch and check `error.response?.status` |
298
+ | Calling `cancelAllRequests()` too broadly | Use `cancelRequest(url)` for surgical cancellation |
299
+ | Forgetting that `response.data` is untyped by default | Pass the type parameter: `howone.raw.get<MyType>(...)` |
@@ -0,0 +1,400 @@
1
+ # Manifest Codegen
2
+
3
+ ## Overview
4
+
5
+ HowOne apps are driven by two backend-synced manifests:
6
+
7
+ | File | Contents | Drives |
8
+ |---|---|---|
9
+ | `.howone/database/manifest.json` | Entity names, fields, types | Entity type definitions + `client.entity<...>` bindings |
10
+ | `.howone/ai/manifest.json` | AI action IDs, input/output JSON schemas | zod schemas + `defineAiAction` bindings |
11
+
12
+ **The coding agent should always generate `src/lib/sdk.ts` from these manifest files, not from memory or assumptions.**
13
+
14
+ Sync tools (`sync_schema_artifacts`, `sync_ai_artifacts`) write the manifests. The coding agent reads the manifests and writes `src/lib/sdk.ts`.
15
+
16
+ ---
17
+
18
+ ## Reading `.howone/database/manifest.json`
19
+
20
+ ### Example manifest
21
+
22
+ ```json
23
+ {
24
+ "version": "1",
25
+ "entities": [
26
+ {
27
+ "name": "Story",
28
+ "fields": [
29
+ { "name": "title", "type": "string", "required": true },
30
+ { "name": "content", "type": "text", "required": true },
31
+ { "name": "authorId", "type": "string", "required": true },
32
+ { "name": "status", "type": "string", "required": true, "enum": ["draft", "published", "archived"] },
33
+ { "name": "wordCount", "type": "integer", "required": true },
34
+ { "name": "tags", "type": "array", "items": "string", "required": false },
35
+ { "name": "coverUrl", "type": "string", "required": false }
36
+ ]
37
+ },
38
+ {
39
+ "name": "Comment",
40
+ "fields": [
41
+ { "name": "storyId", "type": "string", "required": true },
42
+ { "name": "authorId", "type": "string", "required": true },
43
+ { "name": "body", "type": "text", "required": true },
44
+ { "name": "likes", "type": "integer", "required": false }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ ### Field type → TypeScript type mapping
52
+
53
+ | Manifest type | TypeScript type |
54
+ |---|---|
55
+ | `string` | `string` |
56
+ | `text` | `string` |
57
+ | `integer` | `number` |
58
+ | `number` / `float` | `number` |
59
+ | `boolean` | `boolean` |
60
+ | `date` / `datetime` | `string` (ISO 8601) |
61
+ | `array` (items: string) | `string[]` |
62
+ | `array` (items: object) | `Record<string, unknown>[]` or inline type |
63
+ | `object` | `Record<string, unknown>` |
64
+ | `enum` | `'value1' \| 'value2' \| ...` |
65
+
66
+ - Fields in `required: true` are non-optional in `Record` and `Create` types.
67
+ - Fields in `required: false` (or absent from required list) get `?` in `Record` and are optional in `Create`.
68
+
69
+ ### Generated TypeScript from the example manifest
70
+
71
+ ```ts
72
+ import { type EntityRecord } from '@howone/sdk'
73
+
74
+ // ── Story ─────────────────────────────────────────────────────
75
+ export type StoryRecord = EntityRecord & {
76
+ title: string
77
+ content: string
78
+ authorId: string
79
+ status: 'draft' | 'published' | 'archived'
80
+ wordCount: number
81
+ tags?: string[]
82
+ coverUrl?: string
83
+ }
84
+
85
+ export type StoryCreate = {
86
+ title: string
87
+ content: string
88
+ authorId: string
89
+ status: 'draft' | 'published' | 'archived'
90
+ wordCount: number
91
+ tags?: string[]
92
+ coverUrl?: string
93
+ }
94
+
95
+ export type StoryUpdate = Partial<StoryCreate>
96
+
97
+ // ── Comment ───────────────────────────────────────────────────
98
+ export type CommentRecord = EntityRecord & {
99
+ storyId: string
100
+ authorId: string
101
+ body: string
102
+ likes?: number
103
+ }
104
+
105
+ export type CommentCreate = {
106
+ storyId: string
107
+ authorId: string
108
+ body: string
109
+ likes?: number
110
+ }
111
+
112
+ export type CommentUpdate = Partial<CommentCreate>
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Reading `.howone/ai/manifest.json`
118
+
119
+ ### Example manifest
120
+
121
+ ```json
122
+ {
123
+ "version": "1",
124
+ "actions": [
125
+ {
126
+ "id": "generateStory",
127
+ "name": "Generate Story",
128
+ "inputSchema": {
129
+ "type": "object",
130
+ "properties": {
131
+ "topic": { "type": "string" },
132
+ "ageRange": { "type": "string", "enum": ["3-5", "6-8", "9-12"] },
133
+ "language": { "type": "string" }
134
+ },
135
+ "required": ["topic", "ageRange"]
136
+ },
137
+ "outputSchema": {
138
+ "type": "object",
139
+ "properties": {
140
+ "title": { "type": "string" },
141
+ "content": { "type": "string" },
142
+ "summary": { "type": "string" }
143
+ },
144
+ "required": ["title", "content"]
145
+ }
146
+ },
147
+ {
148
+ "id": "translateText",
149
+ "name": "Translate Text",
150
+ "inputSchema": {
151
+ "type": "object",
152
+ "properties": {
153
+ "text": { "type": "string" },
154
+ "targetLang": { "type": "string" },
155
+ "formality": { "type": "string", "enum": ["formal", "informal"] }
156
+ },
157
+ "required": ["text", "targetLang"]
158
+ }
159
+ }
160
+ ]
161
+ }
162
+ ```
163
+
164
+ ### JSON Schema → zod mapping
165
+
166
+ | JSON Schema | zod |
167
+ |---|---|
168
+ | `{ "type": "string" }` | `z.string()` |
169
+ | `{ "type": "number" }` | `z.number()` |
170
+ | `{ "type": "integer" }` | `z.number().int()` |
171
+ | `{ "type": "boolean" }` | `z.boolean()` |
172
+ | `{ "type": "string", "enum": ["a","b"] }` | `z.enum(['a', 'b'])` |
173
+ | `{ "type": "array", "items": { "type": "string" } }` | `z.array(z.string())` |
174
+ | `{ "type": "object", "properties": { ... } }` | `z.object({ ... })` |
175
+ | Field NOT in `required[]` | append `.optional()` |
176
+
177
+ ### Generated zod schemas from the example manifest
178
+
179
+ ```ts
180
+ import { z } from 'zod'
181
+
182
+ // ── generateStory ─────────────────────────────────────────────
183
+ export const generateStoryInputSchema = z.object({
184
+ topic: z.string(),
185
+ ageRange: z.enum(['3-5', '6-8', '9-12']),
186
+ language: z.string().optional(),
187
+ })
188
+ export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
189
+
190
+ // Output type for manual unwrapping (do NOT put in defineAiAction as outputSchema)
191
+ export type GenerateStoryOutput = {
192
+ title: string
193
+ content: string
194
+ summary?: string
195
+ }
196
+
197
+ // ── translateText ─────────────────────────────────────────────
198
+ export const translateTextInputSchema = z.object({
199
+ text: z.string(),
200
+ targetLang: z.string(),
201
+ formality: z.enum(['formal', 'informal']).optional(),
202
+ })
203
+ export type TranslateTextInput = z.infer<typeof translateTextInputSchema>
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Full Generated `src/lib/sdk.ts`
209
+
210
+ Combining both manifests from the examples above:
211
+
212
+ ```ts
213
+ // src/lib/sdk.ts
214
+ // Generated from .howone/database/manifest.json and .howone/ai/manifest.json
215
+ import {
216
+ createClient,
217
+ defineAiAction,
218
+ defineAiActions,
219
+ defineEntities,
220
+ type EntityRecord,
221
+ withAiActions,
222
+ withEntities,
223
+ } from '@howone/sdk'
224
+ import { z } from 'zod'
225
+
226
+ // ═══════════════════════════════════════════════════════════════
227
+ // ENTITY TYPES
228
+ // ═══════════════════════════════════════════════════════════════
229
+
230
+ export type StoryRecord = EntityRecord & {
231
+ title: string
232
+ content: string
233
+ authorId: string
234
+ status: 'draft' | 'published' | 'archived'
235
+ wordCount: number
236
+ tags?: string[]
237
+ coverUrl?: string
238
+ }
239
+ export type StoryCreate = {
240
+ title: string
241
+ content: string
242
+ authorId: string
243
+ status: 'draft' | 'published' | 'archived'
244
+ wordCount: number
245
+ tags?: string[]
246
+ coverUrl?: string
247
+ }
248
+ export type StoryUpdate = Partial<StoryCreate>
249
+
250
+ export type CommentRecord = EntityRecord & {
251
+ storyId: string
252
+ authorId: string
253
+ body: string
254
+ likes?: number
255
+ }
256
+ export type CommentCreate = {
257
+ storyId: string
258
+ authorId: string
259
+ body: string
260
+ likes?: number
261
+ }
262
+ export type CommentUpdate = Partial<CommentCreate>
263
+
264
+ // ═══════════════════════════════════════════════════════════════
265
+ // AI SCHEMAS & TYPES
266
+ // ═══════════════════════════════════════════════════════════════
267
+
268
+ export const generateStoryInputSchema = z.object({
269
+ topic: z.string(),
270
+ ageRange: z.enum(['3-5', '6-8', '9-12']),
271
+ language: z.string().optional(),
272
+ })
273
+ export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
274
+ export type GenerateStoryOutput = {
275
+ title: string
276
+ content: string
277
+ summary?: string
278
+ }
279
+
280
+ export const translateTextInputSchema = z.object({
281
+ text: z.string(),
282
+ targetLang: z.string(),
283
+ formality: z.enum(['formal', 'informal']).optional(),
284
+ })
285
+ export type TranslateTextInput = z.infer<typeof translateTextInputSchema>
286
+ export type TranslateTextOutput = {
287
+ translatedText: string
288
+ detectedLang?: string
289
+ }
290
+
291
+ // ═══════════════════════════════════════════════════════════════
292
+ // CLIENT
293
+ // ═══════════════════════════════════════════════════════════════
294
+
295
+ const client = createClient({
296
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
297
+ env: import.meta.env.VITE_HOWONE_ENV,
298
+ })
299
+
300
+ // ═══════════════════════════════════════════════════════════════
301
+ // ENTITY BINDINGS
302
+ // ═══════════════════════════════════════════════════════════════
303
+
304
+ export const entities = defineEntities({
305
+ Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
306
+ Comment: client.entity<CommentRecord, CommentCreate, CommentUpdate>('Comment'),
307
+ })
308
+
309
+ // ═══════════════════════════════════════════════════════════════
310
+ // AI ACTION BINDINGS
311
+ // ═══════════════════════════════════════════════════════════════
312
+
313
+ export const ai = defineAiActions({
314
+ generateStory: defineAiAction('generateStory', {
315
+ inputSchema: generateStoryInputSchema,
316
+ }),
317
+ translateText: defineAiAction('translateText', {
318
+ inputSchema: translateTextInputSchema,
319
+ }),
320
+ })
321
+
322
+ // ═══════════════════════════════════════════════════════════════
323
+ // COMPOSED CLIENT
324
+ // ═══════════════════════════════════════════════════════════════
325
+
326
+ const howone = withAiActions(withEntities(client, entities), ai)
327
+ export default howone
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Codegen Checklist
333
+
334
+ Before finalising generated code, verify:
335
+
336
+ - [ ] Every entity from `.howone/database/manifest.json` has a `Record`, `Create`, and `Update` type
337
+ - [ ] `Create` types are defined **explicitly** (not via `Omit`)
338
+ - [ ] Optional fields match `required: false` in the manifest
339
+ - [ ] Every AI action from `.howone/ai/manifest.json` has an `inputSchema` zod object
340
+ - [ ] Required input fields are not `.optional()` in zod
341
+ - [ ] AI action names match the manifest `id` exactly (case-sensitive)
342
+ - [ ] `createClient` uses `import.meta.env.*` only
343
+ - [ ] `withEntities` is applied before `withAiActions` in the composition chain
344
+ - [ ] No generated source files are placed under `.howone/`
345
+ - [ ] Exported types and schemas are importable from `@/lib/sdk`
346
+
347
+ ---
348
+
349
+ ## Incremental Update Pattern
350
+
351
+ When new entities or actions are added to the manifests, update `src/lib/sdk.ts` by:
352
+
353
+ 1. Reading the current `src/lib/sdk.ts` to preserve existing bindings and import style.
354
+ 2. Appending new types, schemas, entity bindings, and action bindings.
355
+ 3. Not removing existing bindings unless the manifest explicitly removed them.
356
+ 4. Preserving export names for backward compatibility with existing UI code.
357
+
358
+ ```ts
359
+ // Before (existing)
360
+ export const entities = defineEntities({
361
+ Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
362
+ })
363
+
364
+ // After (added Comment entity from new manifest)
365
+ export const entities = defineEntities({
366
+ Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
367
+ Comment: client.entity<CommentRecord, CommentCreate, CommentUpdate>('Comment'),
368
+ })
369
+ ```
370
+
371
+ ---
372
+
373
+ ## AI-First Persistence Pattern
374
+
375
+ When the app generates data with AI and saves it to an entity:
376
+
377
+ 1. Read `.howone/ai/manifest.json` → determine AI output shape.
378
+ 2. Define entity fields that mirror the AI output fields.
379
+ 3. Add any app-specific metadata fields (status, userId, createdAt, etc.).
380
+ 4. Generate the entity type, then the AI action binding.
381
+
382
+ ```ts
383
+ // AI generates: { title: string, content: string, summary: string }
384
+ // Save it to Story entity which adds: authorId, status, wordCount
385
+
386
+ async function generateAndSave(input: GenerateStoryInput, authorId: string) {
387
+ const result = await howone.ai.generateStory.run(input)
388
+ if (!result.success) throw new Error(result.errors.join(', '))
389
+
390
+ const output = result.finalResult as GenerateStoryOutput
391
+
392
+ return howone.entities.Story.create({
393
+ title: output.title,
394
+ content: output.content,
395
+ authorId,
396
+ status: 'draft',
397
+ wordCount: output.content.split(' ').length,
398
+ })
399
+ }
400
+ ```