@natilon/cms-server 0.10.0 → 0.11.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natilon/cms-server",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Express-based CMS server with pluggable adapters for content, media, auth, and build.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "natilon-cms": "./bin/natilon-cms.mjs"
21
21
  },
22
22
  "scripts": {
23
- "test": "node --test test/"
23
+ "test": "node --test 'test/**/*.test.mjs'"
24
24
  },
25
25
  "exports": {
26
26
  ".": "./src/index.mjs",
@@ -35,7 +35,7 @@
35
35
  "bin"
36
36
  ],
37
37
  "dependencies": {
38
- "@natilon/admin-ui": ">=0.5.0",
38
+ "@natilon/admin-ui": ">=0.10.0",
39
39
  "express": "^4.21.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -25,6 +25,18 @@ export function safeFileName(name) {
25
25
  return name;
26
26
  }
27
27
 
28
+ /**
29
+ * True when a filename is a content entry (and not a generated manifest such
30
+ * as `_index.json`). Manifest/index files are list projections, not entries,
31
+ * so every directory-scan that enumerates entries must exclude them.
32
+ *
33
+ * @param {string} file
34
+ * @returns {boolean}
35
+ */
36
+ export function isEntryFile(file) {
37
+ return typeof file === "string" && file.endsWith(".json") && !file.startsWith("_");
38
+ }
39
+
28
40
  /**
29
41
  * Sorts a pages array in-place and returns it.
30
42
  *
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { execSync } from "child_process";
4
- import { sanitize, safeFileName, sortPages, buildDuplicateData, listScheduledDue, buildListEntry, relationFields } from "./_shared.mjs";
4
+ import { sanitize, safeFileName, sortPages, buildDuplicateData, listScheduledDue, buildListEntry, relationFields, isEntryFile } from "./_shared.mjs";
5
5
 
6
6
  const HISTORY_KEEP = 50; // max revisions kept per file
7
7
 
@@ -65,7 +65,7 @@ export function createFsJsonContent({
65
65
  return dirs.map((dir) => {
66
66
  const files = fs
67
67
  .readdirSync(path.join(PAGES_DIR, dir))
68
- .filter((f) => f.endsWith(".json"));
68
+ .filter(isEntryFile);
69
69
  return { name: dir, count: files.length };
70
70
  });
71
71
  },
@@ -84,7 +84,7 @@ export function createFsJsonContent({
84
84
  if (!fs.existsSync(targetDir)) continue;
85
85
  const table = {};
86
86
  for (const f of fs.readdirSync(targetDir)) {
87
- if (!f.endsWith(".json") || f === "_index.json") continue;
87
+ if (!isEntryFile(f)) continue;
88
88
  try {
89
89
  const d = JSON.parse(fs.readFileSync(path.join(targetDir, f), "utf-8"));
90
90
  if (d.slug) table[d.slug] = d.meta?.title || d.meta?.name || d.slug;
@@ -95,7 +95,7 @@ export function createFsJsonContent({
95
95
  lookups[target] = table;
96
96
  }
97
97
 
98
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
98
+ const files = fs.readdirSync(dir).filter(isEntryFile);
99
99
  const pages = files.map((file) => {
100
100
  const data = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
101
101
  return buildListEntry(collections[collection], collection, file, data, lookups);
@@ -122,6 +122,13 @@ export function createFsJsonContent({
122
122
  const slug = data.slug || data.id || `new-${Date.now()}`;
123
123
  const fileName = `${sanitize(data.lang || "en")}-${sanitize(slug)}.json`;
124
124
  const filePath = pagePath(collection, fileName);
125
+ if (fs.existsSync(filePath)) {
126
+ const err = new Error(
127
+ `An entry already exists for "${data.lang || "en"}/${slug}". Change the slug or language.`,
128
+ );
129
+ err.status = 409;
130
+ throw err;
131
+ }
125
132
  const dir = path.dirname(filePath);
126
133
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
127
134
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
@@ -254,6 +254,13 @@ export function createGitHubContent({ token, owner, repo, branch, pagesDir, comm
254
254
  const slug = data.slug || data.id || `new-${Date.now()}`;
255
255
  const fileName = `${sanitize(data.lang || "en")}-${sanitize(slug)}.json`;
256
256
  const path = contentPath(collection, fileName);
257
+ if (await getFileSha(path)) {
258
+ const err = new Error(
259
+ `An entry already exists for "${data.lang || "en"}/${slug}". Change the slug or language.`,
260
+ );
261
+ err.status = 409;
262
+ throw err;
263
+ }
257
264
  const result = await apiPut(`/contents/${path}`, {
258
265
  message: commitMsg(commitMessage),
259
266
  content: encodeContent(data),
package/src/routes.mjs CHANGED
@@ -150,8 +150,13 @@ export const apiRoutes = [
150
150
  path: "/api/collections/:collection",
151
151
  auth: "any",
152
152
  handler: async ({ adapters, params, body }) => {
153
- const result = await adapters.content.createPage(params.collection, body);
154
- return ok({ ok: true, file: result.file });
153
+ try {
154
+ const result = await adapters.content.createPage(params.collection, body);
155
+ return ok({ ok: true, file: result.file });
156
+ } catch (err) {
157
+ if (err.status === 409) return { status: 409, json: { error: err.message } };
158
+ throw err;
159
+ }
155
160
  },
156
161
  },
157
162