@micacms/runtime 0.4.1 → 0.5.0-rc.2

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 (112) hide show
  1. package/README.md +2 -2
  2. package/dist/app.d.ts +8 -37
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +7 -42
  5. package/dist/app.js.map +1 -1
  6. package/dist/config.d.ts +12 -82
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +14 -82
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +37 -66
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +24 -75
  13. package/dist/context.js.map +1 -1
  14. package/dist/http.d.ts +8 -16
  15. package/dist/http.d.ts.map +1 -1
  16. package/dist/http.js +6 -13
  17. package/dist/http.js.map +1 -1
  18. package/dist/index.d.ts +13 -14
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -14
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/errors.js.map +1 -1
  23. package/dist/lib/file-type.js.map +1 -1
  24. package/dist/lib/id.js.map +1 -1
  25. package/dist/lib/logger.js.map +1 -1
  26. package/dist/lib/ports.js.map +1 -1
  27. package/dist/lib/rate-limit.js.map +1 -1
  28. package/dist/lib/schema-stats.js.map +1 -1
  29. package/dist/lib/version.js.map +1 -1
  30. package/dist/middleware/index.js.map +1 -1
  31. package/dist/routes/admin-content.d.ts +4 -17
  32. package/dist/routes/admin-content.d.ts.map +1 -1
  33. package/dist/routes/admin-content.js +11 -51
  34. package/dist/routes/admin-content.js.map +1 -1
  35. package/dist/routes/admin-media.d.ts +3 -14
  36. package/dist/routes/admin-media.d.ts.map +1 -1
  37. package/dist/routes/admin-media.js +7 -29
  38. package/dist/routes/admin-media.js.map +1 -1
  39. package/dist/routes/admin-spa.js.map +1 -1
  40. package/dist/routes/admin-staging.d.ts +4 -9
  41. package/dist/routes/admin-staging.d.ts.map +1 -1
  42. package/dist/routes/admin-staging.js +31 -18
  43. package/dist/routes/admin-staging.js.map +1 -1
  44. package/dist/routes/admin-system.d.ts +2 -8
  45. package/dist/routes/admin-system.d.ts.map +1 -1
  46. package/dist/routes/admin-system.js +9 -44
  47. package/dist/routes/admin-system.js.map +1 -1
  48. package/dist/routes/health.d.ts +3 -10
  49. package/dist/routes/health.d.ts.map +1 -1
  50. package/dist/routes/health.js +9 -26
  51. package/dist/routes/health.js.map +1 -1
  52. package/dist/schema.d.ts +10 -38
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +14 -65
  55. package/dist/schema.js.map +1 -1
  56. package/dist/server.d.ts +2 -10
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +3 -9
  59. package/dist/server.js.map +1 -1
  60. package/dist/services/changes.d.ts +150 -0
  61. package/dist/services/changes.d.ts.map +1 -0
  62. package/dist/services/changes.js +339 -0
  63. package/dist/services/changes.js.map +1 -0
  64. package/dist/services/content-store.d.ts +61 -0
  65. package/dist/services/content-store.d.ts.map +1 -0
  66. package/dist/services/content-store.js +330 -0
  67. package/dist/services/content-store.js.map +1 -0
  68. package/dist/services/media.d.ts +9 -29
  69. package/dist/services/media.d.ts.map +1 -1
  70. package/dist/services/media.js +17 -57
  71. package/dist/services/media.js.map +1 -1
  72. package/dist/services/staging.d.ts +35 -167
  73. package/dist/services/staging.d.ts.map +1 -1
  74. package/dist/services/staging.js +195 -458
  75. package/dist/services/staging.js.map +1 -1
  76. package/dist/workspace/developer.d.ts +14 -0
  77. package/dist/workspace/developer.d.ts.map +1 -0
  78. package/dist/workspace/developer.js +31 -0
  79. package/dist/workspace/developer.js.map +1 -0
  80. package/dist/workspace/files.d.ts +70 -0
  81. package/dist/workspace/files.d.ts.map +1 -0
  82. package/dist/workspace/files.js +176 -0
  83. package/dist/workspace/files.js.map +1 -0
  84. package/dist/workspace/git.d.ts +45 -0
  85. package/dist/workspace/git.d.ts.map +1 -0
  86. package/dist/workspace/git.js +68 -0
  87. package/dist/workspace/git.js.map +1 -0
  88. package/dist/workspace/locks.d.ts +33 -0
  89. package/dist/workspace/locks.d.ts.map +1 -0
  90. package/dist/workspace/locks.js +76 -0
  91. package/dist/workspace/locks.js.map +1 -0
  92. package/dist/workspace/policy.d.ts +92 -0
  93. package/dist/workspace/policy.d.ts.map +1 -0
  94. package/dist/workspace/policy.js +7 -0
  95. package/dist/workspace/policy.js.map +1 -0
  96. package/dist/workspace/tree.d.ts +73 -0
  97. package/dist/workspace/tree.d.ts.map +1 -0
  98. package/dist/workspace/tree.js +229 -0
  99. package/dist/workspace/tree.js.map +1 -0
  100. package/package.json +2 -3
  101. package/dist/db/driver.d.ts +0 -35
  102. package/dist/db/driver.d.ts.map +0 -1
  103. package/dist/db/driver.js +0 -95
  104. package/dist/db/driver.js.map +0 -1
  105. package/dist/lib/git.d.ts +0 -29
  106. package/dist/lib/git.d.ts.map +0 -1
  107. package/dist/lib/git.js +0 -62
  108. package/dist/lib/git.js.map +0 -1
  109. package/dist/services/staging-local.d.ts +0 -58
  110. package/dist/services/staging-local.d.ts.map +0 -1
  111. package/dist/services/staging-local.js +0 -391
  112. package/dist/services/staging-local.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.js","sourceRoot":"","sources":["../../src/services/changes.ts"],"names":[],"mappings":"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","sourcesContent":["/** The staging API types, the before/after comparison and the analysis of a pending change set. */\n\nimport type { Author } from \"../workspace/files.js\";\nimport {\n ORDER_FILE,\n collectionEntryPath,\n entryLayout,\n entryTitle,\n isValidSlug,\n readCollection,\n readCollectionEntry,\n readSingleton,\n relativizeMedia,\n resolveSlugField,\n singletonPath,\n validateFields,\n type Collection,\n type ContentChange,\n type ContentSource,\n type FieldIssue,\n type PublishResult,\n type Site,\n} from \"@micacms/core\";\n\nimport { badRequest } from \"../lib/errors.js\";\nimport type { SyncStatus } from \"../workspace/policy.js\";\n\n/** Where the change happened, in words an editor recognises. */\nexport type ChangeOwner =\n | {\n kind: \"entry\";\n collection: string;\n slug: string;\n label: string;\n /** The slug before a pending rename. Set on the files of both slugs. */\n renamedFrom?: string;\n }\n | { kind: \"singleton\"; name: string; label: string }\n | { kind: \"order\"; collection: string; label: string }\n | { kind: \"media\"; label: string }\n | { kind: \"other\"; label: string };\n\nexport interface AnnotatedChange extends ContentChange {\n owner: ChangeOwner;\n /**\n * The file's revision, null for a deletion. Publish sends it back, so a file\n * saved after the review is not published unseen.\n */\n revision?: string | null;\n}\n\n/** One thing that would make the staged result unpublishable. */\nexport interface StagingProblem {\n /** The file the problem is about, relative to the content root. */\n path: string;\n label: string;\n issues: FieldIssue[];\n}\n\nexport interface StagingValidation {\n ok: boolean;\n problems: StagingProblem[];\n}\n\nexport interface StagingStatus {\n /** Whose working tree it is. The UI wording changes with it. */\n owner: \"developer\" | \"managed\";\n publish: { enabled: boolean; reason?: string };\n /** The commit the pending changes sit on. */\n head: string | null;\n /** The branch a publication lands on, where the workspace knows it. */\n branch: string | null;\n /** Commits created and not yet pushed. */\n ahead: number | null;\n changes: AnnotatedChange[];\n validation: StagingValidation;\n /** Where a clone that Mica keeps stands against its remote. Null for the developer's checkout. */\n sync: SyncStatus | null;\n}\n\nexport interface PublishInput {\n /** Subject line of the commit. */\n message: string;\n /**\n * The reviewed paths, with the revision each had in the review. Only these\n * are published; a file saved again since, or another file of the same entry,\n * makes Publish refuse.\n */\n paths: Record<string, string | null>;\n author?: Author;\n}\n\n/** What the System page shows about the repository. */\nexport interface RepositoryInfo {\n owner: \"developer\" | \"managed\";\n branch: string | null;\n head: string | null;\n /** The remote, for a clone that Mica keeps. */\n url: string | null;\n cloneBytes: number | null;\n sync: SyncStatus | null;\n}\n\nexport interface ContentStagingService {\n status(): Promise<StagingStatus>;\n publish(input: PublishInput): Promise<PublishResult>;\n discard(paths: string[]): Promise<{ discarded: string[] }>;\n /** Retries the push after `committed-pending-push`. */\n push(): Promise<PublishResult>;\n /**\n * A document before and after a pending change. `origin` compares against the\n * website's version, not the last commit.\n */\n compare(target: CompareTarget, against?: \"head\" | \"origin\"): Promise<Comparison>;\n /** Brings a clone that Mica keeps up to date now. Refused for the developer's checkout. */\n sync(): Promise<void>;\n resolve(group: string, choice: \"mine\" | \"website\"): Promise<void>;\n repository(): Promise<RepositoryInfo>;\n}\n\n/** What to compare on the Publish page. */\nexport type CompareTarget =\n /** `from` names the published slug of a renamed entry. */\n | { kind: \"entry\"; collection: string; slug: string; from?: string }\n | { kind: \"singleton\"; name: string }\n | { kind: \"order\"; collection: string };\n\n/**\n * Raw field data on both sides of a pending change. The editor renders the\n * comparison from the model.\n */\nexport interface Comparison {\n /** As the repository has it — null when the change creates the document. */\n before: Record<string, unknown> | null;\n /** As staged for publication — null when the change deletes it. */\n after: Record<string, unknown> | null;\n}\n\n/**\n * Reads one document from `published` and from `staged`. An order target\n * becomes `{ order: [{ slug, title }] }` on each side, in position order.\n */\nexport async function compareSources(\n model: Site,\n published: ContentSource,\n staged: ContentSource,\n target: CompareTarget,\n): Promise<Comparison> {\n if (target.kind === \"singleton\") {\n const def = model.singletons[target.name];\n if (!def) throw badRequest(`There is no singleton called \"${target.name}\".`);\n return {\n before: await readSingleton(published, def, target.name),\n after: await readSingleton(staged, def, target.name),\n };\n }\n\n const def = model.collections[target.collection];\n if (!def) throw badRequest(`There is no collection called \"${target.collection}\".`);\n\n if (target.kind === \"entry\") {\n const from = target.from ?? target.slug;\n for (const slug of [target.slug, from]) {\n if (!isValidSlug(slug)) throw badRequest(`\"${slug}\" is not a slug.`);\n }\n const before = await readCollectionEntry(published, def, target.collection, from);\n const after = await readCollectionEntry(staged, def, target.collection, target.slug);\n if (from === target.slug) return { before, after };\n\n // A rename: compare the on-disk form, where colocated media paths do not\n // name the entry's folder.\n return {\n before: before && onDisk(def, target.collection, from, before),\n after: after && onDisk(def, target.collection, target.slug, after),\n };\n }\n\n const ordering = async (source: ContentSource) => {\n const entries = await readCollection(source, def, target.collection);\n if (entries.length === 0) return null;\n return {\n order: entries.map((entry) => ({ slug: entry.slug, title: entryTitle(def, entry.data) })),\n };\n };\n\n return { before: await ordering(published), after: await ordering(staged) };\n}\n\n/**\n * Annotates a change set with owners and validates the entries it touches, in\n * one read of the affected collections. Only changed documents are validated: a\n * broken entry committed earlier must not block an unrelated publication.\n */\nexport async function analyseChanges(\n model: Site,\n source: ContentSource,\n changes: ContentChange[],\n /** The repository's side. When given, renamed entries are detected. */\n published?: ContentSource,\n): Promise<{ changes: AnnotatedChange[]; validation: StagingValidation }> {\n const owners = changes.map((change) => classifyPath(model, change.path));\n\n const collectionsToRead = new Set<string>();\n for (const owner of owners) {\n if (owner.kind === \"entry\" || owner.kind === \"order\") collectionsToRead.add(owner.collection);\n }\n\n const problems: StagingProblem[] = [];\n const titles = new Map<string, string>();\n const entriesByCollection = new Map<string, Map<string, Record<string, unknown>>>();\n\n for (const name of collectionsToRead) {\n const def = model.collections[name]!;\n try {\n const entries = await readCollection(source, def, name);\n const bySlug = new Map(entries.map((entry) => [entry.slug, entry.data]));\n entriesByCollection.set(name, bySlug);\n for (const [slug, data] of bySlug) titles.set(`${name}/${slug}`, entryTitle(def, data));\n } catch (error) {\n // An unreadable collection, possibly from an untouched file, is a problem\n // at the path it names.\n const path = (error as { path?: string }).path ?? name;\n problems.push({\n path,\n label: def.label,\n issues: [{ path: \"\", message: (error as Error).message }],\n });\n }\n }\n\n if (published) {\n await detectRenames(model, published, source, changes, owners, entriesByCollection);\n }\n\n const validated = new Set<string>();\n\n for (const owner of owners) {\n if (owner.kind === \"entry\") {\n const key = `${owner.collection}/${owner.slug}`;\n if (validated.has(key)) continue;\n validated.add(key);\n\n const def = model.collections[owner.collection]!;\n // Absent means the change set deletes this entry.\n const data = entriesByCollection.get(owner.collection)?.get(owner.slug);\n if (!data) continue;\n\n const result = validateFields(def.fields, data, \"storage\");\n if (!result.ok) {\n problems.push({\n path: entryDataPath(def, owner.collection, owner.slug),\n label: labelEntry(def, owner.slug, titles.get(key)),\n issues: result.issues,\n });\n }\n }\n\n if (owner.kind === \"singleton\" && !validated.has(`singleton:${owner.name}`)) {\n validated.add(`singleton:${owner.name}`);\n\n const def = model.singletons[owner.name]!;\n const dataPath = singletonPath(def.fields, owner.name);\n try {\n const data = await readSingleton(source, def, owner.name);\n if (data === null) continue;\n\n const result = validateFields(def.fields, data, \"storage\");\n if (!result.ok) {\n problems.push({ path: dataPath, label: def.label, issues: result.issues });\n }\n } catch (error) {\n problems.push({\n path: (error as { path?: string }).path ?? dataPath,\n label: def.label,\n issues: [{ path: \"\", message: (error as Error).message }],\n });\n }\n }\n }\n\n const annotated = changes.map((change, index): AnnotatedChange => {\n const owner = owners[index]!;\n if (owner.kind === \"entry\") {\n const def = model.collections[owner.collection]!;\n return {\n ...change,\n owner: { ...owner, label: labelEntry(def, owner.slug, titles.get(`${owner.collection}/${owner.slug}`)) },\n };\n }\n return { ...change, owner };\n });\n\n return {\n changes: annotated,\n validation: { ok: problems.length === 0, problems },\n };\n}\n\n/** Which piece of content a file belongs to, read from the model's path layout. */\nexport function classifyPath(model: Site, path: string): ChangeOwner {\n const segments = path.split(\"/\");\n const head = segments[0] ?? \"\";\n\n const collection = model.collections[head];\n if (collection && segments.length > 1) {\n if (segments.length === 2 && segments[1] === ORDER_FILE) {\n return { kind: \"order\", collection: head, label: `${collection.label}: order` };\n }\n\n if (entryLayout(collection.fields) === \"directory\") {\n const slug = segments[1]!;\n if (segments.length >= 3 && isValidSlug(slug)) {\n return { kind: \"entry\", collection: head, slug, label: slug };\n }\n } else if (segments.length === 2 && segments[1]!.endsWith(\".json\")) {\n const slug = segments[1]!.slice(0, -\".json\".length);\n if (isValidSlug(slug)) return { kind: \"entry\", collection: head, slug, label: slug };\n }\n }\n\n const singletonDir = model.singletons[head];\n if (singletonDir && segments.length > 1) {\n return { kind: \"singleton\", name: head, label: singletonDir.label };\n }\n if (head.endsWith(\".json\")) {\n const name = head.slice(0, -\".json\".length);\n const singleton = model.singletons[name];\n if (singleton && segments.length === 1) {\n return { kind: \"singleton\", name, label: singleton.label };\n }\n }\n\n if (head === \"media\") return { kind: \"media\", label: \"Shared media\" };\n\n return { kind: \"other\", label: path };\n}\n\n/** Content share that two versions of an entry need to count as one moved entry, as in Git. */\nconst RENAME_SIMILARITY = 0.5;\n\n/**\n * Marks renamed entries in place in `owners`. An entry has no identity but its\n * slug, so deleted and created entries are paired by content, like Git's rename\n * detection. An order file whose only change is the renamed slug joins the\n * entry, so a discard cannot leave the order naming a slug that does not exist.\n */\nasync function detectRenames(\n model: Site,\n published: ContentSource,\n source: ContentSource,\n changes: ContentChange[],\n owners: ChangeOwner[],\n staged: Map<string, Map<string, Record<string, unknown>>>,\n): Promise<void> {\n const kinds = new Map<string, Map<string, Set<ContentChange[\"kind\"]>>>();\n owners.forEach((owner, index) => {\n if (owner.kind !== \"entry\") return;\n const bySlug = kinds.get(owner.collection) ?? new Map<string, Set<ContentChange[\"kind\"]>>();\n kinds.set(owner.collection, bySlug);\n const set = bySlug.get(owner.slug) ?? new Set<ContentChange[\"kind\"]>();\n bySlug.set(owner.slug, set);\n set.add(changes[index]!.kind);\n });\n\n for (const [name, bySlug] of kinds) {\n const after = staged.get(name);\n if (!after) continue;\n\n const only = (kind: ContentChange[\"kind\"]) =>\n [...bySlug].filter(([, set]) => set.size === 1 && set.has(kind)).map(([slug]) => slug);\n const deleted = only(\"delete\").filter((slug) => !after.has(slug));\n const created = only(\"create\").filter((slug) => after.has(slug));\n if (deleted.length === 0 || created.length === 0) continue;\n\n const def = model.collections[name]!;\n let before: Map<string, Record<string, unknown>>;\n try {\n const entries = await readCollection(published, def, name);\n before = new Map(entries.map((entry) => [entry.slug, entry.data]));\n } catch {\n continue;\n }\n\n const candidates: Array<{ from: string; to: string; score: number }> = [];\n for (const from of deleted) {\n const old = before.get(from);\n if (!old) continue;\n for (const to of created) {\n if (before.has(to)) continue;\n const score = similarity(def, onDisk(def, name, from, old), onDisk(def, name, to, after.get(to)!));\n if (score >= RENAME_SIMILARITY) candidates.push({ from, to, score });\n }\n }\n\n const renamedTo = new Map<string, string>();\n const paired = new Set<string>();\n for (const { from, to } of candidates.sort((a, b) => b.score - a.score)) {\n if (renamedTo.has(from) || paired.has(to)) continue;\n renamedTo.set(from, to);\n paired.add(to);\n }\n\n if (renamedTo.size === 0) continue;\n\n const renamedFrom = new Map([...renamedTo].map(([from, to]) => [to, from]));\n const orderMovesOnlySlugs = await onlyRenamedInOrder(published, source, name, renamedTo);\n const [firstFrom, firstTo] = [...renamedTo][0]!;\n\n owners.forEach((owner, index) => {\n if (owner.kind === \"order\" && owner.collection === name && orderMovesOnlySlugs) {\n owners[index] = {\n kind: \"entry\",\n collection: name,\n slug: firstTo,\n label: firstTo,\n renamedFrom: firstFrom,\n };\n }\n if (owner.kind !== \"entry\" || owner.collection !== name) return;\n const to = renamedTo.get(owner.slug);\n if (to) owners[index] = { ...owner, slug: to, label: to, renamedFrom: owner.slug };\n const from = renamedFrom.get(owner.slug);\n if (from) owners[index] = { ...owner, renamedFrom: from };\n });\n }\n}\n\n/** True when a collection's staged order is the published one with the renamed slugs replaced. */\nasync function onlyRenamedInOrder(\n published: ContentSource,\n staged: ContentSource,\n name: string,\n renamedTo: Map<string, string>,\n): Promise<boolean> {\n const path = `${name}/${ORDER_FILE}`;\n const parse = (raw: string | null): unknown => {\n try {\n return raw === null ? null : JSON.parse(raw);\n } catch {\n return null;\n }\n };\n\n const before = parse(await published.read(path));\n const after = parse(await staged.read(path));\n if (!Array.isArray(before) || !Array.isArray(after)) return false;\n\n const expected = before.map((slug: unknown) =>\n typeof slug === \"string\" ? (renamedTo.get(slug) ?? slug) : slug,\n );\n return canonical(expected) === canonical(after);\n}\n\n/** The share of non-empty fields, the slug aside, that two versions of an entry agree on. */\nfunction similarity(def: Collection, a: Record<string, unknown>, b: Record<string, unknown>): number {\n const slugKey = resolveSlugField(def);\n let compared = 0;\n let equal = 0;\n for (const key of Object.keys(def.fields)) {\n if (key === slugKey || (isEmpty(a[key]) && isEmpty(b[key]))) continue;\n compared += 1;\n if (canonical(a[key]) === canonical(b[key])) equal += 1;\n }\n return compared === 0 ? 0 : equal / compared;\n}\n\n/**\n * Entry data with media paths as the files store them: relative to the entry,\n * where it has a folder.\n */\nfunction onDisk(\n def: Collection,\n collection: string,\n slug: string,\n data: Record<string, unknown>,\n): Record<string, unknown> {\n return entryLayout(def.fields) === \"directory\"\n ? relativizeMedia(def.fields, data, collectionEntryPath(def.fields, collection, slug))\n : data;\n}\n\nfunction isEmpty(value: unknown): boolean {\n return value === undefined || value === null || value === \"\" || (Array.isArray(value) && value.length === 0);\n}\n\n/** JSON with sorted keys, so two equal objects compare equal. */\nfunction canonical(value: unknown): string {\n return JSON.stringify(value ?? null, (_key, entry: unknown) => {\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) return entry;\n const record = entry as Record<string, unknown>;\n return Object.fromEntries(Object.keys(record).sort().map((key) => [key, record[key]]));\n });\n}\n\nfunction labelEntry(def: Collection, slug: string, title: string | undefined): string {\n return `${def.labelSingular ?? def.label}: ${title ?? slug}`;\n}\n\nfunction entryDataPath(def: Collection, collection: string, slug: string): string {\n const base = collectionEntryPath(def.fields, collection, slug);\n return entryLayout(def.fields) === \"directory\" ? `${base}/index.json` : base;\n}\n\nexport function describeProblems(problems: StagingProblem[]): string {\n const names = problems.map((problem) => problem.label);\n const listed = names.slice(0, 3).join(\", \") + (names.length > 3 ? \", …\" : \"\");\n return `The pending changes cannot be published until they are valid: ${listed}.`;\n}\n\n/** Field issues with the file path folded in, for the flat API envelope. */\nexport function flattenProblems(problems: StagingProblem[]): FieldIssue[] {\n return problems.flatMap((problem) =>\n problem.issues.map((issue) => ({\n path: issue.path ? `${problem.path}: ${issue.path}` : problem.path,\n message: issue.message,\n })),\n );\n}\n"]}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Collections and singletons over the working tree's files: validation, slugs,
3
+ * entry files and `_order.json`. An entry is its slug and path, Git keeps the
4
+ * history, a save is a draft, and only Publish makes a commit.
5
+ */
6
+ import { type Collection, type ContentEntry, type FieldIssue, type Singleton, type Site } from "@micacms/core";
7
+ import { FilesError, type ContentFiles } from "../workspace/files.js";
8
+ export declare class ContentError extends FilesError {
9
+ name: string;
10
+ }
11
+ export declare class ValidationError extends ContentError {
12
+ readonly issues: FieldIssue[];
13
+ name: string;
14
+ constructor(message: string, issues: FieldIssue[]);
15
+ }
16
+ export interface SaveEntryInput {
17
+ data: Record<string, unknown>;
18
+ /** The entry being replaced. Absent when creating. */
19
+ slug?: string;
20
+ /** The revision `slug` had when the editor opened it. Absent writes unconditionally. */
21
+ expectedRevision?: string | null;
22
+ }
23
+ export interface SavedEntry {
24
+ slug: string;
25
+ title: string;
26
+ /** The entry's revision after this save, for the next save. A file revision, not a commit. */
27
+ revision: string;
28
+ /** True when the save moved the entry's files. */
29
+ renamed: boolean;
30
+ }
31
+ export declare function createContentStore(site: Site, workspace: ContentFiles): {
32
+ list(collectionName: string): Promise<ContentEntry[]>;
33
+ get(collectionName: string, slug: string): Promise<ContentEntry | null>;
34
+ /** The revision an editor must send back with a save of this entry. */
35
+ revisionOf(collectionName: string, slug: string): Promise<string | null>;
36
+ /** The revision an editor must send back with a save of this singleton. */
37
+ revisionOfSingleton(name: string): Promise<string | null>;
38
+ getSingleton(name: string): Promise<Record<string, unknown>>;
39
+ save(collectionName: string, input: SaveEntryInput): Promise<SavedEntry>;
40
+ remove(collectionName: string, slug: string, options?: {
41
+ expectedRevision?: string | null;
42
+ }): Promise<void>;
43
+ saveSingleton(name: string, payload: unknown, options?: {
44
+ expectedRevision?: string | null;
45
+ }): Promise<{
46
+ revision: string;
47
+ }>;
48
+ reorder(collectionName: string, slugs: string[]): Promise<void>;
49
+ requireCollection: (name: string) => Collection;
50
+ requireSingleton: (name: string) => Singleton;
51
+ /** Slug the server would assign, so the editor's URL preview matches it. */
52
+ suggestSlug(collectionName: string, source: string, except?: string): Promise<string>;
53
+ /** Entries pointing at `slug`, so the editor can warn before a delete. */
54
+ findReferences(collectionName: string, slug: string): Promise<{
55
+ collection: string;
56
+ slug: string;
57
+ title: string;
58
+ }[]>;
59
+ };
60
+ export type ContentStore = ReturnType<typeof createContentStore>;
61
+ //# sourceMappingURL=content-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-store.d.ts","sourceRoot":"","sources":["../../src/services/content-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAoBL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAiB,UAAU,EAAE,KAAK,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAEtG,qBAAa,YAAa,SAAQ,UAAU;IACjC,IAAI,SAAkB;CAChC;AAED,qBAAa,eAAgB,SAAQ,YAAY;IAI7C,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE;IAHtB,IAAI,SAAqB;gBAEhC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,UAAU,EAAE;CAIhC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY;yBA6IvC,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;wBAIjC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAK7E,uEAAuE;+BACtC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQ9E,2EAA2E;8BAC3C,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;uBAKtC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;yBAOvC,MAAM,SAAS,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;2BA8D5D,MAAM,QAChB,MAAM,YACH;QAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAC5C,OAAO,CAAC,IAAI,CAAC;wBAuBR,MAAM,WACH,OAAO,YACP;QAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAC5C,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;4BAwBd,MAAM,SACf,MAAM,EAAE,GACd,OAAO,CAAC,IAAI,CAAC;8BAhSe,MAAM,KAAG,UAAU;6BAMpB,MAAM,KAAG,SAAS;IA8ShD,4EAA4E;gCAC1C,MAAM,UAAU,MAAM,WAAW,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK3F,0EAA0E;mCACrC,MAAM,QAAQ,MAAM;oBACpB,MAAM;cAAQ,MAAM;eAAS,MAAM;;EAuB3E;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Collections and singletons over the working tree's files: validation, slugs,
3
+ * entry files and `_order.json`. An entry is its slug and path, Git keeps the
4
+ * history, a save is a draft, and only Publish makes a commit.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { ContentPathError, DOCUMENT_EXTENSION, INDEX_FILE, ORDER_FILE, collectionEntryPath, entryFiles, entryLayout, entryTitle, isValidSlug, readCollection, readSingleton, relativizeMedia, resolveSlugField, serializeEntry, singletonPath, slugify, uniqueSlug, validateFields, withDefaults, } from "@micacms/core";
8
+ import { ConflictError, FilesError } from "../workspace/files.js";
9
+ export class ContentError extends FilesError {
10
+ name = "ContentError";
11
+ }
12
+ export class ValidationError extends ContentError {
13
+ issues;
14
+ name = "ValidationError";
15
+ constructor(message, issues) {
16
+ super(message);
17
+ this.issues = issues;
18
+ }
19
+ }
20
+ export function createContentStore(site, workspace) {
21
+ const requireCollection = (name) => {
22
+ const def = site.collections[name];
23
+ if (!def)
24
+ throw new ContentError(`Unknown collection "${name}".`);
25
+ return def;
26
+ };
27
+ const requireSingleton = (name) => {
28
+ const def = site.singletons[name];
29
+ if (!def)
30
+ throw new ContentError(`Unknown singleton "${name}".`);
31
+ return def;
32
+ };
33
+ const validate = (def, data, what) => {
34
+ const result = validateFields(def.fields, data, "storage");
35
+ if (!result.ok) {
36
+ throw new ValidationError(`${what} cannot be saved until these fields are correct.`, result.issues);
37
+ }
38
+ return result.data;
39
+ };
40
+ /**
41
+ * Deletes for `.mdoc` files not in `keep`, so a cleared rich-text field does
42
+ * not come back on the next read. Media stays: an unreferenced upload is not
43
+ * garbage.
44
+ */
45
+ const staleDocuments = async (base, keep) => {
46
+ const changes = [];
47
+ for (const item of await workspace.list(base)) {
48
+ if (item.type !== "file")
49
+ continue;
50
+ if (!item.name.endsWith(DOCUMENT_EXTENSION))
51
+ continue;
52
+ if (keep.has(item.name))
53
+ continue;
54
+ changes.push({ type: "delete", path: `${base}/${item.name}` });
55
+ }
56
+ return changes;
57
+ };
58
+ /**
59
+ * The document's revision after a write. Throws when the file is absent,
60
+ * because an empty token could never match.
61
+ */
62
+ const revisionAfterWrite = async (dataFile, folder) => {
63
+ const revision = await documentRevision(dataFile, folder);
64
+ if (revision === null) {
65
+ throw new ContentError(`Saved, but "${dataFile}" is not in the repository afterwards.`);
66
+ }
67
+ return revision;
68
+ };
69
+ /** The folder whose files make up one document, or null for a one-file document. */
70
+ const folderOf = (fields, base) => entryLayout(fields) === "directory" ? base : null;
71
+ /**
72
+ * The revision of a whole document: its data file and, in the folder layout,
73
+ * every file of its folder. A change to any one of them makes the next save a
74
+ * conflict, not an overwrite.
75
+ */
76
+ const documentRevision = async (dataFile, folder) => {
77
+ const files = await documentFiles(dataFile, folder);
78
+ if (files === null)
79
+ return null;
80
+ if (folder === null)
81
+ return files.get(dataFile);
82
+ const parts = [...files].map(([path, revision]) => `${path}\u0000${revision}`);
83
+ return createHash("sha256").update(parts.sort().join("\n")).digest("hex").slice(0, 16);
84
+ };
85
+ /** Every file of a document with its revision, or null when its data file is absent. */
86
+ const documentFiles = async (dataFile, folder) => {
87
+ const data = await workspace.revision(dataFile);
88
+ if (data === null)
89
+ return null;
90
+ const files = new Map([[dataFile, data]]);
91
+ if (folder === null)
92
+ return files;
93
+ const walk = async (dir) => {
94
+ for (const item of await workspace.list(dir)) {
95
+ const child = `${dir}/${item.name}`;
96
+ if (item.type === "directory")
97
+ await walk(child);
98
+ else
99
+ files.set(child, (await workspace.revision(child)) ?? "");
100
+ }
101
+ };
102
+ await walk(folder);
103
+ return files;
104
+ };
105
+ /**
106
+ * Refuses when the document changed since the editor read `expected`, and
107
+ * returns every file's revision for `apply`, which checks them all again
108
+ * under the write lock.
109
+ */
110
+ const expectDocument = async (expected, dataFile, folder) => {
111
+ if (expected === undefined)
112
+ return undefined;
113
+ const files = await documentFiles(dataFile, folder);
114
+ if ((await documentRevision(dataFile, folder)) !== expected) {
115
+ throw new ConflictError("This content changed after you opened it. Reload to see the current version.", [
116
+ dataFile,
117
+ ]);
118
+ }
119
+ return files === null ? { [dataFile]: null } : Object.fromEntries(files);
120
+ };
121
+ /**
122
+ * The order file with a renamed slug in its old place, so the entry does not
123
+ * drop to the end of the list.
124
+ */
125
+ const renameInOrder = async (name, from, to) => {
126
+ const path = `${name}/${ORDER_FILE}`;
127
+ const raw = await workspace.read(path);
128
+ if (raw === null)
129
+ return null;
130
+ let listed;
131
+ try {
132
+ listed = JSON.parse(raw);
133
+ }
134
+ catch {
135
+ // Reading the collection reports a malformed order file; a save need not.
136
+ return null;
137
+ }
138
+ if (!Array.isArray(listed) || !listed.includes(from))
139
+ return null;
140
+ const order = listed.map((slug) => (slug === from ? to : slug));
141
+ return { type: "write", path, contents: `${JSON.stringify(order, null, 2)}\n` };
142
+ };
143
+ /** Slugs already taken in a collection, so a new one can be made unique. */
144
+ const takenSlugs = async (def, name, except) => {
145
+ const entries = await readCollection(workspace, def, name);
146
+ return entries.map((entry) => entry.slug).filter((slug) => slug !== except);
147
+ };
148
+ return {
149
+ async list(collectionName) {
150
+ return readCollection(workspace, requireCollection(collectionName), collectionName);
151
+ },
152
+ async get(collectionName, slug) {
153
+ const entries = await this.list(collectionName);
154
+ return entries.find((entry) => entry.slug === slug) ?? null;
155
+ },
156
+ /** The revision an editor must send back with a save of this entry. */
157
+ async revisionOf(collectionName, slug) {
158
+ const def = requireCollection(collectionName);
159
+ return documentRevision(dataPath(def, collectionName, slug), folderOf(def.fields, collectionEntryPath(def.fields, collectionName, slug)));
160
+ },
161
+ /** The revision an editor must send back with a save of this singleton. */
162
+ async revisionOfSingleton(name) {
163
+ const def = requireSingleton(name);
164
+ return documentRevision(singletonDataPath(def, name), folderOf(def.fields, singletonPath(def.fields, name)));
165
+ },
166
+ async getSingleton(name) {
167
+ const def = requireSingleton(name);
168
+ const data = await readSingleton(workspace, def, name);
169
+ // A singleton with no file yet opens as an empty form, not a 404.
170
+ return withDefaults(def.fields, data);
171
+ },
172
+ async save(collectionName, input) {
173
+ const def = requireCollection(collectionName);
174
+ const data = validate(def, input.data, "This entry");
175
+ const slugKey = resolveSlugField(def);
176
+ const requested = pickSlug(def, data, input.slug);
177
+ const slug = requested === input.slug
178
+ ? requested
179
+ : uniqueSlug(requested, await takenSlugs(def, collectionName, input.slug));
180
+ data[slugKey] = slug;
181
+ const base = collectionEntryPath(def.fields, collectionName, slug);
182
+ const previousBase = input.slug && input.slug !== slug
183
+ ? collectionEntryPath(def.fields, collectionName, input.slug)
184
+ : null;
185
+ // The editor uses root-relative media paths; store them relative to the
186
+ // entry (the old directory on a rename).
187
+ const serialized = serializeEntry(def.fields, mediaForDisk(def.fields, data, previousBase ?? base));
188
+ const files = entryFiles(def.fields, base, serialized);
189
+ const changes = Object.entries(files).map(([path, contents]) => ({
190
+ type: "write",
191
+ path,
192
+ contents,
193
+ }));
194
+ if (previousBase) {
195
+ changes.push(entryLayout(def.fields) === "directory"
196
+ ? { type: "deleteTree", path: previousBase }
197
+ : { type: "delete", path: previousBase });
198
+ await moveMedia(workspace, previousBase, base, changes);
199
+ const order = await renameInOrder(collectionName, input.slug, slug);
200
+ if (order)
201
+ changes.push(order);
202
+ }
203
+ else if (entryLayout(def.fields) === "directory") {
204
+ changes.push(...(await staleDocuments(base, new Set(Object.keys(serialized.documents)))));
205
+ }
206
+ const title = entryTitle(def, data);
207
+ await workspace.apply({
208
+ changes,
209
+ expect: await expectDocument(input.expectedRevision, dataPath(def, collectionName, input.slug ?? slug), folderOf(def.fields, previousBase ?? base)),
210
+ });
211
+ const revision = await revisionAfterWrite(dataPath(def, collectionName, slug), folderOf(def.fields, base));
212
+ return { slug, title, revision, renamed: previousBase !== null };
213
+ },
214
+ async remove(collectionName, slug, options = {}) {
215
+ const def = requireCollection(collectionName);
216
+ // One path segment: an empty slug or `a/b` would delete more than one entry.
217
+ if (slug === "" || slug.includes("/") || slug === "." || slug === "..") {
218
+ throw new ContentPathError(`"${slug}" is not an entry of "${collectionName}".`);
219
+ }
220
+ const base = collectionEntryPath(def.fields, collectionName, slug);
221
+ await workspace.apply({
222
+ changes: [
223
+ entryLayout(def.fields) === "directory"
224
+ ? { type: "deleteTree", path: base }
225
+ : { type: "delete", path: base },
226
+ ],
227
+ expect: await expectDocument(options.expectedRevision, dataPath(def, collectionName, slug), folderOf(def.fields, base)),
228
+ });
229
+ },
230
+ async saveSingleton(name, payload, options = {}) {
231
+ const def = requireSingleton(name);
232
+ const data = validate(def, payload, `The "${def.label}" settings`);
233
+ const base = singletonPath(def.fields, name);
234
+ const serialized = serializeEntry(def.fields, mediaForDisk(def.fields, data, base));
235
+ const changes = Object.entries(entryFiles(def.fields, base, serialized)).map(([path, contents]) => ({ type: "write", path, contents }));
236
+ if (entryLayout(def.fields) === "directory") {
237
+ changes.push(...(await staleDocuments(base, new Set(Object.keys(serialized.documents)))));
238
+ }
239
+ await workspace.apply({
240
+ changes,
241
+ expect: await expectDocument(options.expectedRevision, singletonDataPath(def, name), folderOf(def.fields, base)),
242
+ });
243
+ return { revision: await revisionAfterWrite(singletonDataPath(def, name), folderOf(def.fields, base)) };
244
+ },
245
+ async reorder(collectionName, slugs) {
246
+ const def = requireCollection(collectionName);
247
+ if (!def.sortable) {
248
+ throw new ContentError(`The "${def.label}" collection is not manually sortable.`);
249
+ }
250
+ await workspace.apply({
251
+ changes: [
252
+ {
253
+ type: "write",
254
+ path: `${collectionName}/${ORDER_FILE}`,
255
+ contents: `${JSON.stringify(slugs, null, 2)}\n`,
256
+ },
257
+ ],
258
+ });
259
+ },
260
+ requireCollection,
261
+ requireSingleton,
262
+ /** Slug the server would assign, so the editor's URL preview matches it. */
263
+ async suggestSlug(collectionName, source, except) {
264
+ const def = requireCollection(collectionName);
265
+ return uniqueSlug(slugify(source), await takenSlugs(def, collectionName, except));
266
+ },
267
+ /** Entries pointing at `slug`, so the editor can warn before a delete. */
268
+ async findReferences(collectionName, slug) {
269
+ const results = [];
270
+ for (const [name, def] of Object.entries(site.collections)) {
271
+ const keys = Object.entries(def.fields)
272
+ .filter(([, field]) => field.kind === "relation" && field.collection === collectionName)
273
+ .map(([key]) => key);
274
+ if (keys.length === 0)
275
+ continue;
276
+ for (const entry of await readCollection(workspace, def, name)) {
277
+ const points = keys.some((key) => {
278
+ const value = entry.data[key];
279
+ return Array.isArray(value) ? value.includes(slug) : value === slug;
280
+ });
281
+ if (points) {
282
+ results.push({ collection: name, slug: entry.slug, title: entryTitle(def, entry.data) });
283
+ }
284
+ }
285
+ }
286
+ return results;
287
+ },
288
+ };
289
+ }
290
+ /**
291
+ * Media paths as the repository stores them. Only a directory entry has
292
+ * colocated media to relativize.
293
+ */
294
+ function mediaForDisk(fields, data, base) {
295
+ return entryLayout(fields) === "directory" ? relativizeMedia(fields, data, base) : data;
296
+ }
297
+ function dataPath(def, collection, slug) {
298
+ const base = collectionEntryPath(def.fields, collection, slug);
299
+ return entryLayout(def.fields) === "directory" ? `${base}/${INDEX_FILE}` : base;
300
+ }
301
+ function singletonDataPath(def, name) {
302
+ const base = singletonPath(def.fields, name);
303
+ return entryLayout(def.fields) === "directory" ? `${base}/${INDEX_FILE}` : base;
304
+ }
305
+ /** The slug from the payload's slug field, or one derived from the title. */
306
+ function pickSlug(def, data, current) {
307
+ const slugKey = resolveSlugField(def);
308
+ const fromData = data[slugKey];
309
+ const candidate = (typeof fromData === "string" && fromData.trim()) || slugify(entryTitle(def, data));
310
+ if (!candidate) {
311
+ throw new ContentError("An entry needs a title or a URL before it can be saved.");
312
+ }
313
+ const normalised = isValidSlug(candidate) ? candidate : slugify(candidate);
314
+ if (!normalised) {
315
+ throw new ContentError(`"${candidate}" cannot be turned into a URL. Use letters and numbers.`);
316
+ }
317
+ return normalised === current ? current : normalised;
318
+ }
319
+ /** Copies colocated media to the renamed entry; the caller deletes the old directory. */
320
+ async function moveMedia(files, from, to, changes) {
321
+ for (const item of await files.list(`${from}/media`)) {
322
+ if (item.type !== "file")
323
+ continue;
324
+ const bytes = await files.readBinary(`${from}/media/${item.name}`);
325
+ if (bytes === null)
326
+ continue;
327
+ changes.push({ type: "write", path: `${to}/media/${item.name}`, contents: bytes });
328
+ }
329
+ }
330
+ //# sourceMappingURL=content-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-store.js","sourceRoot":"","sources":["../../src/services/content-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Collections and singletons over the working tree's files: validation, slugs,\n * entry files and `_order.json`. An entry is its slug and path, Git keeps the\n * history, a save is a draft, and only Publish makes a commit.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport {\n ContentPathError,\n DOCUMENT_EXTENSION,\n INDEX_FILE,\n ORDER_FILE,\n collectionEntryPath,\n entryFiles,\n entryLayout,\n entryTitle,\n isValidSlug,\n readCollection,\n readSingleton,\n relativizeMedia,\n resolveSlugField,\n serializeEntry,\n singletonPath,\n slugify,\n uniqueSlug,\n validateFields,\n withDefaults,\n type Collection,\n type ContentEntry,\n type FieldIssue,\n type FieldMap,\n type Singleton,\n type Site,\n} from \"@micacms/core\";\n\nimport { ConflictError, FilesError, type ContentFiles, type FileChange } from \"../workspace/files.js\";\n\nexport class ContentError extends FilesError {\n override name = \"ContentError\";\n}\n\nexport class ValidationError extends ContentError {\n override name = \"ValidationError\";\n constructor(\n message: string,\n readonly issues: FieldIssue[],\n ) {\n super(message);\n }\n}\n\nexport interface SaveEntryInput {\n data: Record<string, unknown>;\n /** The entry being replaced. Absent when creating. */\n slug?: string;\n /** The revision `slug` had when the editor opened it. Absent writes unconditionally. */\n expectedRevision?: string | null;\n}\n\nexport interface SavedEntry {\n slug: string;\n title: string;\n /** The entry's revision after this save, for the next save. A file revision, not a commit. */\n revision: string;\n /** True when the save moved the entry's files. */\n renamed: boolean;\n}\n\nexport function createContentStore(site: Site, workspace: ContentFiles) {\n const requireCollection = (name: string): Collection => {\n const def = site.collections[name];\n if (!def) throw new ContentError(`Unknown collection \"${name}\".`);\n return def;\n };\n\n const requireSingleton = (name: string): Singleton => {\n const def = site.singletons[name];\n if (!def) throw new ContentError(`Unknown singleton \"${name}\".`);\n return def;\n };\n\n const validate = (\n def: Collection | Singleton,\n data: unknown,\n what: string,\n ): Record<string, unknown> => {\n const result = validateFields(def.fields, data, \"storage\");\n if (!result.ok) {\n throw new ValidationError(\n `${what} cannot be saved until these fields are correct.`,\n result.issues,\n );\n }\n return result.data as Record<string, unknown>;\n };\n\n /**\n * Deletes for `.mdoc` files not in `keep`, so a cleared rich-text field does\n * not come back on the next read. Media stays: an unreferenced upload is not\n * garbage.\n */\n const staleDocuments = async (base: string, keep: Set<string>): Promise<FileChange[]> => {\n const changes: FileChange[] = [];\n\n for (const item of await workspace.list(base)) {\n if (item.type !== \"file\") continue;\n if (!item.name.endsWith(DOCUMENT_EXTENSION)) continue;\n if (keep.has(item.name)) continue;\n changes.push({ type: \"delete\", path: `${base}/${item.name}` });\n }\n\n return changes;\n };\n\n /**\n * The document's revision after a write. Throws when the file is absent,\n * because an empty token could never match.\n */\n const revisionAfterWrite = async (dataFile: string, folder: string | null): Promise<string> => {\n const revision = await documentRevision(dataFile, folder);\n if (revision === null) {\n throw new ContentError(`Saved, but \"${dataFile}\" is not in the repository afterwards.`);\n }\n return revision;\n };\n\n /** The folder whose files make up one document, or null for a one-file document. */\n const folderOf = (fields: FieldMap, base: string): string | null =>\n entryLayout(fields) === \"directory\" ? base : null;\n\n /**\n * The revision of a whole document: its data file and, in the folder layout,\n * every file of its folder. A change to any one of them makes the next save a\n * conflict, not an overwrite.\n */\n const documentRevision = async (dataFile: string, folder: string | null): Promise<string | null> => {\n const files = await documentFiles(dataFile, folder);\n if (files === null) return null;\n if (folder === null) return files.get(dataFile)!;\n const parts = [...files].map(([path, revision]) => `${path}\\u0000${revision}`);\n return createHash(\"sha256\").update(parts.sort().join(\"\\n\")).digest(\"hex\").slice(0, 16);\n };\n\n /** Every file of a document with its revision, or null when its data file is absent. */\n const documentFiles = async (dataFile: string, folder: string | null): Promise<Map<string, string> | null> => {\n const data = await workspace.revision(dataFile);\n if (data === null) return null;\n const files = new Map([[dataFile, data]]);\n if (folder === null) return files;\n const walk = async (dir: string): Promise<void> => {\n for (const item of await workspace.list(dir)) {\n const child = `${dir}/${item.name}`;\n if (item.type === \"directory\") await walk(child);\n else files.set(child, (await workspace.revision(child)) ?? \"\");\n }\n };\n await walk(folder);\n return files;\n };\n\n /**\n * Refuses when the document changed since the editor read `expected`, and\n * returns every file's revision for `apply`, which checks them all again\n * under the write lock.\n */\n const expectDocument = async (\n expected: string | null | undefined,\n dataFile: string,\n folder: string | null,\n ): Promise<Record<string, string | null> | undefined> => {\n if (expected === undefined) return undefined;\n const files = await documentFiles(dataFile, folder);\n if ((await documentRevision(dataFile, folder)) !== expected) {\n throw new ConflictError(\"This content changed after you opened it. Reload to see the current version.\", [\n dataFile,\n ]);\n }\n return files === null ? { [dataFile]: null } : Object.fromEntries(files);\n };\n\n /**\n * The order file with a renamed slug in its old place, so the entry does not\n * drop to the end of the list.\n */\n const renameInOrder = async (name: string, from: string, to: string): Promise<FileChange | null> => {\n const path = `${name}/${ORDER_FILE}`;\n const raw = await workspace.read(path);\n if (raw === null) return null;\n\n let listed: unknown;\n try {\n listed = JSON.parse(raw);\n } catch {\n // Reading the collection reports a malformed order file; a save need not.\n return null;\n }\n if (!Array.isArray(listed) || !listed.includes(from)) return null;\n\n const order = listed.map((slug: unknown) => (slug === from ? to : slug));\n return { type: \"write\", path, contents: `${JSON.stringify(order, null, 2)}\\n` };\n };\n\n /** Slugs already taken in a collection, so a new one can be made unique. */\n const takenSlugs = async (def: Collection, name: string, except?: string): Promise<string[]> => {\n const entries = await readCollection(workspace, def, name);\n return entries.map((entry) => entry.slug).filter((slug) => slug !== except);\n };\n\n return {\n async list(collectionName: string): Promise<ContentEntry[]> {\n return readCollection(workspace, requireCollection(collectionName), collectionName);\n },\n\n async get(collectionName: string, slug: string): Promise<ContentEntry | null> {\n const entries = await this.list(collectionName);\n return entries.find((entry) => entry.slug === slug) ?? null;\n },\n\n /** The revision an editor must send back with a save of this entry. */\n async revisionOf(collectionName: string, slug: string): Promise<string | null> {\n const def = requireCollection(collectionName);\n return documentRevision(\n dataPath(def, collectionName, slug),\n folderOf(def.fields, collectionEntryPath(def.fields, collectionName, slug)),\n );\n },\n\n /** The revision an editor must send back with a save of this singleton. */\n async revisionOfSingleton(name: string): Promise<string | null> {\n const def = requireSingleton(name);\n return documentRevision(singletonDataPath(def, name), folderOf(def.fields, singletonPath(def.fields, name)));\n },\n\n async getSingleton(name: string): Promise<Record<string, unknown>> {\n const def = requireSingleton(name);\n const data = await readSingleton(workspace, def, name);\n // A singleton with no file yet opens as an empty form, not a 404.\n return withDefaults(def.fields, data);\n },\n\n async save(collectionName: string, input: SaveEntryInput): Promise<SavedEntry> {\n const def = requireCollection(collectionName);\n const data = validate(def, input.data, \"This entry\");\n\n const slugKey = resolveSlugField(def)!;\n const requested = pickSlug(def, data, input.slug);\n const slug =\n requested === input.slug\n ? requested\n : uniqueSlug(requested, await takenSlugs(def, collectionName, input.slug));\n\n data[slugKey] = slug;\n\n const base = collectionEntryPath(def.fields, collectionName, slug);\n const previousBase =\n input.slug && input.slug !== slug\n ? collectionEntryPath(def.fields, collectionName, input.slug)\n : null;\n\n // The editor uses root-relative media paths; store them relative to the\n // entry (the old directory on a rename).\n const serialized = serializeEntry(\n def.fields,\n mediaForDisk(def.fields, data, previousBase ?? base),\n );\n const files = entryFiles(def.fields, base, serialized);\n\n const changes: FileChange[] = Object.entries(files).map(([path, contents]) => ({\n type: \"write\",\n path,\n contents,\n }));\n\n if (previousBase) {\n changes.push(\n entryLayout(def.fields) === \"directory\"\n ? { type: \"deleteTree\", path: previousBase }\n : { type: \"delete\", path: previousBase },\n );\n await moveMedia(workspace, previousBase, base, changes);\n const order = await renameInOrder(collectionName, input.slug!, slug);\n if (order) changes.push(order);\n } else if (entryLayout(def.fields) === \"directory\") {\n changes.push(...(await staleDocuments(base, new Set(Object.keys(serialized.documents)))));\n }\n\n const title = entryTitle(def, data);\n await workspace.apply({\n changes,\n expect: await expectDocument(\n input.expectedRevision,\n dataPath(def, collectionName, input.slug ?? slug),\n folderOf(def.fields, previousBase ?? base),\n ),\n });\n\n const revision = await revisionAfterWrite(dataPath(def, collectionName, slug), folderOf(def.fields, base));\n\n return { slug, title, revision, renamed: previousBase !== null };\n },\n\n async remove(\n collectionName: string,\n slug: string,\n options: { expectedRevision?: string | null } = {},\n ): Promise<void> {\n const def = requireCollection(collectionName);\n // One path segment: an empty slug or `a/b` would delete more than one entry.\n if (slug === \"\" || slug.includes(\"/\") || slug === \".\" || slug === \"..\") {\n throw new ContentPathError(`\"${slug}\" is not an entry of \"${collectionName}\".`);\n }\n const base = collectionEntryPath(def.fields, collectionName, slug);\n\n await workspace.apply({\n changes: [\n entryLayout(def.fields) === \"directory\"\n ? { type: \"deleteTree\", path: base }\n : { type: \"delete\", path: base },\n ],\n expect: await expectDocument(\n options.expectedRevision,\n dataPath(def, collectionName, slug),\n folderOf(def.fields, base),\n ),\n });\n },\n\n async saveSingleton(\n name: string,\n payload: unknown,\n options: { expectedRevision?: string | null } = {},\n ): Promise<{ revision: string }> {\n const def = requireSingleton(name);\n const data = validate(def, payload, `The \"${def.label}\" settings`);\n\n const base = singletonPath(def.fields, name);\n const serialized = serializeEntry(def.fields, mediaForDisk(def.fields, data, base));\n\n const changes: FileChange[] = Object.entries(entryFiles(def.fields, base, serialized)).map(\n ([path, contents]) => ({ type: \"write\", path, contents }),\n );\n\n if (entryLayout(def.fields) === \"directory\") {\n changes.push(...(await staleDocuments(base, new Set(Object.keys(serialized.documents)))));\n }\n\n await workspace.apply({\n changes,\n expect: await expectDocument(options.expectedRevision, singletonDataPath(def, name), folderOf(def.fields, base)),\n });\n\n return { revision: await revisionAfterWrite(singletonDataPath(def, name), folderOf(def.fields, base)) };\n },\n\n async reorder(\n collectionName: string,\n slugs: string[],\n ): Promise<void> {\n const def = requireCollection(collectionName);\n if (!def.sortable) {\n throw new ContentError(`The \"${def.label}\" collection is not manually sortable.`);\n }\n\n await workspace.apply({\n changes: [\n {\n type: \"write\",\n path: `${collectionName}/${ORDER_FILE}`,\n contents: `${JSON.stringify(slugs, null, 2)}\\n`,\n },\n ],\n });\n },\n\n requireCollection,\n requireSingleton,\n\n /** Slug the server would assign, so the editor's URL preview matches it. */\n async suggestSlug(collectionName: string, source: string, except?: string): Promise<string> {\n const def = requireCollection(collectionName);\n return uniqueSlug(slugify(source), await takenSlugs(def, collectionName, except));\n },\n\n /** Entries pointing at `slug`, so the editor can warn before a delete. */\n async findReferences(collectionName: string, slug: string) {\n const results: Array<{ collection: string; slug: string; title: string }> = [];\n\n for (const [name, def] of Object.entries(site.collections)) {\n const keys = Object.entries(def.fields)\n .filter(([, field]) => field.kind === \"relation\" && field.collection === collectionName)\n .map(([key]) => key);\n\n if (keys.length === 0) continue;\n\n for (const entry of await readCollection(workspace, def, name)) {\n const points = keys.some((key) => {\n const value = entry.data[key];\n return Array.isArray(value) ? value.includes(slug) : value === slug;\n });\n if (points) {\n results.push({ collection: name, slug: entry.slug, title: entryTitle(def, entry.data) });\n }\n }\n }\n\n return results;\n },\n };\n}\n\nexport type ContentStore = ReturnType<typeof createContentStore>;\n\n/**\n * Media paths as the repository stores them. Only a directory entry has\n * colocated media to relativize.\n */\nfunction mediaForDisk(\n fields: FieldMap,\n data: Record<string, unknown>,\n base: string,\n): Record<string, unknown> {\n return entryLayout(fields) === \"directory\" ? relativizeMedia(fields, data, base) : data;\n}\n\nfunction dataPath(def: Collection, collection: string, slug: string): string {\n const base = collectionEntryPath(def.fields, collection, slug);\n return entryLayout(def.fields) === \"directory\" ? `${base}/${INDEX_FILE}` : base;\n}\n\nfunction singletonDataPath(def: Singleton, name: string): string {\n const base = singletonPath(def.fields, name);\n return entryLayout(def.fields) === \"directory\" ? `${base}/${INDEX_FILE}` : base;\n}\n\n/** The slug from the payload's slug field, or one derived from the title. */\nfunction pickSlug(\n def: Collection,\n data: Record<string, unknown>,\n current: string | undefined,\n): string {\n const slugKey = resolveSlugField(def)!;\n const fromData = data[slugKey];\n\n const candidate =\n (typeof fromData === \"string\" && fromData.trim()) || slugify(entryTitle(def, data));\n\n if (!candidate) {\n throw new ContentError(\"An entry needs a title or a URL before it can be saved.\");\n }\n\n const normalised = isValidSlug(candidate) ? candidate : slugify(candidate);\n if (!normalised) {\n throw new ContentError(`\"${candidate}\" cannot be turned into a URL. Use letters and numbers.`);\n }\n\n return normalised === current ? current : normalised;\n}\n\n/** Copies colocated media to the renamed entry; the caller deletes the old directory. */\nasync function moveMedia(\n files: ContentFiles,\n from: string,\n to: string,\n changes: FileChange[],\n): Promise<void> {\n for (const item of await files.list(`${from}/media`)) {\n if (item.type !== \"file\") continue;\n\n const bytes = await files.readBinary(`${from}/media/${item.name}`);\n if (bytes === null) continue;\n\n changes.push({ type: \"write\", path: `${to}/media/${item.name}`, contents: bytes });\n }\n}\n"]}
@@ -1,27 +1,17 @@
1
1
  /**
2
- * Media uploads.
3
- *
4
- * Every uploaded byte passes through here, so this is where the file is judged:
5
- * size, and the real content type read from the bytes rather than from the
6
- * browser's claim.
7
- *
8
- * An upload is a *file in the content repository*. There is no media table, no
9
- * generated key and no object store — a media value is the path to a file a
10
- * reviewer can see in the diff.
11
- *
12
- * The filename therefore matters. It is derived from what the editor uploaded,
13
- * slugified so it is safe as a path segment, and made unique inside its own
14
- * directory rather than globally.
2
+ * Media uploads. Every upload is checked here for size and for its real content
3
+ * type, read from the bytes. An upload is a file in the content repository; its
4
+ * name is slugified and unique in its own directory.
15
5
  */
