Skip to content

Commit 3ca1209

Browse files
committed
fix(cli): normalize API envelopes on edit and update
1 parent 6234407 commit 3ca1209

14 files changed

Lines changed: 229 additions & 64 deletions

File tree

packages/cli/src/cli/draft/_shared.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ export const unwrapData = <T>(res: unknown): T => {
9494
return res as T
9595
}
9696

97+
export const normalizeData = <T>(value: unknown): T =>
98+
camelizeDeep(unwrapData(value)) as T
99+
97100
export const normalizeDraftRow = (row: unknown): DraftRow | null => {
98-
const normalized = camelizeDeep(unwrapData(row))
101+
const normalized = normalizeData(row)
99102
if (!normalized || typeof normalized !== 'object') return null
100103
const branch = normalized as Partial<DraftRow>
101104
return branch.id && branch.document && branch.headRevision
@@ -104,7 +107,7 @@ export const normalizeDraftRow = (row: unknown): DraftRow | null => {
104107
}
105108

106109
export const normalizeVersionContext = (value: unknown): VersionContext =>
107-
camelizeDeep(unwrapData(value)) as VersionContext
110+
normalizeData(value)
108111

109112
const mergeRevisionPatch = (
110113
base: RevisionSnapshot,

packages/cli/src/cli/note/edit.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { Editor } from '../../services/Editor'
1010
import { Lexical } from '../../services/Lexical'
1111
import { Renderer } from '../../services/Renderer'
1212
import { Resolver } from '../../services/Resolver'
13-
import { publishSavedDraft, saveDraftPayload } from '../draft/_shared'
13+
import {
14+
normalizeData,
15+
publishSavedDraft,
16+
saveDraftPayload,
17+
} from '../draft/_shared'
1418
import { noteWriteOptions, resolveTopicRefs, toNoteFlagInputs } from './_flags'
1519

1620
const slugOrId = Args.text({ name: 'slugOrId' })
@@ -35,9 +39,9 @@ const materializeForEditor = (slugOrId: string) =>
3539
const resolver = yield* Resolver
3640
const lexical = yield* Lexical
3741
const id = yield* resolver.resolveNoteId(slugOrId)
38-
const note = (yield* api.request(`/notes/${id}`, {
39-
query: { prefer: 'lexical' },
40-
})) as NoteForEditor
42+
const note = normalizeData<NoteForEditor>(
43+
yield* api.request(`/notes/${id}`, { query: { prefer: 'lexical' } }),
44+
)
4145
let inner: string
4246
if (note.contentFormat === 'lexical' && note.content) {
4347
const state = JSON.parse(note.content)
@@ -90,7 +94,9 @@ export const edit = Command.make(
9094
const renderer = yield* Renderer
9195
const resolver = yield* Resolver
9296
const id = yield* resolver.resolveNoteId(slugOrId)
93-
const current = (yield* api.request(`/notes/${id}`)) as NoteForEditor
97+
const current = normalizeData<NoteForEditor>(
98+
yield* api.request(`/notes/${id}`),
99+
)
94100

95101
if (!flags.file && flags.content === undefined) {
96102
const xml = yield* materializeForEditor(slugOrId)

packages/cli/src/cli/note/update.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { buildNotePayload } from '../../domain/payload'
66
import { Api } from '../../services/Api'
77
import { Renderer } from '../../services/Renderer'
88
import { Resolver } from '../../services/Resolver'
9-
import { publishSavedDraft, saveDraftPayload } from '../draft/_shared'
9+
import {
10+
normalizeData,
11+
publishSavedDraft,
12+
saveDraftPayload,
13+
} from '../draft/_shared'
1014
import { noteWriteOptions, resolveTopicRefs, toNoteFlagInputs } from './_flags'
1115

1216
const slugOrId = Args.text({ name: 'slugOrId' })
@@ -32,9 +36,9 @@ export const update = Command.make(
3236
const renderer = yield* Renderer
3337
const current =
3438
flags.state === undefined
35-
? ((yield* api.request(`/notes/${id}`)) as {
36-
isPublished?: boolean
37-
})
39+
? normalizeData<{ isPublished?: boolean }>(
40+
yield* api.request(`/notes/${id}`),
41+
)
3842
: null
3943
const saved = yield* saveDraftPayload(api, 'note', resolved, id)
4044
const response =

packages/cli/src/cli/page/edit.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { Editor } from '../../services/Editor'
1010
import { Lexical } from '../../services/Lexical'
1111
import { Renderer } from '../../services/Renderer'
1212
import { isSnowflakeId } from '../../services/Resolver'
13-
import { publishSavedDraft, saveDraftPayload } from '../draft/_shared'
13+
import {
14+
normalizeData,
15+
publishSavedDraft,
16+
saveDraftPayload,
17+
} from '../draft/_shared'
1418
import { pageWriteOptions, toPageFlagInputs } from './create'
1519

1620
const slugOrId = Args.text({ name: 'slugOrId' })
@@ -62,9 +66,9 @@ const materializeForEditor = (ref: string) =>
6266
const path = isSnowflakeId(ref)
6367
? `/pages/${ref}`
6468
: `/pages/slug/${encodeURIComponent(ref)}`
65-
const page = (yield* api.request(path, {
66-
query: { prefer: 'lexical' },
67-
})) as PageForEditor
69+
const page = normalizeData<PageForEditor>(
70+
yield* api.request(path, { query: { prefer: 'lexical' } }),
71+
)
6872
const isLexical = page.contentFormat === 'lexical'
6973
let innerXml: string
7074
if (isLexical && page.content) {

packages/cli/src/cli/post/edit.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { Editor } from '../../services/Editor'
1010
import { Lexical } from '../../services/Lexical'
1111
import { Renderer } from '../../services/Renderer'
1212
import { Resolver } from '../../services/Resolver'
13-
import { publishSavedDraft, saveDraftPayload } from '../draft/_shared'
13+
import {
14+
normalizeData,
15+
publishSavedDraft,
16+
saveDraftPayload,
17+
} from '../draft/_shared'
1418
import {
1519
postWriteOptions,
1620
resolveCategoryRefs,
@@ -39,9 +43,9 @@ const materializeForEditor = (slugOrId: string) =>
3943
const resolver = yield* Resolver
4044
const lexical = yield* Lexical
4145
const path = yield* resolver.resolvePostReadPath(slugOrId)
42-
const post = (yield* api.request(path, {
43-
query: { prefer: 'lexical' },
44-
})) as PostForEditor
46+
const post = normalizeData<PostForEditor>(
47+
yield* api.request(path, { query: { prefer: 'lexical' } }),
48+
)
4549
const isLexical = post.contentFormat === 'lexical'
4650
let innerXml: string
4751
if (isLexical && post.content) {
@@ -114,7 +118,9 @@ export const edit = Command.make(
114118
const renderer = yield* Renderer
115119
const resolver = yield* Resolver
116120
const id = yield* resolver.resolvePostId(slugOrId)
117-
const current = (yield* api.request(`/posts/${id}`)) as PostForEditor
121+
const current = normalizeData<PostForEditor>(
122+
yield* api.request(`/posts/${id}`),
123+
)
118124

119125
// Editor round-trip path: no --file and no --content → spawn $EDITOR.
120126
if (!flags.file && flags.content === undefined) {

packages/cli/src/cli/post/update.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { buildPostPayload } from '../../domain/payload'
66
import { Api } from '../../services/Api'
77
import { Renderer } from '../../services/Renderer'
88
import { Resolver } from '../../services/Resolver'
9-
import { publishSavedDraft, saveDraftPayload } from '../draft/_shared'
9+
import {
10+
normalizeData,
11+
publishSavedDraft,
12+
saveDraftPayload,
13+
} from '../draft/_shared'
1014
import {
1115
postWriteOptions,
1216
resolveCategoryRefs,
@@ -36,9 +40,9 @@ export const update = Command.make(
3640
const renderer = yield* Renderer
3741
const current =
3842
flags.state === undefined
39-
? ((yield* api.request(`/posts/${id}`)) as {
40-
isPublished?: boolean
41-
})
43+
? normalizeData<{ isPublished?: boolean }>(
44+
yield* api.request(`/posts/${id}`),
45+
)
4246
: null
4347
const saved = yield* saveDraftPayload(api, 'post', resolved, id)
4448
const response =

packages/cli/test/cli/edit-roundtrip.test.ts

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,43 @@ const makeApi = (calls: string[]): ApiService => ({
5757
if (path === '/publish-jobs') return { id: 'task-1' } as never
5858
if (path.startsWith('/notes/')) {
5959
return {
60-
id: '123456789012346',
61-
title: 'Note',
62-
slug: 'note',
63-
contentFormat: 'markdown',
64-
content: 'note body',
65-
isPublished: false,
66-
mood: 'calm',
67-
weather: 'clear',
60+
data: {
61+
id: '123456789012346',
62+
title: 'Note',
63+
slug: 'note',
64+
content_format: 'markdown',
65+
content: 'note body',
66+
is_published: false,
67+
mood: 'calm',
68+
weather: 'clear',
69+
},
70+
meta: {},
6871
} as never
6972
}
7073
if (path.startsWith('/pages/')) {
7174
return {
72-
id: '123456789012347',
73-
title: 'Page',
74-
slug: 'page',
75-
contentFormat: 'markdown',
76-
content: 'page body',
75+
data: {
76+
id: '123456789012347',
77+
title: 'Page',
78+
slug: 'page',
79+
content_format: 'markdown',
80+
content: 'page body',
81+
},
82+
meta: {},
7783
} as never
7884
}
7985
return {
80-
id: '123456789012345',
81-
title: 'Post',
82-
slug: 'post',
83-
contentFormat: 'markdown',
84-
content: 'post body',
85-
summary: 'summary',
86-
isPublished: true,
87-
tags: ['a', 'b'],
86+
data: {
87+
id: '123456789012345',
88+
title: 'Post',
89+
slug: 'post',
90+
content_format: 'markdown',
91+
content: 'post body',
92+
summary: 'summary',
93+
is_published: true,
94+
tags: ['a', 'b'],
95+
},
96+
meta: {},
8897
} as never
8998
}),
9099
raw: (path) =>
@@ -190,7 +199,16 @@ describe('edit command no-change round trip', () => {
190199
const exit = await Effect.runPromiseExit(
191200
editPost
192201
.handler({ slugOrId: 'post', ...commonPostOptions })
193-
.pipe(Effect.provide(buildLayer(calls))),
202+
.pipe(
203+
Effect.provide(
204+
buildLayer(calls, (initial) => {
205+
expect(initial).toContain('<title>Post</title>')
206+
expect(initial).toContain('<state>publish</state>')
207+
expect(initial).toContain('post body')
208+
return initial
209+
}),
210+
),
211+
),
194212
)
195213
expect(Exit.isSuccess(exit)).toBe(true)
196214
expect(calls).toEqual(['/posts/123456789012345', '/posts/post'])
@@ -206,7 +224,16 @@ describe('edit command no-change round trip', () => {
206224
const exit = await Effect.runPromiseExit(
207225
editNote
208226
.handler({ slugOrId: 'note', ...commonNoteOptions })
209-
.pipe(Effect.provide(buildLayer(calls))),
227+
.pipe(
228+
Effect.provide(
229+
buildLayer(calls, (initial) => {
230+
expect(initial).toContain('<title>Note</title>')
231+
expect(initial).toContain('<state>draft</state>')
232+
expect(initial).toContain('note body')
233+
return initial
234+
}),
235+
),
236+
),
210237
)
211238
expect(Exit.isSuccess(exit)).toBe(true)
212239
expect(calls).toEqual([
@@ -225,7 +252,15 @@ describe('edit command no-change round trip', () => {
225252
const exit = await Effect.runPromiseExit(
226253
editPage
227254
.handler({ slugOrId: '123456789012347', ...commonPageOptions })
228-
.pipe(Effect.provide(buildLayer(calls))),
255+
.pipe(
256+
Effect.provide(
257+
buildLayer(calls, (initial) => {
258+
expect(initial).toContain('<title>Page</title>')
259+
expect(initial).toContain('page body')
260+
return initial
261+
}),
262+
),
263+
),
229264
)
230265
expect(Exit.isSuccess(exit)).toBe(true)
231266
expect(calls).toEqual(['/pages/123456789012347'])

packages/cli/test/cli/post-note-crud.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,26 @@ const makeApi = (calls: Array<{ path: string; options: unknown }>): ApiService =
9494
status: 'active',
9595
} as never
9696
}
97+
if (path === '/posts/123456789012345') {
98+
return {
99+
data: {
100+
id: '123456789012345',
101+
is_published: true,
102+
title: 'Title',
103+
},
104+
meta: {},
105+
} as never
106+
}
107+
if (path === '/notes/123456789012346') {
108+
return {
109+
data: {
110+
id: '123456789012346',
111+
is_published: true,
112+
title: 'Title',
113+
},
114+
meta: {},
115+
} as never
116+
}
97117
return { id: 'resource-id', ok: true, title: 'Title' } as never
98118
}),
99119
raw: (path, options) =>
@@ -180,7 +200,7 @@ describe('post command CRUD handlers', () => {
180200
}
181201
})
182202

183-
it('patches a post update payload', async () => {
203+
it('publishes a post update payload', async () => {
184204
const calls: Array<{ path: string; options: unknown }> = []
185205
const exit = await Effect.runPromiseExit(
186206
updatePost
@@ -213,6 +233,7 @@ describe('post command CRUD handlers', () => {
213233
'/posts/123456789012345',
214234
'/drafts/context/post/123456789012345',
215235
'/drafts',
236+
'/publish-jobs',
216237
])
217238
expect(calls[2]).toMatchObject({
218239
options: { method: 'POST' },
@@ -315,7 +336,7 @@ describe('note command CRUD handlers', () => {
315336
}
316337
})
317338

318-
it('patches a note update payload', async () => {
339+
it('publishes a note update payload', async () => {
319340
const calls: Array<{ path: string; options: unknown }> = []
320341
const exit = await Effect.runPromiseExit(
321342
updateNote
@@ -350,6 +371,7 @@ describe('note command CRUD handlers', () => {
350371
'/notes/123456789012346',
351372
'/drafts/context/note/123456789012346',
352373
'/drafts',
374+
'/publish-jobs',
353375
])
354376
expect(calls[2]).toMatchObject({
355377
options: { method: 'POST' },

packages/cli/test/cli/post/update.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ describe('post update command', () => {
138138
const http = testHttpLayer({
139139
[`GET https://blog.example.com/api/v2/posts/${SNOWFLAKE}`]: {
140140
status: 200,
141-
body: { id: SNOWFLAKE, isPublished: true },
141+
body: {
142+
data: { id: SNOWFLAKE, is_published: true },
143+
meta: {},
144+
},
142145
},
143146
[`GET https://blog.example.com/api/v2/drafts/context/post/${SNOWFLAKE}`]:
144147
{

0 commit comments

Comments
 (0)