@micacms/runtime 0.6.0-rc.2 → 0.7.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.
Files changed (70) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +5 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/context.d.ts +17 -2
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +62 -18
  7. package/dist/context.js.map +1 -1
  8. package/dist/http.d.ts +2 -0
  9. package/dist/http.d.ts.map +1 -1
  10. package/dist/http.js +5 -0
  11. package/dist/http.js.map +1 -1
  12. package/dist/lib/file-type.d.ts +2 -0
  13. package/dist/lib/file-type.d.ts.map +1 -1
  14. package/dist/lib/file-type.js +3 -1
  15. package/dist/lib/file-type.js.map +1 -1
  16. package/dist/routes/admin-content.d.ts +1 -1
  17. package/dist/routes/admin-content.d.ts.map +1 -1
  18. package/dist/routes/admin-content.js +15 -0
  19. package/dist/routes/admin-content.js.map +1 -1
  20. package/dist/routes/admin-media.d.ts +5 -3
  21. package/dist/routes/admin-media.d.ts.map +1 -1
  22. package/dist/routes/admin-media.js +72 -2
  23. package/dist/routes/admin-media.js.map +1 -1
  24. package/dist/routes/admin-staging.d.ts.map +1 -1
  25. package/dist/routes/admin-staging.js +18 -7
  26. package/dist/routes/admin-staging.js.map +1 -1
  27. package/dist/routes/admin-system.d.ts.map +1 -1
  28. package/dist/routes/admin-system.js +4 -0
  29. package/dist/routes/admin-system.js.map +1 -1
  30. package/dist/services/changes.d.ts +19 -4
  31. package/dist/services/changes.d.ts.map +1 -1
  32. package/dist/services/changes.js +60 -1
  33. package/dist/services/changes.js.map +1 -1
  34. package/dist/services/content-store.d.ts +27 -6
  35. package/dist/services/content-store.d.ts.map +1 -1
  36. package/dist/services/content-store.js +231 -18
  37. package/dist/services/content-store.js.map +1 -1
  38. package/dist/services/media.d.ts.map +1 -1
  39. package/dist/services/media.js +4 -1
  40. package/dist/services/media.js.map +1 -1
  41. package/dist/services/staging.d.ts.map +1 -1
  42. package/dist/services/staging.js +26 -14
  43. package/dist/services/staging.js.map +1 -1
  44. package/dist/workspace/developer.d.ts +2 -1
  45. package/dist/workspace/developer.d.ts.map +1 -1
  46. package/dist/workspace/developer.js +19 -4
  47. package/dist/workspace/developer.js.map +1 -1
  48. package/dist/workspace/policy.d.ts +1 -1
  49. package/dist/workspace/policy.d.ts.map +1 -1
  50. package/dist/workspace/policy.js.map +1 -1
  51. package/dist/workspace/tree.d.ts +15 -2
  52. package/dist/workspace/tree.d.ts.map +1 -1
  53. package/dist/workspace/tree.js +29 -6
  54. package/dist/workspace/tree.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/app.ts +5 -0
  57. package/src/context.ts +69 -20
  58. package/src/http.ts +6 -0
  59. package/src/lib/file-type.ts +4 -1
  60. package/src/routes/admin-content.ts +16 -2
  61. package/src/routes/admin-media.ts +74 -3
  62. package/src/routes/admin-staging.ts +22 -7
  63. package/src/routes/admin-system.ts +4 -0
  64. package/src/services/changes.ts +84 -5
  65. package/src/services/content-store.ts +243 -25
  66. package/src/services/media.ts +7 -1
  67. package/src/services/staging.ts +30 -14
  68. package/src/workspace/developer.ts +22 -5
  69. package/src/workspace/policy.ts +1 -1
  70. package/src/workspace/tree.ts +35 -6
@@ -1,17 +1,80 @@
1
1
  /**
2
- * Admin media routes. Media is files in the content repository, and there is no delete endpoint.
3
- * An upload is written on its own, before the entry save, because the editor needs its path first.
2
+ * Admin media routes. Media is files in the content repository. An upload is
3
+ * written on its own, before the entry save, because the editor needs its path
4
+ * first. A delete is a pending change like any other, and is refused while
5
+ * something uses the file.
4
6
  */
7
+ import { contentPath, ContentPathError, MEDIA_DIR } from "@micacms/core";
5
8
  import { Hono } from "hono";
9
+ import { z } from "zod";
6
10
  import { ApiError, badRequest, notFound } from "../lib/errors.js";
7
11
  import { isImageType } from "../lib/file-type.js";
8
12
  import { MAX_FILES_PER_UPLOAD } from "../middleware/index.js";
13
+ import { editorOf } from "../http.js";
14
+ const deleteSchema = z.object({ paths: z.array(z.string().min(1)).min(1).max(500) });
15
+ /** Content paths, or a 400 for one that leaves the content root. */
16
+ function cleanPaths(paths) {
17
+ try {
18
+ return [...new Set(paths.map((path) => contentPath(path)))];
19
+ }
20
+ catch (error) {
21
+ if (error instanceof ContentPathError)
22
+ throw badRequest(error.message);
23
+ throw error;
24
+ }
25
+ }
26
+ /** A file in a `media/` folder, as an upload writes it. Nothing else is deleted here. */
27
+ function mediaFilesOnly(paths) {
28
+ for (const path of paths) {
29
+ if (path.split("/").at(-2) !== MEDIA_DIR)
30
+ throw badRequest(`"${path}" is not a media file.`);
31
+ }
32
+ return paths;
33
+ }
34
+ function describeUses(path, referrers) {
35
+ const names = referrers.map((item) => item.title);
36
+ const listed = names.slice(0, 3).join(", ") + (names.length > 3 ? ` and ${names.length - 3} more` : "");
37
+ return `"${path.split("/").at(-1)}" is still used by ${listed}. Remove it there first.`;
38
+ }
9
39
  export function adminMediaRoutes() {
10
40
  const app = new Hono();
11
41
  app.get("/", async (c) => {
12
42
  const { media } = c.get("ctx").services;
13
43
  return c.json({ items: await media.list() });
14
44
  });
45
+ /** Where files are used, before a delete: `?path=a&path=b`. */
46
+ app.get("/usage", async (c) => {
47
+ const { content } = c.get("ctx").services;
48
+ const paths = cleanPaths(c.req.queries("path") ?? []);
49
+ if (paths.length === 0)
50
+ throw badRequest("Name at least one file.");
51
+ return c.json(await content.mediaUses(paths));
52
+ });
53
+ app.post("/delete", async (c) => {
54
+ const ctx = c.get("ctx");
55
+ const parsed = deleteSchema.safeParse(await c.req.json().catch(() => null));
56
+ if (!parsed.success)
57
+ throw badRequest("Expected a list of file paths.");
58
+ const paths = mediaFilesOnly(cleanPaths(parsed.data.paths));
59
+ for (const path of paths) {
60
+ const owner = path.split("/")[0] ?? "";
61
+ const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);
62
+ if (readOnly)
63
+ throw new ApiError("conflict", readOnly);
64
+ if ((await ctx.services.media.read(path)) === null)
65
+ throw notFound(`There is no file "${path}".`);
66
+ }
67
+ const { uses, unreadable } = await ctx.services.content.mediaUses(paths);
68
+ if (unreadable.length > 0) {
69
+ throw new ApiError("conflict", `Mica cannot read ${unreadable.join(", ")}, so it cannot tell whether these files are used.`);
70
+ }
71
+ const used = paths.filter((path) => (uses[path] ?? []).length > 0);
72
+ if (used.length > 0)
73
+ throw new ApiError("conflict", used.map((path) => describeUses(path, uses[path])).join(" "));
74
+ await ctx.site.contentRepository.apply({ changes: paths.map((path) => ({ type: "delete", path })) });
75
+ ctx.logger.info("Deleted media", { paths, editor: editorOf(c) });
76
+ return c.json({ deleted: paths });
77
+ });
15
78
  app.post("/", async (c) => {
16
79
  const ctx = c.get("ctx");
17
80
  const form = await c.req.formData().catch(() => {
@@ -34,6 +97,13 @@ export function adminMediaRoutes() {
34
97
  const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);
35
98
  if (readOnly)
36
99
  throw new ApiError("conflict", readOnly);
100
+ // A new entry uploads into the folder of the URL in its form; never into a saved entry's.
101
+ const folder = /^([^/]+)\/([^/]+)\/media$/.exec((directory ?? "").replace(/^\/+/, ""));
102
+ if (form.get("newEntry") === "true" && folder && ctx.site.model.collections[folder[1]]) {
103
+ if ((await ctx.services.content.revisionOf(folder[1], folder[2])) !== null) {
104
+ throw new ApiError("conflict", `Another entry already has the URL “${folder[2]}”. Change the URL, then upload.`);
105
+ }
106
+ }
37
107
  const prepared = await Promise.all(files.map((file) => ctx.services.media.prepare({
38
108
  file,
39
109
  directory,
@@ -1 +1 @@
1
- {"version":3,"file":"admin-media.js","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin media routes. Media is files in the content repository, and there is no delete endpoint.\n * An upload is written on its own, before the entry save, because the editor needs its path first.\n */\n\nimport { Hono } from \"hono\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport { isImageType } from \"../lib/file-type.js\";\nimport { MAX_FILES_PER_UPLOAD } from \"../middleware/index.js\";\nimport type { Env } from \"../http.js\";\n\nexport function adminMediaRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n return c.json({ items: await media.list() });\n });\n\n app.post(\"/\", async (c) => {\n const ctx = c.get(\"ctx\");\n const form = await c.req.formData().catch(() => {\n throw badRequest(\"Expected a multipart upload.\");\n });\n\n // The cast picks one of the two structurally different `File` types (Node's and undici's).\n const files = form\n .getAll(\"file\")\n .filter((value) => typeof value !== \"string\") as unknown as File[];\n\n if (files.length === 0) throw badRequest(\"No file was uploaded.\");\n if (files.length > MAX_FILES_PER_UPLOAD) {\n throw badRequest(`Upload at most ${MAX_FILES_PER_UPLOAD} files at a time.`);\n }\n\n const accept = form.get(\"accept\");\n // An entry's own `media/` directory or the shared one; the media service refuses anything else.\n const directory = typeof form.get(\"directory\") === \"string\" ? String(form.get(\"directory\")) : undefined;\n // The media service accepts `/gallery/x/media` too; take the owner from the path it will write to.\n const owner = (directory ?? \"\").replace(/^[/\\\\]+/, \"\").split(/[/\\\\]/)[0] ?? \"\";\n const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);\n if (readOnly) throw new ApiError(\"conflict\", readOnly);\n\n const prepared = await Promise.all(\n files.map((file) =>\n ctx.services.media.prepare({\n file,\n directory,\n accept: accept === \"image\" || accept === \"document\" ? accept : \"any\",\n }),\n ),\n );\n\n await ctx.site.contentRepository.apply({\n changes: prepared.map((item) => ({\n type: \"write\" as const,\n path: item.file.path,\n contents: item.contents,\n })),\n });\n\n return c.json({ items: prepared.map((item) => item.file) }, 201);\n });\n\n /**\n * Serves a file to the editor, behind the admin gate. The site build reads\n * the repository, not this route.\n */\n app.get(\"/file/*\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n const mediaPath = c.req.path.replace(/^.*\\/file\\//, \"\");\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(mediaPath);\n } catch {\n throw badRequest(\"The file path is not valid.\");\n }\n\n const found = await media.read(decoded);\n if (!found) throw notFound(\"No such file.\");\n\n return c.body(found.bytes as unknown as ArrayBuffer, 200, {\n // Text without a charset is decoded by guessing; the repository's text is UTF-8.\n \"content-type\": found.mimeType.startsWith(\"text/\") ? `${found.mimeType}; charset=utf-8` : found.mimeType,\n \"cache-control\": \"private, max-age=60\",\n // A file opened directly runs nothing and reaches nothing on this origin.\n \"content-security-policy\": \"sandbox; default-src 'none'\",\n // An image is shown as a preview; anything else is downloaded, never rendered here.\n ...(isImageType(found.mimeType) ? {} : { \"content-disposition\": \"attachment\" }),\n });\n });\n\n return app;\n}\n"]}
1
+ {"version":3,"file":"admin-media.js","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin media routes. Media is files in the content repository. An upload is\n * written on its own, before the entry save, because the editor needs its path\n * first. A delete is a pending change like any other, and is refused while\n * something uses the file.\n */\n\nimport { contentPath, ContentPathError, MEDIA_DIR } from \"@micacms/core\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport { isImageType } from \"../lib/file-type.js\";\nimport { MAX_FILES_PER_UPLOAD } from \"../middleware/index.js\";\nimport { editorOf, type Env } from \"../http.js\";\nimport type { Referrer } from \"../services/content-store.js\";\n\nconst deleteSchema = z.object({ paths: z.array(z.string().min(1)).min(1).max(500) });\n\n/** Content paths, or a 400 for one that leaves the content root. */\nfunction cleanPaths(paths: string[]): string[] {\n try {\n return [...new Set(paths.map((path) => contentPath(path)))];\n } catch (error) {\n if (error instanceof ContentPathError) throw badRequest(error.message);\n throw error;\n }\n}\n\n/** A file in a `media/` folder, as an upload writes it. Nothing else is deleted here. */\nfunction mediaFilesOnly(paths: string[]): string[] {\n for (const path of paths) {\n if (path.split(\"/\").at(-2) !== MEDIA_DIR) throw badRequest(`\"${path}\" is not a media file.`);\n }\n return paths;\n}\n\nfunction describeUses(path: string, referrers: Referrer[]): string {\n const names = referrers.map((item) => item.title);\n const listed = names.slice(0, 3).join(\", \") + (names.length > 3 ? ` and ${names.length - 3} more` : \"\");\n return `\"${path.split(\"/\").at(-1)}\" is still used by ${listed}. Remove it there first.`;\n}\n\nexport function adminMediaRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n return c.json({ items: await media.list() });\n });\n\n /** Where files are used, before a delete: `?path=a&path=b`. */\n app.get(\"/usage\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const paths = cleanPaths(c.req.queries(\"path\") ?? []);\n if (paths.length === 0) throw badRequest(\"Name at least one file.\");\n return c.json(await content.mediaUses(paths));\n });\n\n app.post(\"/delete\", async (c) => {\n const ctx = c.get(\"ctx\");\n const parsed = deleteSchema.safeParse(await c.req.json().catch(() => null));\n if (!parsed.success) throw badRequest(\"Expected a list of file paths.\");\n const paths = mediaFilesOnly(cleanPaths(parsed.data.paths));\n\n for (const path of paths) {\n const owner = path.split(\"/\")[0] ?? \"\";\n const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);\n if (readOnly) throw new ApiError(\"conflict\", readOnly);\n if ((await ctx.services.media.read(path)) === null) throw notFound(`There is no file \"${path}\".`);\n }\n\n const { uses, unreadable } = await ctx.services.content.mediaUses(paths);\n if (unreadable.length > 0) {\n throw new ApiError(\"conflict\", `Mica cannot read ${unreadable.join(\", \")}, so it cannot tell whether these files are used.`);\n }\n const used = paths.filter((path) => (uses[path] ?? []).length > 0);\n if (used.length > 0) throw new ApiError(\"conflict\", used.map((path) => describeUses(path, uses[path]!)).join(\" \"));\n\n await ctx.site.contentRepository.apply({ changes: paths.map((path) => ({ type: \"delete\" as const, path })) });\n ctx.logger.info(\"Deleted media\", { paths, editor: editorOf(c) });\n return c.json({ deleted: paths });\n });\n\n app.post(\"/\", async (c) => {\n const ctx = c.get(\"ctx\");\n const form = await c.req.formData().catch(() => {\n throw badRequest(\"Expected a multipart upload.\");\n });\n\n // The cast picks one of the two structurally different `File` types (Node's and undici's).\n const files = form\n .getAll(\"file\")\n .filter((value) => typeof value !== \"string\") as unknown as File[];\n\n if (files.length === 0) throw badRequest(\"No file was uploaded.\");\n if (files.length > MAX_FILES_PER_UPLOAD) {\n throw badRequest(`Upload at most ${MAX_FILES_PER_UPLOAD} files at a time.`);\n }\n\n const accept = form.get(\"accept\");\n // An entry's own `media/` directory or the shared one; the media service refuses anything else.\n const directory = typeof form.get(\"directory\") === \"string\" ? String(form.get(\"directory\")) : undefined;\n // The media service accepts `/gallery/x/media` too; take the owner from the path it will write to.\n const owner = (directory ?? \"\").replace(/^[/\\\\]+/, \"\").split(/[/\\\\]/)[0] ?? \"\";\n const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);\n if (readOnly) throw new ApiError(\"conflict\", readOnly);\n // A new entry uploads into the folder of the URL in its form; never into a saved entry's.\n const folder = /^([^/]+)\\/([^/]+)\\/media$/.exec((directory ?? \"\").replace(/^\\/+/, \"\"));\n if (form.get(\"newEntry\") === \"true\" && folder && ctx.site.model.collections[folder[1]!]) {\n if ((await ctx.services.content.revisionOf(folder[1]!, folder[2]!)) !== null) {\n throw new ApiError(\"conflict\", `Another entry already has the URL “${folder[2]}”. Change the URL, then upload.`);\n }\n }\n\n const prepared = await Promise.all(\n files.map((file) =>\n ctx.services.media.prepare({\n file,\n directory,\n accept: accept === \"image\" || accept === \"document\" ? accept : \"any\",\n }),\n ),\n );\n\n await ctx.site.contentRepository.apply({\n changes: prepared.map((item) => ({\n type: \"write\" as const,\n path: item.file.path,\n contents: item.contents,\n })),\n });\n\n return c.json({ items: prepared.map((item) => item.file) }, 201);\n });\n\n /**\n * Serves a file to the editor, behind the admin gate. The site build reads\n * the repository, not this route.\n */\n app.get(\"/file/*\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n const mediaPath = c.req.path.replace(/^.*\\/file\\//, \"\");\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(mediaPath);\n } catch {\n throw badRequest(\"The file path is not valid.\");\n }\n\n const found = await media.read(decoded);\n if (!found) throw notFound(\"No such file.\");\n\n return c.body(found.bytes as unknown as ArrayBuffer, 200, {\n // Text without a charset is decoded by guessing; the repository's text is UTF-8.\n \"content-type\": found.mimeType.startsWith(\"text/\") ? `${found.mimeType}; charset=utf-8` : found.mimeType,\n \"cache-control\": \"private, max-age=60\",\n // A file opened directly runs nothing and reaches nothing on this origin.\n \"content-security-policy\": \"sandbox; default-src 'none'\",\n // An image is shown as a preview; anything else is downloaded, never rendered here.\n ...(isImageType(found.mimeType) ? {} : { \"content-disposition\": \"attachment\" }),\n });\n });\n\n return app;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"admin-staging.d.ts","sourceRoot":"","sources":["../../src/routes/admin-staging.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAW,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AA4C/C,wBAAgB,kBAAkB,IAAI,IAAI,CAAC,GAAG,CAAC,CAuE9C"}
1
+ {"version":3,"file":"admin-staging.d.ts","sourceRoot":"","sources":["../../src/routes/admin-staging.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAqB,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AAiDzD,wBAAgB,kBAAkB,IAAI,IAAI,CAAC,GAAG,CAAC,CAiF9C"}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { Hono } from "hono";
8
8
  import { z } from "zod";
9
- import { actorOf } from "../http.js";
9
+ import { actorOf, editorOf } from "../http.js";
10
10
  import { ApiError, badRequest } from "../lib/errors.js";
11
11
  import { ConflictError } from "../workspace/files.js";
12
12
  const publishSchema = z.object({
@@ -45,11 +45,15 @@ async function handled(run) {
45
45
  throw error;
46
46
  }
47
47
  }
48
+ /** Which model is in use, so that the editor can tell a stale form, and why a newer one is not. */
49
+ function modelState(ctx) {
50
+ return { revision: ctx.site.schemaRevision, warning: ctx.site.modelWarning?.message ?? null };
51
+ }
48
52
  export function adminStagingRoutes() {
49
53
  const app = new Hono();
50
54
  app.get("/changes", async (c) => {
51
- const { staging } = c.get("ctx").services;
52
- return c.json(await staging.status());
55
+ const ctx = c.get("ctx");
56
+ return c.json({ ...(await ctx.services.staging.status()), model: modelState(ctx) });
53
57
  });
54
58
  // Raw data before and after one pending change; the editor renders the comparison.
55
59
  app.get("/changes/compare", async (c) => {
@@ -67,6 +71,12 @@ export function adminStagingRoutes() {
67
71
  return c.json(await staging.compare(parsed.data, against));
68
72
  });
69
73
  app.post("/publish", async (c) => {
74
+ // Publish validates with the newest model, so a model change never needs a restart first.
75
+ await c.get("ctx").refreshModel({ force: true });
76
+ const warning = c.get("ctx").site.modelWarning;
77
+ if (warning) {
78
+ throw new ApiError("conflict", `The website's new content model cannot be read, so Mica cannot publish yet. Tell the developer: ${warning.message}`);
79
+ }
70
80
  const { staging } = c.get("ctx").services;
71
81
  const parsed = publishSchema.safeParse(await jsonBody(c));
72
82
  if (!parsed.success)
@@ -76,6 +86,7 @@ export function adminStagingRoutes() {
76
86
  message: parsed.data.message,
77
87
  paths: parsed.data.paths,
78
88
  author: actor ? { name: actor.name || actor.email, email: actor.email } : undefined,
89
+ editor: editorOf(c),
79
90
  }));
80
91
  return c.json(result);
81
92
  });
@@ -87,22 +98,22 @@ export function adminStagingRoutes() {
87
98
  app.post("/sync", async (c) => {
88
99
  const { staging } = c.get("ctx").services;
89
100
  await staging.sync();
90
- return c.json(await staging.status());
101
+ return c.json({ ...(await staging.status()), model: modelState(c.get("ctx")) });
91
102
  });
92
103
  app.post("/changes/resolve", async (c) => {
93
104
  const { staging } = c.get("ctx").services;
94
105
  const parsed = resolveSchema.safeParse(await jsonBody(c));
95
106
  if (!parsed.success)
96
107
  throw badRequest("Expected a group and a choice: mine or website.");
97
- await handled(() => staging.resolve(parsed.data.group, parsed.data.choice));
98
- return c.json(await staging.status());
108
+ await handled(() => staging.resolve(parsed.data.group, parsed.data.choice, editorOf(c)));
109
+ return c.json({ ...(await staging.status()), model: modelState(c.get("ctx")) });
99
110
  });
100
111
  app.post("/changes/discard", async (c) => {
101
112
  const { staging } = c.get("ctx").services;
102
113
  const parsed = discardSchema.safeParse(await jsonBody(c));
103
114
  if (!parsed.success)
104
115
  throw badRequest("Expected a list of change paths.");
105
- return c.json(await handled(() => staging.discard(parsed.data.paths)));
116
+ return c.json(await handled(() => staging.discard(parsed.data.paths, editorOf(c))));
106
117
  });
107
118
  return app;
108
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"admin-staging.js","sourceRoot":"","sources":["../../src/routes/admin-staging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin routes for the pending changes and Publish. Sync and conflict\n * resolution exist only for a clone that Mica keeps. `committed-pending-push`\n * is a 200: the commit is real, and an error status would tell the editor that\n * nothing happened.\n */\n\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { actorOf, type Env } from \"../http.js\";\nimport { ApiError, badRequest } from \"../lib/errors.js\";\nimport { ConflictError } from \"../workspace/files.js\";\n\nconst publishSchema = z.object({\n message: z.string().max(500),\n /** The reviewed paths, with the revision each had in the review. */\n paths: z.record(z.string().min(1), z.string().nullable()),\n});\n\nconst resolveSchema = z.object({\n group: z.string().min(1),\n choice: z.enum([\"mine\", \"website\"]),\n});\n\nconst discardSchema = z.object({ paths: z.array(z.string().min(1)).min(1).max(2000) });\n\nconst compareSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"entry\"),\n collection: z.string().min(1),\n slug: z.string().min(1),\n from: z.string().min(1).optional(),\n }),\n z.object({ kind: z.literal(\"singleton\"), name: z.string().min(1) }),\n z.object({ kind: z.literal(\"order\"), collection: z.string().min(1) }),\n]);\n\nasync function jsonBody(c: { req: { json(): Promise<unknown> } }): Promise<unknown> {\n return c.req.json().catch(() => {\n throw badRequest(\"The request body must be valid JSON.\");\n });\n}\n\n/** A repository conflict is 409 and recoverable — refresh, review, retry. */\nasync function handled<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof ConflictError) throw new ApiError(\"conflict\", error.message);\n throw error;\n }\n}\n\nexport function adminStagingRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/changes\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n return c.json(await staging.status());\n });\n\n // Raw data before and after one pending change; the editor renders the comparison.\n app.get(\"/changes/compare\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = compareSchema.safeParse({\n kind: c.req.query(\"kind\"),\n collection: c.req.query(\"collection\"),\n slug: c.req.query(\"slug\"),\n from: c.req.query(\"from\"),\n name: c.req.query(\"name\"),\n });\n if (!parsed.success) throw badRequest(\"The compare target is malformed.\");\n const against = c.req.query(\"against\") === \"origin\" ? \"origin\" : \"head\";\n\n return c.json(await staging.compare(parsed.data, against));\n });\n\n app.post(\"/publish\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = publishSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The publish payload is malformed.\");\n\n const actor = actorOf(c);\n const result = await handled(() =>\n staging.publish({\n message: parsed.data.message,\n paths: parsed.data.paths,\n author: actor ? { name: actor.name || actor.email, email: actor.email } : undefined,\n }),\n );\n\n return c.json(result);\n });\n\n app.post(\"/publish/push\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n return c.json(await handled(() => staging.push()));\n });\n\n // \"Try again\" after a sync error.\n app.post(\"/sync\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n await staging.sync();\n return c.json(await staging.status());\n });\n\n app.post(\"/changes/resolve\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = resolveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a group and a choice: mine or website.\");\n\n await handled(() => staging.resolve(parsed.data.group, parsed.data.choice));\n return c.json(await staging.status());\n });\n\n app.post(\"/changes/discard\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = discardSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a list of change paths.\");\n\n return c.json(await handled(() => staging.discard(parsed.data.paths)));\n });\n\n return app;\n}\n"]}
1
+ {"version":3,"file":"admin-staging.js","sourceRoot":"","sources":["../../src/routes/admin-staging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin routes for the pending changes and Publish. Sync and conflict\n * resolution exist only for a clone that Mica keeps. `committed-pending-push`\n * is a 200: the commit is real, and an error status would tell the editor that\n * nothing happened.\n */\n\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { actorOf, editorOf, type Env } from \"../http.js\";\nimport { ApiError, badRequest } from \"../lib/errors.js\";\nimport { ConflictError } from \"../workspace/files.js\";\n\nconst publishSchema = z.object({\n message: z.string().max(500),\n /** The reviewed paths, with the revision each had in the review. */\n paths: z.record(z.string().min(1), z.string().nullable()),\n});\n\nconst resolveSchema = z.object({\n group: z.string().min(1),\n choice: z.enum([\"mine\", \"website\"]),\n});\n\nconst discardSchema = z.object({ paths: z.array(z.string().min(1)).min(1).max(2000) });\n\nconst compareSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"entry\"),\n collection: z.string().min(1),\n slug: z.string().min(1),\n from: z.string().min(1).optional(),\n }),\n z.object({ kind: z.literal(\"singleton\"), name: z.string().min(1) }),\n z.object({ kind: z.literal(\"order\"), collection: z.string().min(1) }),\n]);\n\nasync function jsonBody(c: { req: { json(): Promise<unknown> } }): Promise<unknown> {\n return c.req.json().catch(() => {\n throw badRequest(\"The request body must be valid JSON.\");\n });\n}\n\n/** A repository conflict is 409 and recoverable — refresh, review, retry. */\nasync function handled<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof ConflictError) throw new ApiError(\"conflict\", error.message);\n throw error;\n }\n}\n\n/** Which model is in use, so that the editor can tell a stale form, and why a newer one is not. */\nfunction modelState(ctx: Env[\"Variables\"][\"ctx\"]) {\n return { revision: ctx.site.schemaRevision, warning: ctx.site.modelWarning?.message ?? null };\n}\n\nexport function adminStagingRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/changes\", async (c) => {\n const ctx = c.get(\"ctx\");\n return c.json({ ...(await ctx.services.staging.status()), model: modelState(ctx) });\n });\n\n // Raw data before and after one pending change; the editor renders the comparison.\n app.get(\"/changes/compare\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = compareSchema.safeParse({\n kind: c.req.query(\"kind\"),\n collection: c.req.query(\"collection\"),\n slug: c.req.query(\"slug\"),\n from: c.req.query(\"from\"),\n name: c.req.query(\"name\"),\n });\n if (!parsed.success) throw badRequest(\"The compare target is malformed.\");\n const against = c.req.query(\"against\") === \"origin\" ? \"origin\" : \"head\";\n\n return c.json(await staging.compare(parsed.data, against));\n });\n\n app.post(\"/publish\", async (c) => {\n // Publish validates with the newest model, so a model change never needs a restart first.\n await c.get(\"ctx\").refreshModel({ force: true });\n const warning = c.get(\"ctx\").site.modelWarning;\n if (warning) {\n throw new ApiError(\n \"conflict\",\n `The website's new content model cannot be read, so Mica cannot publish yet. Tell the developer: ${warning.message}`,\n );\n }\n const { staging } = c.get(\"ctx\").services;\n const parsed = publishSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The publish payload is malformed.\");\n\n const actor = actorOf(c);\n const result = await handled(() =>\n staging.publish({\n message: parsed.data.message,\n paths: parsed.data.paths,\n author: actor ? { name: actor.name || actor.email, email: actor.email } : undefined,\n editor: editorOf(c),\n }),\n );\n\n return c.json(result);\n });\n\n app.post(\"/publish/push\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n return c.json(await handled(() => staging.push()));\n });\n\n // \"Try again\" after a sync error.\n app.post(\"/sync\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n await staging.sync();\n return c.json({ ...(await staging.status()), model: modelState(c.get(\"ctx\")) });\n });\n\n app.post(\"/changes/resolve\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = resolveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a group and a choice: mine or website.\");\n\n await handled(() => staging.resolve(parsed.data.group, parsed.data.choice, editorOf(c)));\n return c.json({ ...(await staging.status()), model: modelState(c.get(\"ctx\")) });\n });\n\n app.post(\"/changes/discard\", async (c) => {\n const { staging } = c.get(\"ctx\").services;\n const parsed = discardSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a list of change paths.\");\n\n return c.json(await handled(() => staging.discard(parsed.data.paths, editorOf(c))));\n });\n\n return app;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"admin-system.d.ts","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAMtC,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,GAAG,CAAC,CAkI7C"}
1
+ {"version":3,"file":"admin-system.d.ts","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAMtC,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,GAAG,CAAC,CAsI7C"}
@@ -34,6 +34,8 @@ export function adminSystemRoutes() {
34
34
  fields: def.fields,
35
35
  readOnly: site.readOnly.get(`singletons.${def.name}`) ?? null,
36
36
  })),
37
+ /** Which model this is, so that a page can tell that a newer one is in use. */
38
+ revision: site.schemaRevision,
37
39
  });
38
40
  });
39
41
  app.get("/overview", async (c) => {
@@ -87,6 +89,8 @@ export function adminSystemRoutes() {
87
89
  entries: counts[item.name] ?? 0,
88
90
  })),
89
91
  singletons: stats.singletons,
92
+ /** Why a newer model in the working tree is not in use, or null. */
93
+ warning: ctx.site.modelWarning?.message ?? null,
90
94
  },
91
95
  repository: {
92
96
  origin: ctx.site.contentRepository.origin,
@@ -1 +1 @@
1
- {"version":3,"file":"admin-system.js","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schema, dashboard and system information. The SPA knows no collection; it\n * renders what `/schema` returns.\n */\n\nimport { Hono } from \"hono\";\n\nimport { listedFields, resolveSlugField, resolveTitleField } from \"@micacms/core\";\n\nimport type { Env } from \"../http.js\";\nimport { schemaStats } from \"../lib/schema-stats.js\";\nimport { schemaOrigin } from \"../schema.js\";\nimport { VERSION } from \"../lib/version.js\";\nimport { formatBytes } from \"../services/media.js\";\n\nexport function adminSystemRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n /** The content model, plus the derived values that the UI would otherwise recompute. */\n app.get(\"/schema\", (c) => {\n const { site } = c.get(\"ctx\");\n\n return c.json({\n collections: Object.values(site.model.collections).map((def) => ({\n name: def.name,\n label: def.label,\n labelSingular: def.labelSingular ?? def.label,\n description: def.description ?? null,\n sortable: def.sortable ?? false,\n defaultSort: def.defaultSort ?? null,\n titleField: resolveTitleField(def),\n slugField: resolveSlugField(def),\n listedFields: listedFields(def),\n fields: def.fields,\n readOnly: site.readOnly.get(`collections.${def.name}`) ?? null,\n })),\n singletons: Object.values(site.model.singletons).map((def) => ({\n name: def.name,\n label: def.label,\n description: def.description ?? null,\n fields: def.fields,\n readOnly: site.readOnly.get(`singletons.${def.name}`) ?? null,\n })),\n });\n });\n\n app.get(\"/overview\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { content, media } = ctx.services;\n\n const collections = await Promise.all(\n Object.values(ctx.site.model.collections).map(async (def) => ({\n name: def.name,\n label: def.label,\n total: (await content.list(def.name)).length,\n })),\n );\n\n const files = await media.list();\n\n return c.json({\n collections,\n media: {\n files: files.length,\n bytes: files.reduce((sum, file) => sum + file.size, 0),\n readable: formatBytes(files.reduce((sum, file) => sum + file.size, 0)),\n },\n repository: ctx.site.contentRepository.origin,\n });\n });\n\n /**\n * What this deployment is and serves. Every admitted editor sees it, because\n * there is no administrator role. It never returns a secret value.\n */\n app.get(\"/system\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { config } = ctx;\n\n const stats = schemaStats(ctx.site.model);\n\n const counts = Object.fromEntries(\n await Promise.all(\n Object.values(ctx.site.model.collections).map(\n async (def) => [def.name, (await ctx.services.content.list(def.name)).length] as const,\n ),\n ),\n );\n\n const files = await ctx.services.media.list();\n const mediaBytes = files.reduce((sum, file) => sum + file.size, 0);\n\n const uptimeSeconds = Math.floor(process.uptime());\n\n const operational = ctx.operational();\n const repository = await ctx.services.staging.repository();\n\n return c.json({\n cms: {\n version: VERSION,\n environment: config.NODE_ENV,\n node: process.version,\n platform: `${process.platform}/${process.arch}`,\n startedAt: new Date(Date.now() - uptimeSeconds * 1000).toISOString(),\n uptimeSeconds,\n },\n schema: {\n totals: stats.totals,\n kinds: stats.kinds,\n relations: stats.relations,\n collections: stats.collections.map((item) => ({\n ...item,\n entries: counts[item.name] ?? 0,\n })),\n singletons: stats.singletons,\n },\n repository: {\n origin: ctx.site.contentRepository.origin,\n entries: Object.values(counts).reduce((sum, count) => sum + count, 0),\n media: { files: files.length, bytes: mediaBytes, readable: formatBytes(mediaBytes) },\n ...repository,\n },\n deployment: {\n site: ctx.site.id,\n baseUrl: config.MICA_PUBLIC_URL,\n schemaOrigin: schemaOrigin(ctx.site.contentRepository),\n // A clone that Mica keeps names its remote in `repository.url` instead.\n contentRoot: repository.owner === \"developer\" ? config.contentRoot : null,\n dataDir: operational.dataDir ?? null,\n logLevel: config.MICA_LOG_LEVEL,\n maxUploadBytes: config.MICA_MAX_UPLOAD_BYTES,\n maxUploadReadable: formatBytes(config.MICA_MAX_UPLOAD_BYTES),\n sessionTtlHours: operational.sessionTtlHours ?? null,\n sessionIdleHours: operational.sessionIdleHours ?? null,\n auth: operational.auth,\n admission: operational.admission ?? null,\n trustProxy: config.MICA_TRUST_PROXY,\n adminOrigins: config.adminOrigins,\n generatedSecrets: operational.generatedSecrets ?? [],\n memoryBytes: process.memoryUsage().rss,\n },\n });\n });\n\n return app;\n}\n"]}
1
+ {"version":3,"file":"admin-system.js","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schema, dashboard and system information. The SPA knows no collection; it\n * renders what `/schema` returns.\n */\n\nimport { Hono } from \"hono\";\n\nimport { listedFields, resolveSlugField, resolveTitleField } from \"@micacms/core\";\n\nimport type { Env } from \"../http.js\";\nimport { schemaStats } from \"../lib/schema-stats.js\";\nimport { schemaOrigin } from \"../schema.js\";\nimport { VERSION } from \"../lib/version.js\";\nimport { formatBytes } from \"../services/media.js\";\n\nexport function adminSystemRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n /** The content model, plus the derived values that the UI would otherwise recompute. */\n app.get(\"/schema\", (c) => {\n const { site } = c.get(\"ctx\");\n\n return c.json({\n collections: Object.values(site.model.collections).map((def) => ({\n name: def.name,\n label: def.label,\n labelSingular: def.labelSingular ?? def.label,\n description: def.description ?? null,\n sortable: def.sortable ?? false,\n defaultSort: def.defaultSort ?? null,\n titleField: resolveTitleField(def),\n slugField: resolveSlugField(def),\n listedFields: listedFields(def),\n fields: def.fields,\n readOnly: site.readOnly.get(`collections.${def.name}`) ?? null,\n })),\n singletons: Object.values(site.model.singletons).map((def) => ({\n name: def.name,\n label: def.label,\n description: def.description ?? null,\n fields: def.fields,\n readOnly: site.readOnly.get(`singletons.${def.name}`) ?? null,\n })),\n /** Which model this is, so that a page can tell that a newer one is in use. */\n revision: site.schemaRevision,\n });\n });\n\n app.get(\"/overview\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { content, media } = ctx.services;\n\n const collections = await Promise.all(\n Object.values(ctx.site.model.collections).map(async (def) => ({\n name: def.name,\n label: def.label,\n total: (await content.list(def.name)).length,\n })),\n );\n\n const files = await media.list();\n\n return c.json({\n collections,\n media: {\n files: files.length,\n bytes: files.reduce((sum, file) => sum + file.size, 0),\n readable: formatBytes(files.reduce((sum, file) => sum + file.size, 0)),\n },\n repository: ctx.site.contentRepository.origin,\n });\n });\n\n /**\n * What this deployment is and serves. Every admitted editor sees it, because\n * there is no administrator role. It never returns a secret value.\n */\n app.get(\"/system\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { config } = ctx;\n\n const stats = schemaStats(ctx.site.model);\n\n const counts = Object.fromEntries(\n await Promise.all(\n Object.values(ctx.site.model.collections).map(\n async (def) => [def.name, (await ctx.services.content.list(def.name)).length] as const,\n ),\n ),\n );\n\n const files = await ctx.services.media.list();\n const mediaBytes = files.reduce((sum, file) => sum + file.size, 0);\n\n const uptimeSeconds = Math.floor(process.uptime());\n\n const operational = ctx.operational();\n const repository = await ctx.services.staging.repository();\n\n return c.json({\n cms: {\n version: VERSION,\n environment: config.NODE_ENV,\n node: process.version,\n platform: `${process.platform}/${process.arch}`,\n startedAt: new Date(Date.now() - uptimeSeconds * 1000).toISOString(),\n uptimeSeconds,\n },\n schema: {\n totals: stats.totals,\n kinds: stats.kinds,\n relations: stats.relations,\n collections: stats.collections.map((item) => ({\n ...item,\n entries: counts[item.name] ?? 0,\n })),\n singletons: stats.singletons,\n /** Why a newer model in the working tree is not in use, or null. */\n warning: ctx.site.modelWarning?.message ?? null,\n },\n repository: {\n origin: ctx.site.contentRepository.origin,\n entries: Object.values(counts).reduce((sum, count) => sum + count, 0),\n media: { files: files.length, bytes: mediaBytes, readable: formatBytes(mediaBytes) },\n ...repository,\n },\n deployment: {\n site: ctx.site.id,\n baseUrl: config.MICA_PUBLIC_URL,\n schemaOrigin: schemaOrigin(ctx.site.contentRepository),\n // A clone that Mica keeps names its remote in `repository.url` instead.\n contentRoot: repository.owner === \"developer\" ? config.contentRoot : null,\n dataDir: operational.dataDir ?? null,\n logLevel: config.MICA_LOG_LEVEL,\n maxUploadBytes: config.MICA_MAX_UPLOAD_BYTES,\n maxUploadReadable: formatBytes(config.MICA_MAX_UPLOAD_BYTES),\n sessionTtlHours: operational.sessionTtlHours ?? null,\n sessionIdleHours: operational.sessionIdleHours ?? null,\n auth: operational.auth,\n admission: operational.admission ?? null,\n trustProxy: config.MICA_TRUST_PROXY,\n adminOrigins: config.adminOrigins,\n generatedSecrets: operational.generatedSecrets ?? [],\n memoryBytes: process.memoryUsage().rss,\n },\n });\n });\n\n return app;\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  import type { Author } from "../workspace/files.js";
3
3
  import { type ContentChange, type ContentSource, type FieldIssue, type PublishResult, type Site } from "@micacms/core";
4
4
  import type { SyncStatus } from "../workspace/policy.js";
5
+ import type { UnpushedCommits } from "../workspace/tree.js";
5
6
  /** Where the change happened, in words an editor recognises. */
6
7
  export type ChangeOwner = {
7
8
  kind: "entry";
@@ -32,6 +33,13 @@ export interface AnnotatedChange extends ContentChange {
32
33
  * saved after the review is not published unseen.
33
34
  */
34
35
  revision?: string | null;
36
+ /** When the file was last saved, for "changed 2 hours ago". Null for a deletion. */
37
+ savedAt?: string | null;
38
+ /**
39
+ * New pending files that this document uses (a linked entry, an image), so
40
+ * that it cannot be published without them. On the document's data file.
41
+ */
42
+ needs?: string[];
35
43
  }
36
44
  /** One thing that would make the staged result unpublishable. */
37
45
  export interface StagingProblem {
@@ -55,8 +63,12 @@ export interface StagingStatus {
55
63
  head: string | null;
56
64
  /** The branch a publication lands on, where the workspace knows it. */
57
65
  branch: string | null;
58
- /** Commits created and not yet pushed. */
59
- ahead: number | null;
66
+ /**
67
+ * Commits that no remote has, in the developer's checkout: Mica's waits for a
68
+ * retry, another one blocks Publish. Null for a clone that Mica keeps, or
69
+ * without a remote.
70
+ */
71
+ unpushed: UnpushedCommits | null;
60
72
  changes: AnnotatedChange[];
61
73
  validation: StagingValidation;
62
74
  /** Where a clone that Mica keeps stands against its remote. Null for the developer's checkout. */
@@ -72,6 +84,8 @@ export interface PublishInput {
72
84
  */
73
85
  paths: Record<string, string | null>;
74
86
  author?: Author;
87
+ /** Who published, for the log. */
88
+ editor?: string;
75
89
  }
76
90
  /** What the System page shows about the repository. */
77
91
  export interface RepositoryInfo {
@@ -86,7 +100,8 @@ export interface RepositoryInfo {
86
100
  export interface ContentStagingService {
87
101
  status(): Promise<StagingStatus>;
88
102
  publish(input: PublishInput): Promise<PublishResult>;
89
- discard(paths: string[]): Promise<{
103
+ /** `editor` names who discarded, for the log. */
104
+ discard(paths: string[], editor?: string): Promise<{
90
105
  discarded: string[];
91
106
  }>;
92
107
  /** Retries the push after `committed-pending-push`. */
@@ -98,7 +113,7 @@ export interface ContentStagingService {
98
113
  compare(target: CompareTarget, against?: "head" | "origin"): Promise<Comparison>;
99
114
  /** Brings a clone that Mica keeps up to date now. Refused for the developer's checkout. */
100
115
  sync(): Promise<void>;
101
- resolve(group: string, choice: "mine" | "website"): Promise<void>;
116
+ resolve(group: string, choice: "mine" | "website", editor?: string): Promise<void>;
102
117
  repository(): Promise<RepositoryInfo>;
103
118
  }
104
119
  /** What to compare on the Publish page. */
@@ -1 +1 @@
1
- {"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../../src/services/changes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../../src/services/changes.ts"],"names":[],"mappings":"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"}
@@ -87,6 +87,21 @@ published) {
87
87
  if (published) {
88
88
  await detectRenames(model, published, source, changes, owners, entriesByCollection);
89
89
  }
90
+ const created = new Set(changes.filter((change) => change.kind === "create").map((change) => change.path));
91
+ const needsByPath = new Map();
92
+ /** How to read each changed document as the website has it, by its data file. */
93
+ const publishedOf = new Map();
94
+ const addNeeds = (dataPath, fields, data) => {
95
+ const needed = new Set();
96
+ for (const reference of collectReferences(fields, data)) {
97
+ const def = reference.kind === "relation" ? model.collections[reference.collection] : undefined;
98
+ const path = reference.kind === "media" ? reference.path : def ? entryDataPath(def, reference.collection, reference.slug) : null;
99
+ if (path && path !== dataPath && created.has(path))
100
+ needed.add(path);
101
+ }
102
+ if (needed.size > 0)
103
+ needsByPath.set(dataPath, [...needed]);
104
+ };
90
105
  const validated = new Set();
91
106
  const targets = createReferenceCheck(model, source, (name) => {
92
107
  const entries = entriesByCollection.get(name);
@@ -103,6 +118,11 @@ published) {
103
118
  const data = entriesByCollection.get(owner.collection)?.get(owner.slug);
104
119
  if (!data)
105
120
  continue;
121
+ addNeeds(entryDataPath(def, owner.collection, owner.slug), def.fields, data);
122
+ if (published) {
123
+ const { collection, slug } = owner;
124
+ publishedOf.set(entryDataPath(def, collection, slug), () => readCollectionEntry(published, def, collection, slug).catch(() => null));
125
+ }
106
126
  const result = validateFields(def.fields, data);
107
127
  const issues = [...(result.ok ? [] : result.issues), ...(await targets.issues(def.fields, data))];
108
128
  if (issues.length > 0) {
@@ -121,6 +141,13 @@ published) {
121
141
  const data = await readSingleton(source, def, owner.name);
122
142
  if (data === null)
123
143
  continue;
144
+ // The data file, not the folder: a folder-layout singleton's change is `<name>/index.json`.
145
+ const dataFile = entryLayout(def.fields) === "directory" ? `${dataPath}/index.json` : dataPath;
146
+ addNeeds(dataFile, def.fields, data);
147
+ if (published) {
148
+ const name = owner.name;
149
+ publishedOf.set(dataFile, () => readSingleton(published, def, name).catch(() => null));
150
+ }
124
151
  const result = validateFields(def.fields, data);
125
152
  const issues = [...(result.ok ? [] : result.issues), ...(await targets.issues(def.fields, data))];
126
153
  if (issues.length > 0)
@@ -135,23 +162,55 @@ published) {
135
162
  }
136
163
  }
137
164
  }
165
+ // A URL change and the links that follow it are published together, both ways. A
166
+ // document follows when the website's version of it links to the old URL; a new
167
+ // draft that links to the new URL only needs the change, and does not hold it back.
168
+ const renamed = new Map(owners.flatMap((owner) => owner.kind === "entry" && owner.renamedFrom
169
+ ? [[entryDataPath(model.collections[owner.collection], owner.collection, owner.slug), owner]]
170
+ : []));
171
+ for (const [document, needed] of [...needsByPath]) {
172
+ for (const path of needed) {
173
+ const target = renamed.get(path);
174
+ if (!target || target.kind !== "entry")
175
+ continue;
176
+ const before = await publishedOf.get(document)?.();
177
+ const fields = before && fieldsOfDocument(model, document);
178
+ const linkedBefore = before &&
179
+ fields &&
180
+ collectReferences(fields, before).some((reference) => reference.kind === "relation" && reference.collection === target.collection && reference.slug === target.renamedFrom);
181
+ if (linkedBefore)
182
+ needsByPath.set(path, [...new Set([...(needsByPath.get(path) ?? []), document])]);
183
+ }
184
+ }
138
185
  problems.push(...(await brokenByRemoval(model, source, changes, owners, entriesByCollection, titles)));
139
186
  const annotated = changes.map((change, index) => {
140
187
  const owner = owners[index];
188
+ const needs = needsByPath.get(change.path);
189
+ const extra = needs ? { needs } : {};
141
190
  if (owner.kind === "entry") {
142
191
  const def = model.collections[owner.collection];
143
192
  return {
144
193
  ...change,
194
+ ...extra,
145
195
  owner: { ...owner, label: labelEntry(def, owner.slug, titles.get(`${owner.collection}/${owner.slug}`)) },
146
196
  };
147
197
  }
148
- return { ...change, owner };
198
+ return { ...change, ...extra, owner };
149
199
  });
150
200
  return {
151
201
  changes: annotated,
152
202
  validation: { ok: problems.length === 0, problems },
153
203
  };
154
204
  }
205
+ /** The fields of the entry or singleton whose data file is `path`. */
206
+ function fieldsOfDocument(model, path) {
207
+ const owner = classifyPath(model, path);
208
+ if (owner.kind === "entry")
209
+ return model.collections[owner.collection]?.fields ?? null;
210
+ if (owner.kind === "singleton")
211
+ return model.singletons[owner.name]?.fields ?? null;
212
+ return null;
213
+ }
155
214
  /** Which piece of content a file belongs to, read from the model's path layout. */
156
215
  export function classifyPath(model, path) {
157
216
  const segments = path.split("/");