77 "io"
88 "net/http"
99 "net/url"
10+ "strings"
1011 "time"
1112
1213 "github.com/ashrafali/craft-cli/internal/models"
@@ -108,25 +109,62 @@ func (c *Client) GetDocuments() (*models.DocumentList, error) {
108109 return & result , nil
109110}
110111
111- // GetDocument retrieves a single document by ID
112+ // GetDocument retrieves a single document by ID using the blocks endpoint
112113func (c * Client ) GetDocument (id string ) (* models.Document , error ) {
113- path := fmt .Sprintf ("/documents/ %s" , id )
114+ path := fmt .Sprintf ("/blocks?id= %s" , url . QueryEscape ( id ) )
114115 data , err := c .doRequest ("GET" , path , nil )
115116 if err != nil {
116117 return nil , err
117118 }
118119
119- var doc models.Document
120- if err := json .Unmarshal (data , & doc ); err != nil {
120+ var blocksResp models.BlocksResponse
121+ if err := json .Unmarshal (data , & blocksResp ); err != nil {
121122 return nil , fmt .Errorf ("invalid response from API: %w" , err )
122123 }
123124
124- return & doc , nil
125+ // Combine markdown from all blocks
126+ markdown := combineBlocksMarkdown (blocksResp )
127+
128+ doc := & models.Document {
129+ ID : blocksResp .ID ,
130+ Title : blocksResp .Markdown ,
131+ Markdown : markdown ,
132+ }
133+
134+ return doc , nil
135+ }
136+
137+ // combineBlocksMarkdown extracts and combines markdown from all blocks
138+ func combineBlocksMarkdown (resp models.BlocksResponse ) string {
139+ var parts []string
140+
141+ // Add the document title/header
142+ if resp .Markdown != "" {
143+ parts = append (parts , "# " + resp .Markdown )
144+ }
145+
146+ // Recursively collect markdown from all content blocks
147+ for _ , block := range resp .Content {
148+ collectBlockMarkdown (& block , & parts )
149+ }
150+
151+ return strings .Join (parts , "\n \n " )
152+ }
153+
154+ // collectBlockMarkdown recursively collects markdown from a block and its children
155+ func collectBlockMarkdown (block * models.Block , parts * []string ) {
156+ if block .Markdown != "" {
157+ * parts = append (* parts , block .Markdown )
158+ }
159+ for _ , child := range block .Content {
160+ collectBlockMarkdown (& child , parts )
161+ }
125162}
126163
127164// SearchDocuments searches for documents matching a query
128165func (c * Client ) SearchDocuments (query string ) (* models.SearchResult , error ) {
129- path := fmt .Sprintf ("/documents/search?query=%s" , url .QueryEscape (query ))
166+ // Craft API uses 'include' parameter instead of 'query'
167+ path := fmt .Sprintf ("/documents/search?include=%s" , url .QueryEscape (query ))
130168 data , err := c .doRequest ("GET" , path , nil )
131169 if err != nil {
132170 return nil , err
@@ -140,40 +178,195 @@ func (c *Client) SearchDocuments(query string) (*models.SearchResult, error) {
140178 return & result , nil
141179}
142180
181+ // createDocumentsRequest wraps documents for the API
182+ type createDocumentsRequest struct {
183+ Documents []models.CreateDocumentRequest `json:"documents"`
184+ }
185+
186+ // createDocumentsResponse represents the API response for document creation
187+ type createDocumentsResponse struct {
188+ Items []struct {
189+ ID string `json:"id"`
190+ Title string `json:"title"`
191+ } `json:"items"`
192+ }
193+
143194// CreateDocument creates a new document
144195func (c * Client ) CreateDocument (req * models.CreateDocumentRequest ) (* models.Document , error ) {
145- data , err := c .doRequest ("POST" , "/documents" , req )
196+ // Craft API expects {"documents": [...]} wrapper
197+ wrapper := createDocumentsRequest {
198+ Documents : []models.CreateDocumentRequest {* req },
199+ }
200+
201+ data , err := c .doRequest ("POST" , "/documents" , wrapper )
146202 if err != nil {
147203 return nil , err
148204 }
149205
150- var doc models. Document
151- if err := json .Unmarshal (data , & doc ); err != nil {
206+ var resp createDocumentsResponse
207+ if err := json .Unmarshal (data , & resp ); err != nil {
152208 return nil , fmt .Errorf ("invalid response from API: %w" , err )
153209 }
154210
155- return & doc , nil
211+ if len (resp .Items ) == 0 {
212+ return nil , fmt .Errorf ("no document returned from API" )
213+ }
214+
215+ doc := & models.Document {
216+ ID : resp .Items [0 ].ID ,
217+ Title : resp .Items [0 ].Title ,
218+ }
219+
220+ return doc , nil
156221}
157222
158- // UpdateDocument updates an existing document
223+ // blockPosition specifies where to insert a block
224+ type blockPosition struct {
225+ PageID string `json:"pageId"`
226+ Position string `json:"position"` // "start", "end", or block ID
227+ }
228+
229+ // addBlockRequest is the request body for adding blocks
230+ type addBlockRequest struct {
231+ Markdown string `json:"markdown"`
232+ Position blockPosition `json:"position"`
233+ }
234+
235+ // addBlockResponse is the response from adding blocks
236+ type addBlockResponse struct {
237+ Items []struct {
238+ ID string `json:"id"`
239+ Type string `json:"type"`
240+ Markdown string `json:"markdown"`
241+ } `json:"items"`
242+ }
243+
244+ // UpdateDocument updates an existing document by adding content
245+ // Note: The Craft Connect API only supports adding content blocks, not updating title or replacing content
159246func (c * Client ) UpdateDocument (id string , req * models.UpdateDocumentRequest ) (* models.Document , error ) {
160- path := fmt .Sprintf ("/documents/%s" , id )
161- data , err := c .doRequest ("PUT" , path , req )
247+ // Title updates are not supported via the API
248+ if req .Title != "" && req .Markdown == "" {
249+ return nil , fmt .Errorf ("the Craft Connect API does not support title updates. Use the Craft app to rename documents" )
250+ }
251+
252+ if req .Markdown == "" {
253+ return nil , fmt .Errorf ("markdown content is required for updates" )
254+ }
255+
256+ // Add content blocks to the document
257+ addReq := addBlockRequest {
258+ Markdown : req .Markdown ,
259+ Position : blockPosition {
260+ PageID : id ,
261+ Position : "end" ,
262+ },
263+ }
264+
265+ data , err := c .doRequest ("POST" , "/blocks" , addReq )
162266 if err != nil {
163267 return nil , err
164268 }
165269
166- var doc models. Document
167- if err := json .Unmarshal (data , & doc ); err != nil {
270+ var resp addBlockResponse
271+ if err := json .Unmarshal (data , & resp ); err != nil {
168272 return nil , fmt .Errorf ("invalid response from API: %w" , err )
169273 }
170274
171- return & doc , nil
275+ // Return a document with the update info
276+ doc := & models.Document {
277+ ID : id ,
278+ }
279+
280+ // If title was also requested, note it in return
281+ if req .Title != "" {
282+ doc .Title = req .Title + " (title not updated - API limitation)"
283+ }
284+
285+ // Set markdown to confirm what was added
286+ if len (resp .Items ) > 0 {
287+ doc .Markdown = resp .Items [0 ].Markdown
288+ }
289+
290+ return doc , nil
291+ }
292+
293+ // deleteBlocksRequest is the request body for deleting blocks
294+ type deleteBlocksRequest struct {
295+ BlockIDs []string `json:"blockIds"`
296+ }
297+
298+ // deleteBlocksResponse is the response from deleting blocks
299+ type deleteBlocksResponse struct {
300+ Items []struct {
301+ ID string `json:"id"`
302+ } `json:"items"`
172303}
173304
174305// DeleteDocument deletes a document by ID
306+ // Note: The Craft Connect API does not support deleting root page blocks (documents)
307+ // This will attempt to delete all content blocks within the document
175308func (c * Client ) DeleteDocument (id string ) error {
176- path := fmt .Sprintf ("/documents/%s" , id )
177- _ , err := c .doRequest ("DELETE" , path , nil )
178- return err
309+ // First, get the document to find all its content blocks
310+ doc , err := c .GetDocument (id )
311+ if err != nil {
312+ return fmt .Errorf ("failed to get document: %w" , err )
313+ }
314+
315+ // Get the blocks again to get block IDs
316+ path := fmt .Sprintf ("/blocks?id=%s" , url .QueryEscape (id ))
317+ data , err := c .doRequest ("GET" , path , nil )
318+ if err != nil {
319+ return fmt .Errorf ("failed to get document blocks: %w" , err )
320+ }
321+
322+ var blocksResp models.BlocksResponse
323+ if err := json .Unmarshal (data , & blocksResp ); err != nil {
324+ return fmt .Errorf ("invalid response from API: %w" , err )
325+ }
326+
327+ // Collect all content block IDs (not the root page)
328+ var blockIDs []string
329+ for _ , block := range blocksResp .Content {
330+ collectBlockIDs (& block , & blockIDs )
331+ }
332+
333+ if len (blockIDs ) == 0 {
334+ return fmt .Errorf ("the Craft Connect API cannot delete documents (only content blocks). Document '%s' has no deletable content blocks" , doc .Title )
335+ }
336+
337+ // Delete the content blocks
338+ deleteReq := deleteBlocksRequest {
339+ BlockIDs : blockIDs ,
340+ }
341+
342+ _ , err = c .doRequest ("DELETE" , "/blocks" , deleteReq )
343+ if err != nil {
344+ return fmt .Errorf ("failed to delete blocks: %w" , err )
345+ }
346+
347+ return nil
348+ }
349+
350+ // collectBlockIDs recursively collects all block IDs
351+ func collectBlockIDs (block * models.Block , ids * []string ) {
352+ if block .ID != "" {
353+ * ids = append (* ids , block .ID )
354+ }
355+ for _ , child := range block .Content {
356+ collectBlockIDs (& child , ids )
357+ }
358+ }
359+
360+ // DeleteBlock deletes a specific block by ID
361+ func (c * Client ) DeleteBlock (blockID string ) error {
362+ deleteReq := deleteBlocksRequest {
363+ BlockIDs : []string {blockID },
364+ }
365+
366+ _ , err := c .doRequest ("DELETE" , "/blocks" , deleteReq )
367+ if err != nil {
368+ return fmt .Errorf ("failed to delete block: %w" , err )
369+ }
370+
371+ return nil
179372}
0 commit comments