16
- import type { RepositoryConnector } from "@micacms/connectors";
6
+ import type { ContentFiles } from "../workspace/files.js";
17
7
  /** Where uploads go when they are not attached to a particular entry. */
18
8
  export declare const SHARED_MEDIA_DIR = "media";
19
9
  export interface MediaServiceDeps {
20
- connector: RepositoryConnector;
10
+ files: ContentFiles;
21
11
  maxBytes: number;
22
12
  }
23
13
  export interface MediaFile {
24
- /** Path relative to the content root — the value a `media()` field holds. */
14
+ /** Path relative to the content root: the value a `media()` field holds. */
25
15
  path: string;
26
16
  filename: string;
27
17
  mimeType: string;
@@ -30,14 +20,7 @@ export interface MediaFile {
30
20
  height: number | null;
31
21
  }
32
22
  export declare function createMediaService(deps: MediaServiceDeps): {
33
- /**
34
- * Reads and checks an upload, without writing it.
35
- *
36
- * Separate from the commit so that an entry and the media it references go
37
- * into the repository in *one* change set. Uploading first and saving
38
- * second would leave an orphaned file behind whenever the editor changed
39
- * their mind.
40
- */
23
+ /** Reads and checks an upload, without writing it. */
41
24
  prepare(input: {
42
25
  file: File;
43
26
  /** Directory to place the file in, relative to the content root. */
@@ -48,11 +31,8 @@ export declare function createMediaService(deps: MediaServiceDeps): {
48
31
  contents: Uint8Array;
49
32
  }>;
50
33
  /**
51
- * Every media file in the repository, for the editor's picker.
52
- *
53
- * A walk of the content tree rather than a table scan. Content trees are
54
- * small, and the alternative — an index Mica maintains — would be a second
55
- * source of truth that could disagree with the repository.
34
+ * Every media file in the repository. A tree walk is sufficient because
35
+ * content trees are small.
56
36
  */
57
37
  list(): Promise<MediaFile[]>;
58
38
  read(mediaPath: string): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAM/D,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB;IAyBrD;;;;;;;OAOG;mBACkB;QACnB,IAAI,EAAE,IAAI,CAAC;QACX,oEAAoE;QACpE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;KACvC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC;IAyCtD;;;;;;OAMG;YACW,OAAO,CAAC,SAAS,EAAE,CAAC;oBAkCZ,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;EASzF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AA2FjE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAM1D,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB;IAkBrD,sDAAsD;mBACjC;QACnB,IAAI,EAAE,IAAI,CAAC;QACX,oEAAoE;QACpE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;KACvC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC;IAyCtD;;;OAGG;YACW,OAAO,CAAC,SAAS,EAAE,CAAC;oBAkCZ,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;EASzF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AA6EjE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}