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

scriptype / writ-cms / 29020981160

09 Jul 2026 01:19PM UTC coverage: 51.606% (-0.5%) from 52.155%
29020981160

push

github

scriptype
Get rid of posix-specific slashes

650 of 1284 branches covered (50.62%)

Branch coverage included in aggregate %.

0 of 12 new or added lines in 5 files covered. (0.0%)

83 existing lines in 2 files now uncovered.

2242 of 4320 relevant lines covered (51.9%)

1512.21 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')
×
NEW
2
const { join, dirname, basename, relative, sep } = 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

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

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

30
const findPost = (contentModel, path) => {
×
NEW
31
    const collectionName = path.split(sep)[0]
×
32
    const collection = contentModel.subtree.collections.find(c => c.name === collectionName)
×
33
    return collection.subtree.posts.find(p => p.path === path)
×
34
}
35

36
const getRelativePath = async (settings, absolutePath) => {
×
37
  const { rootDirectory, contentDirectory } = settings
×
38
  const root = await contentRootPath(rootDirectory, contentDirectory)
×
39

40
  return relative(root, absolutePath)
×
41
}
42

43
const createPostModel = ({ getSettings, getContentModel }) => {
×
44
  const createPost = async (data, attachments) => {
×
45
    const opts = {
×
46
      taxonomyPath: data.taxonomyPath || [],
×
47
      title: data.title || 'Untitled',
×
48
      content: data.content || '',
×
49
      excerpt: data.excerpt || '',
×
50
      extension: data.extension || '.md',
×
51
      deletedAttachments: data.deletedAttachments || [],
×
52
      metadata: _(data)
53
        .omit(['taxonomyPath', 'title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
54
        .pickBy(value => (!!value || value === 0))
×
55
        .value()
56
    }
57

58
    const { rootDirectory, contentDirectory } = getSettings()
×
59
    const root = await contentRootPath(rootDirectory, contentDirectory)
×
60

61
    const shouldFolder = !!attachments.length
×
62
    const sanitizedTitle = filenamify(opts.title)
×
63

64
    const path = join(...[
×
65
      root,
66
      ...opts.taxonomyPath,
67
      shouldFolder ? sanitizedTitle : `${sanitizedTitle}${opts.extension}`
×
68
    ])
69
    const unusedPath = await unusedFilename(path)
×
70

71
    const shouldOverrideTitle = (sanitizedTitle !== opts.title) || (unusedPath !== path)
×
72
    const metadataWithTitle = shouldOverrideTitle ?
×
73
      {
74
        ...opts.metadata,
75
        title: `${opts.title}`
76
      } : opts.metadata
77

78
    const fileContent = matter.stringify({
×
79
      data: metadataWithTitle,
80
      content: opts.content,
81
      excerpt: opts.excerpt
82
    })
83

84
    if (attachments.length) {
×
85
      try {
×
86
        await mkdir(unusedPath, { recursive: true })
×
87
      } catch {}
88
      await Promise.all([
×
89
        writeFile(join(unusedPath, `post${opts.extension}`), fileContent),
90
        ...uploadAttachments(attachments, unusedPath)
91
      ])
92
    } else {
93
      await writeFile(unusedPath, fileContent)
×
94
    }
95

96
    return {
×
97
      path: await getRelativePath(getSettings(), unusedPath)
98
    }
99
  }
100

101
  const updatePost = async (path, data, attachments = []) => {
×
102
    if (!path) {
×
103
      throw new Error('path is required')
×
104
    }
105

106
    const opts = {
×
107
      title: data.title || 'Untitled',
×
108
      content: data.content || '',
×
109
      excerpt: data.excerpt || '',
×
110
      extension: data.extension || '.md',
×
111
      deletedAttachments: data.deletedAttachments || [],
×
112
      metadata: _(data)
113
        .omit(['title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
114
        .pickBy(value => (!!value || value === 0))
×
115
        .value()
116
    }
117

118
    const post = findPost(getContentModel(), path)
×
119

120
    const isFoldered = !post.extension
×
121
    const shouldFolder = !!attachments.length || (post.subtree.attachments.length > opts.deletedAttachments.length)
×
122

123
    // When foldering changes, just re-create post and delete the old one
124
    if (isFoldered !== shouldFolder) {
×
125
      const result = await createPost({
×
126
        ...data,
127
        taxonomyPath: path.split(sep).slice(0, -1),
128
      }, attachments)
129
      await rm(post.absolutePath, { recursive: true, force: true })
×
130
      return result
×
131
    }
132

133
    const isTitleDifferent = opts.title !== post.title
×
134
    let absolutePath = post.absolutePath
×
135
    let isPathDifferentThanTitle
136

137
    if (isTitleDifferent) {
×
138
      const sanitizedTitle = filenamify(opts.title)
×
139
      const sanitizedPath = replaceFilename(post.absolutePath, sanitizedTitle)
×
140
      const unusedPath = await unusedFilename(sanitizedPath)
×
141

142
      if (isFoldered) {
×
143
        await rename(post.absolutePath, unusedPath)
×
144
      } else {
145
        await rm(post.absolutePath)
×
146
      }
147

148
      isPathDifferentThanTitle = (sanitizedTitle !== opts.title) || (unusedPath !== sanitizedPath)
×
149
      absolutePath = isFoldered ? unusedPath : `${unusedPath}${opts.extension || post.extension}`
×
150
    }
151

152
    const metadataWithTitle = isPathDifferentThanTitle ? {
×
153
      ...opts.metadata,
154
      title: `${opts.title}`
155
    } : opts.metadata
156

157
    const fileContent = matter.stringify({
×
158
      data: metadataWithTitle,
159
      content: opts.content,
160
      excerpt: opts.excerpt
161
    })
162

163
    const targetPath = isFoldered ?
×
164
      join(absolutePath, post.indexFile.name) :
165
      absolutePath
166

167
    await Promise.all([
×
168
      writeFile(targetPath, fileContent),
169
      (async () => {
170
        await Promise.all(deleteAttachments(opts.deletedAttachments, absolutePath))
×
171
        await Promise.all(uploadAttachments(attachments, absolutePath))
×
172
      })()
173
    ])
174

175
    return {
×
176
      path: await getRelativePath(getSettings(), absolutePath)
177
    }
178
  }
179

180
  return {
×
181
    create: createPost,
182
    update: updatePost
183
  }
184
}
185

186
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