• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scriptype / writ-cms / 28860160269

07 Jul 2026 10:41AM UTC coverage: 52.382% (-1.0%) from 53.397%
28860160269

push

github

scriptype
Implement upload/delete attachment in all models

... except for home. it currently lacks handling what to do if
attachments or deletedAttachments are received.

todo: switch to foldered homepage, entry and page depending on
attachments. home needing special love and care

650 of 1256 branches covered (51.75%)

Branch coverage included in aggregate %.

0 of 173 new or added lines in 11 files covered. (0.0%)

21 existing lines in 9 files now uncovered.

2242 of 4265 relevant lines covered (52.57%)

1531.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/cms/api/models/post.js
1
const { writeFile, mkdir, rename, rm } = require('fs/promises')
×
2
const { join, dirname, basename, relative } = require('path')
×
3
const matter = require('gray-matter')
×
4
const _ = require('lodash')
×
5
const { ['default']: filenamify } = require('filenamify')
×
6
const { unusedFilename } = require('unused-filename')
×
7
const { contentRootPath } = require('../helpers')
×
8

9
const replaceFilename = (oldAbsolutePath, newAbsolutePath) => {
×
10
  return join(
×
11
    dirname(oldAbsolutePath),
12
    basename(newAbsolutePath)
13
  )
14
}
15

NEW
16
const deleteAttachments = (attachments, parentAbsolutePath) => {
×
NEW
17
  return attachments.map(fileName => {
×
NEW
18
    const filePath = join(parentAbsolutePath, fileName)
×
NEW
19
    return rm(filePath)
×
20
  })
21
}
22

NEW
23
const uploadAttachments = (attachments, parentAbsolutePath) => {
×
NEW
24
  return attachments.map(file => {
×
NEW
25
    const dest = join(parentAbsolutePath, file.originalname)
×
NEW
26
    return writeFile(dest, file.buffer)
×
27
  })
28
}
29

30
const createPostModel = ({ getSettings, getContentModel }) => {
×
31
  const createPost = async (data, attachments) => {
×
32
    const opts = {
×
33
      taxonomyPath: data.taxonomyPath || [],
×
34
      title: data.title || 'Untitled',
×
35
      content: data.content || '',
×
36
      excerpt: data.excerpt || '',
×
37
      extension: data.extension || '.md',
×
38
      deletedAttachments: data.deletedAttachments || [],
×
39
      metadata: _(data)
40
        .omit(['taxonomyPath', 'title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
41
        .pickBy(value => (!!value || value === 0))
×
42
        .value()
43
    }
44

45
    const { rootDirectory, contentDirectory } = getSettings()
×
46
    const root = await contentRootPath(rootDirectory, contentDirectory)
×
47

48
    const sanitizedTitle = filenamify(opts.title)
×
49

50
    const path = join(...[root].concat(opts.taxonomyPath).concat(sanitizedTitle))
×
51
    const unusedPath = await unusedFilename(path)
×
52

53
    const shouldOverrideTitle = (sanitizedTitle !== opts.title) || (unusedPath !== path)
×
54
    const metadataWithTitle = shouldOverrideTitle ?
×
55
      {
56
        ...opts.metadata,
57
        title: `${opts.title}`
58
      } : opts.metadata
59

60
    const fileContent = matter.stringify({
×
61
      data: metadataWithTitle,
62
      content: opts.content,
63
      excerpt: opts.excerpt
64
    })
65
    try {
×
66
      await mkdir(unusedPath, { recursive: true })
×
67
    } catch {}
68
    return Promise.all([
×
69
      writeFile(`${join(unusedPath, 'post')}${opts.extension}`, fileContent),
70
      ...uploadAttachments(attachments, unusedPath)
71
    ])
72
  }
73

74
  const updatePost = async (path, data, attachments = []) => {
×
75
    if (!path) {
×
76
      throw new Error('path is required')
×
77
    }
78

79
    const opts = {
×
80
      title: data.title || 'Untitled',
×
81
      content: data.content || '',
×
82
      excerpt: data.excerpt || '',
×
83
      extension: data.extension || '',
×
84
      deletedAttachments: data.deletedAttachments || [],
×
85
      metadata: _(data)
86
        .omit(['title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
87
        .pickBy(value => (!!value || value === 0))
×
88
        .value()
89
    }
90

91
    const collectionName = path.split('/')[0]
×
92
    const collection = getContentModel().subtree.collections.find(c => c.name === collectionName)
×
93
    const post = collection.subtree.posts.find(p => p.path === path)
×
94

95
    const isFoldered = !post.extension
×
96
    const isTitleDifferent = opts.title !== post.title
×
97
    let absolutePath = post.absolutePath
×
98
    let isPathDifferentThanTitle
99

100
    if (isTitleDifferent) {
×
101
      const sanitizedTitle = filenamify(opts.title)
×
102
      const sanitizedPath = replaceFilename(post.absolutePath, sanitizedTitle)
×
103
      const unusedPath = await unusedFilename(sanitizedPath)
×
104

105
      if (isFoldered) {
×
106
        await rename(post.absolutePath, unusedPath)
×
107
      } else {
108
        await rm(post.absolutePath)
×
109
      }
110

111
      isPathDifferentThanTitle = (sanitizedTitle !== opts.title) || (unusedPath !== sanitizedPath)
×
112
      absolutePath = isFoldered ? unusedPath : `${unusedPath}${opts.extension || post.extension}`
×
113
    }
114

115
    const metadataWithTitle = isPathDifferentThanTitle ? {
×
116
      ...opts.metadata,
117
      title: `${opts.title}`
118
    } : opts.metadata
119

120
    const fileContent = matter.stringify({
×
121
      data: metadataWithTitle,
122
      content: opts.content,
123
      excerpt: opts.excerpt
124
    })
125

126
    const targetPath = isFoldered ?
×
127
      join(absolutePath, post.indexFile.name) :
128
      absolutePath
129

130
    await Promise.all([
×
131
      writeFile(targetPath, fileContent),
132
      (async () => {
NEW
133
        await Promise.all(deleteAttachments(opts.deletedAttachments, absolutePath))
×
NEW
134
        await Promise.all(uploadAttachments(attachments, absolutePath))
×
135
      })()
136
    ])
137

138
    const { rootDirectory, contentDirectory } = getSettings()
×
139
    const root = await contentRootPath(rootDirectory, contentDirectory)
×
140

141
    return {
×
142
      path: relative(root, absolutePath)
143
    }
144
  }
145

146
  return {
×
147
    create: createPost,
148
    update: updatePost
149
  }
150
}
151

152
module.exports = createPostModel
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc