@karibsen/eponyme 0.1.0 → 0.2.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 (84) hide show
  1. package/README.md +198 -16
  2. package/dist/eponyme.d.mts +22 -2
  3. package/dist/eponyme.mjs +5 -1
  4. package/dist/module.d.mts +3 -2
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +73 -43
  7. package/dist/runtime/components/editor/EponymeCollectionPage.vue +81 -19
  8. package/dist/runtime/components/editor/EponymeForm.vue +3 -3
  9. package/dist/runtime/components/editor/EponymeFormPage.vue +7 -3
  10. package/dist/runtime/components/editor/EponymeSidebar.vue +25 -33
  11. package/dist/runtime/components/editor/EponymeSidebarTree.d.vue.ts +2 -0
  12. package/dist/runtime/components/editor/EponymeSidebarTree.vue +3 -1
  13. package/dist/runtime/components/editor/EponymeSidebarTree.vue.d.ts +2 -0
  14. package/dist/runtime/components/fields/RichTextField.vue +80 -8
  15. package/dist/runtime/components/ui/EPLinkDialog.d.vue.ts +15 -0
  16. package/dist/runtime/components/ui/EPLinkDialog.vue +142 -0
  17. package/dist/runtime/components/ui/EPLinkDialog.vue.d.ts +15 -0
  18. package/dist/runtime/components/ui/EPLinkEditor.vue +10 -91
  19. package/dist/runtime/composables/useEponyme.d.ts +2 -0
  20. package/dist/runtime/composables/useEponyme.js +7 -5
  21. package/dist/runtime/composables/useEponymeCollection.d.ts +12 -2
  22. package/dist/runtime/composables/useEponymeCollection.js +15 -5
  23. package/dist/runtime/composables/useEponymeForm.js +4 -1
  24. package/dist/runtime/composables/useEponymeHistory.d.ts +2 -2
  25. package/dist/runtime/composables/useEponymeHistory.js +2 -1
  26. package/dist/runtime/composables/useEponymeVariables.d.ts +3 -0
  27. package/dist/runtime/composables/useEponymeVariables.js +5 -0
  28. package/dist/runtime/pages/EponymeDetailPage.vue +1 -1
  29. package/dist/runtime/pages/EponymeIndexPage.vue +173 -10
  30. package/dist/runtime/server/api/eponyme/[name].get.d.ts +5 -1
  31. package/dist/runtime/server/api/eponyme/[name].get.js +3 -1
  32. package/dist/runtime/server/api/eponyme/[name].patch.js +17 -1
  33. package/dist/runtime/server/api/eponyme-collections/[path].delete.js +7 -2
  34. package/dist/runtime/server/api/eponyme-collections/[path].get.d.ts +1 -3
  35. package/dist/runtime/server/api/eponyme-collections/[path].get.js +31 -4
  36. package/dist/runtime/server/api/eponyme-export.get.d.ts +2 -0
  37. package/dist/runtime/server/api/eponyme-export.get.js +11 -0
  38. package/dist/runtime/server/api/eponyme-forms/[path].post.js +14 -1
  39. package/dist/runtime/server/api/eponyme-history/[path].patch.d.ts +1 -1
  40. package/dist/runtime/server/api/eponyme-history/[path].patch.js +14 -2
  41. package/dist/runtime/server/api/eponyme-import.post.d.ts +2 -0
  42. package/dist/runtime/server/api/eponyme-import.post.js +31 -0
  43. package/dist/runtime/server/api/eponyme-trash/[path].delete.d.ts +4 -0
  44. package/dist/runtime/server/api/eponyme-trash/[path].delete.js +16 -0
  45. package/dist/runtime/server/api/eponyme-trash/[path].get.d.ts +2 -0
  46. package/dist/runtime/server/api/eponyme-trash/[path].get.js +11 -0
  47. package/dist/runtime/server/api/eponyme-trash/[path].patch.d.ts +4 -0
  48. package/dist/runtime/server/api/eponyme-trash/[path].patch.js +16 -0
  49. package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
  50. package/dist/runtime/server/services/eponyme-store.d.ts +91 -3
  51. package/dist/runtime/server/services/eponyme-store.js +272 -9
  52. package/dist/runtime/server/utils/eponyme-entry.d.ts +9 -0
  53. package/dist/runtime/server/utils/eponyme-entry.js +7 -0
  54. package/dist/runtime/server/utils/eponyme-hooks.d.ts +16 -0
  55. package/dist/runtime/server/utils/eponyme-hooks.js +24 -0
  56. package/dist/runtime/server/utils/eponyme-variables.d.ts +5 -0
  57. package/dist/runtime/server/utils/eponyme-variables.js +5 -0
  58. package/dist/runtime/types/eponyme-config.d.ts +7 -0
  59. package/dist/runtime/types/field.d.ts +1 -0
  60. package/dist/runtime/types/hooks.d.ts +67 -0
  61. package/dist/runtime/types/hooks.js +0 -0
  62. package/dist/runtime/types/index.d.ts +1 -0
  63. package/dist/runtime/types/index.js +1 -0
  64. package/dist/runtime/types/nuxt.d.ts +10 -0
  65. package/dist/runtime/types/variables.d.ts +17 -0
  66. package/dist/runtime/types/variables.js +0 -0
  67. package/dist/runtime/utils/build-navigation-tree.d.ts +22 -0
  68. package/dist/runtime/utils/build-navigation-tree.js +30 -0
  69. package/dist/runtime/utils/date-locale.d.ts +10 -0
  70. package/dist/runtime/utils/date-locale.js +1 -0
  71. package/dist/runtime/utils/downloadable-link.d.ts +2 -0
  72. package/dist/runtime/utils/downloadable-link.js +35 -0
  73. package/dist/runtime/utils/empty-variables.d.ts +2 -0
  74. package/dist/runtime/utils/empty-variables.js +1 -0
  75. package/dist/runtime/utils/filter-navigation-tree.d.ts +7 -0
  76. package/dist/runtime/utils/filter-navigation-tree.js +39 -0
  77. package/dist/runtime/utils/get-field-default-value.js +1 -1
  78. package/dist/runtime/utils/hydration-cache.d.ts +11 -0
  79. package/dist/runtime/utils/hydration-cache.js +3 -0
  80. package/dist/runtime/utils/validate-eponyme-data.js +1 -1
  81. package/dist/runtime/utils/variables.d.ts +18 -0
  82. package/dist/runtime/utils/variables.js +52 -0
  83. package/dist/types.d.mts +2 -2
  84. package/package.json +3 -1
@@ -1,4 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
- entries: import("../../services/eponyme-store.js").EponymeCollectionEntry<Record<string, unknown>>[];
3
- }>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../services/eponyme-store.js").EponymeCollectionPage<Record<string, unknown>>>>;
4
2
  export default _default;
@@ -1,11 +1,38 @@
1
1
  import { createError, defineEventHandler, getQuery, getRequestURL } from "h3";
2
2
  import { useEponymeService } from "../../services/eponyme-service.js";
3
3
  import { requireEponymeUser } from "../../utils/auth.js";
4
+ import { interpolateEponymeContent } from "../../utils/eponyme-variables.js";
5
+ const MAX_TAKE = 200;
6
+ function readCount(raw, min, max) {
7
+ if (raw === void 0 || raw === "") return void 0;
8
+ const value = Number(raw);
9
+ if (!Number.isFinite(value)) return void 0;
10
+ return Math.min(max, Math.max(min, Math.trunc(value)));
11
+ }
4
12
  export default defineEventHandler(async (event) => {
5
13
  const name = decodeURIComponent(getRequestURL(event).pathname.replace(/^\/api\/eponyme-collections\//, ""));
6
- const version = getQuery(event).version === "draft" ? "draft" : "published";
14
+ const query = getQuery(event);
15
+ const version = query.version === "draft" ? "draft" : "published";
7
16
  if (version === "draft") await requireEponymeUser(event);
8
- const entries = name ? await useEponymeService().listCollection(name, version) : void 0;
9
- if (!entries) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
10
- return { entries };
17
+ if (!name) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
18
+ const service = useEponymeService();
19
+ const orderBy = query.orderBy === void 0 || query.orderBy === "" ? void 0 : String(query.orderBy);
20
+ if (orderBy) {
21
+ const allowed = service.collectionSortKeys(name);
22
+ if (!allowed) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
23
+ if (!allowed.includes(orderBy)) {
24
+ throw createError({
25
+ statusCode: 400,
26
+ statusMessage: `Unknown sort key "${orderBy}". Available keys: ${allowed.join(", ")}.`
27
+ });
28
+ }
29
+ }
30
+ const page = await service.listCollection(name, version, {
31
+ take: readCount(query.take, 1, MAX_TAKE),
32
+ skip: readCount(query.skip, 0, Number.MAX_SAFE_INTEGER),
33
+ orderBy,
34
+ order: query.order === "asc" ? "asc" : query.order === "desc" ? "desc" : void 0
35
+ });
36
+ if (!page) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
37
+ return query.raw ? page : interpolateEponymeContent(page);
11
38
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../services/eponyme-store.js").EponymeExportFile>>;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { defineEventHandler, setResponseHeader } from "h3";
2
+ import { useEponymeService } from "../services/eponyme-service.js";
3
+ import { requireEponymeUser } from "../utils/auth.js";
4
+ export default defineEventHandler(async (event) => {
5
+ await requireEponymeUser(event, { roles: ["editor", "owner"] });
6
+ setResponseHeader(event, "Cache-Control", "no-store");
7
+ const file = await useEponymeService().exportContent();
8
+ const day = file.eponyme.exportedAt.slice(0, 10);
9
+ setResponseHeader(event, "Content-Disposition", `attachment; filename="eponyme-export-${day}.json"`);
10
+ return file;
11
+ });
@@ -1,6 +1,7 @@
1
1
  import { createError, defineEventHandler, getRequestHeader, readRawBody, setResponseHeader, setResponseStatus } from "h3";
2
2
  import { useEponymeFormService } from "../../services/eponyme-form-service.js";
3
3
  import { readEponymeFormRoute } from "../../utils/form-route.js";
4
+ import { callEponymeBlockingHook, callEponymeHook } from "../../utils/eponyme-hooks.js";
4
5
  export default defineEventHandler(async (event) => {
5
6
  const route = readEponymeFormRoute(event);
6
7
  const service = useEponymeFormService();
@@ -25,12 +26,24 @@ export default defineEventHandler(async (event) => {
25
26
  setResponseStatus(event, 201);
26
27
  return { submitted: true };
27
28
  }
28
- const result = await service.submit(route.name, payload);
29
+ const validated = service.validate(route.name, payload);
30
+ if (validated && "errors" in validated) {
31
+ setResponseStatus(event, 422);
32
+ return { errors: validated.errors };
33
+ }
34
+ const beforeSubmit = { form: route.name, data: validated?.data ?? {} };
35
+ await callEponymeBlockingHook("eponyme:form:beforeSubmit", beforeSubmit);
36
+ const result = await service.submit(route.name, beforeSubmit.data);
29
37
  if (!result) throw createError({ statusCode: 404, statusMessage: "Eponyme form not found." });
30
38
  if ("errors" in result) {
31
39
  setResponseStatus(event, 422);
32
40
  return { errors: result.errors };
33
41
  }
42
+ await callEponymeHook("eponyme:form:submitted", {
43
+ form: route.name,
44
+ data: result.submission.data,
45
+ id: result.submission.id
46
+ });
34
47
  setResponseStatus(event, 201);
35
48
  return { submitted: true, id: result.submission.id };
36
49
  });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../services/eponyme-store.js").EponymeResult | import("../../services/eponyme-store.js").EponymeConflict>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../services/eponyme-store.js").EponymeResult>>;
2
2
  export default _default;
@@ -1,6 +1,8 @@
1
1
  import { createError, defineEventHandler, getRequestURL } from "h3";
2
2
  import { useEponymeService } from "../../services/eponyme-service.js";
3
3
  import { assertEponymeMutationOrigin, requireEponymeUser } from "../../utils/auth.js";
4
+ import { callEponymeHook } from "../../utils/eponyme-hooks.js";
5
+ import { splitEponymeCollectionEntry } from "../../utils/eponyme-entry.js";
4
6
  export default defineEventHandler(async (event) => {
5
7
  assertEponymeMutationOrigin(event);
6
8
  const user = await requireEponymeUser(event, { roles: ["editor", "owner"] });
@@ -10,9 +12,19 @@ export default defineEventHandler(async (event) => {
10
12
  const [, name, rawVersionId] = match;
11
13
  const versionId = Number(rawVersionId);
12
14
  if (!Number.isSafeInteger(versionId)) throw createError({ statusCode: 404, statusMessage: "Eponyme version not found." });
13
- const result = await useEponymeService().restore(name, versionId, user.id);
15
+ const service = useEponymeService();
16
+ const result = await service.restore(name, versionId, user.id);
14
17
  if (!result) throw createError({ statusCode: 404, statusMessage: "Eponyme version not found." });
15
- if ("conflict" in result && result.conflict)
18
+ if ("conflict" in result)
16
19
  throw createError({ statusCode: 409, message: "This entry was changed by someone else. Reload the page to get the latest version." });
20
+ await callEponymeHook("eponyme:entry:restored", {
21
+ name,
22
+ collection: splitEponymeCollectionEntry(service, name),
23
+ action: "restore",
24
+ status: result.status,
25
+ publishedAt: result.publishedAt,
26
+ data: result.data,
27
+ userId: user.id
28
+ });
17
29
  return result;
18
30
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../services/eponyme-store.js").EponymeImportResult>>;
2
+ export default _default;
@@ -0,0 +1,31 @@
1
+ import { createError, defineEventHandler, getQuery, getRequestHeader, readRawBody, setResponseHeader } from "h3";
2
+ import { useEponymeService } from "../services/eponyme-service.js";
3
+ import { assertEponymeMutationOrigin, requireEponymeUser } from "../utils/auth.js";
4
+ const MAX_BODY_BYTES = 5 * 1024 * 1024;
5
+ export default defineEventHandler(async (event) => {
6
+ assertEponymeMutationOrigin(event);
7
+ const user = await requireEponymeUser(event, { roles: ["owner"] });
8
+ setResponseHeader(event, "Cache-Control", "no-store");
9
+ const declaredLength = Number(getRequestHeader(event, "content-length"));
10
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES)
11
+ throw createError({ statusCode: 413, statusMessage: "Import file is too large." });
12
+ const raw = await readRawBody(event, "utf8");
13
+ if (raw && Buffer.byteLength(raw, "utf8") > MAX_BODY_BYTES)
14
+ throw createError({ statusCode: 413, statusMessage: "Import file is too large." });
15
+ let payload;
16
+ try {
17
+ payload = raw ? JSON.parse(raw) : void 0;
18
+ } catch {
19
+ throw createError({ statusCode: 400, statusMessage: "Import file must be valid JSON." });
20
+ }
21
+ const dryRun = Boolean(getQuery(event).dryRun);
22
+ const result = await useEponymeService().importContent(payload, { dryRun, actorId: user.id });
23
+ if ("errors" in result) {
24
+ throw createError({
25
+ statusCode: result.schemaMismatch?.length ? 409 : 400,
26
+ statusMessage: result.errors[0] ?? "This import could not be applied.",
27
+ data: { errors: result.errors, schemaMismatch: result.schemaMismatch ?? [] }
28
+ });
29
+ }
30
+ return result;
31
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ purged: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { createError, defineEventHandler, getRequestURL } from "h3";
2
+ import { useEponymeService } from "../../services/eponyme-service.js";
3
+ import { assertEponymeMutationOrigin, requireEponymeUser } from "../../utils/auth.js";
4
+ import { callEponymeHook } from "../../utils/eponyme-hooks.js";
5
+ import { splitEponymeCollectionEntry } from "../../utils/eponyme-entry.js";
6
+ export default defineEventHandler(async (event) => {
7
+ assertEponymeMutationOrigin(event);
8
+ const user = await requireEponymeUser(event, { roles: ["owner"] });
9
+ const name = decodeURIComponent(getRequestURL(event).pathname.replace(/^\/api\/eponyme-trash\//, ""));
10
+ const service = useEponymeService();
11
+ const collection = name ? splitEponymeCollectionEntry(service, name) : void 0;
12
+ if (!collection || !await service.purgeCollectionEntry(name))
13
+ throw createError({ statusCode: 404, statusMessage: "Eponyme trashed entry not found." });
14
+ await callEponymeHook("eponyme:entry:purged", { name, collection, userId: user.id });
15
+ return { purged: true };
16
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../services/eponyme-store.js").EponymeCollectionPage<Record<string, unknown>>>>;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { createError, defineEventHandler, getRequestURL } from "h3";
2
+ import { useEponymeService } from "../../services/eponyme-service.js";
3
+ import { requireEponymeUser } from "../../utils/auth.js";
4
+ export default defineEventHandler(async (event) => {
5
+ await requireEponymeUser(event, { roles: ["editor", "owner"] });
6
+ const name = decodeURIComponent(getRequestURL(event).pathname.replace(/^\/api\/eponyme-trash\//, ""));
7
+ if (!name) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
8
+ const page = await useEponymeService().listCollectionTrash(name);
9
+ if (!page) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
10
+ return page;
11
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ restored: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { createError, defineEventHandler, getRequestURL } from "h3";
2
+ import { useEponymeService } from "../../services/eponyme-service.js";
3
+ import { assertEponymeMutationOrigin, requireEponymeUser } from "../../utils/auth.js";
4
+ import { callEponymeHook } from "../../utils/eponyme-hooks.js";
5
+ import { splitEponymeCollectionEntry } from "../../utils/eponyme-entry.js";
6
+ export default defineEventHandler(async (event) => {
7
+ assertEponymeMutationOrigin(event);
8
+ const user = await requireEponymeUser(event, { roles: ["editor", "owner"] });
9
+ const name = decodeURIComponent(getRequestURL(event).pathname.replace(/^\/api\/eponyme-trash\//, ""));
10
+ const service = useEponymeService();
11
+ const collection = name ? splitEponymeCollectionEntry(service, name) : void 0;
12
+ if (!collection || !await service.restoreCollectionEntry(name))
13
+ throw createError({ statusCode: 404, statusMessage: "Eponyme trashed entry not found." });
14
+ await callEponymeHook("eponyme:entry:untrashed", { name, collection, userId: user.id });
15
+ return { restored: true };
16
+ });
@@ -1,5 +1,6 @@
1
1
  import type { EponymeConfig, EponymeFormDefinitionBase } from '../../types/index.js';
2
2
  import { type ValidationErrors } from '../../utils/validate-eponyme-data.js';
3
+ /** Kept here rather than imported, so the store stays free of H3 imports. */
3
4
  type DateValue = Date | string;
4
5
  export interface EponymeFormSubmission {
5
6
  id: string;
@@ -16,7 +16,7 @@ export interface EponymeVersionAuthor {
16
16
  }
17
17
  export interface EponymeHistoryEntry {
18
18
  id: number;
19
- action: EponymeAction | 'restore';
19
+ action: EponymeAction | 'restore' | 'import';
20
20
  status: EponymeStatus;
21
21
  createdAt: string;
22
22
  user: EponymeVersionAuthor | null;
@@ -33,15 +33,69 @@ export interface EponymeCollectionEntry<Data extends Record<string, unknown> = R
33
33
  status: EponymeStatus;
34
34
  publishedAt: string | null;
35
35
  updatedAt: string | null;
36
+ /** Only set for entries read from the trash. */
37
+ deletedAt?: string | null;
36
38
  }
37
39
  export interface EponymeSitemapEntry {
38
40
  loc: string;
39
41
  lastmod?: string;
40
42
  }
43
+ /** One entry of an export file: the complete stored state, without the envelope. */
44
+ export interface EponymeExportEntry {
45
+ name: string;
46
+ /** Set for collection entries, so a reader can group them without re-parsing the name. */
47
+ collection?: string;
48
+ draft: Record<string, unknown>;
49
+ published: Record<string, unknown>;
50
+ status: EponymeStatus;
51
+ publishedAt: string | null;
52
+ }
53
+ export interface EponymeExportFile {
54
+ eponyme: {
55
+ format: 1;
56
+ exportedAt: string;
57
+ /** Schema fingerprint per singleton and per collection, checked before an import writes anything. */
58
+ schemas: Record<string, string>;
59
+ };
60
+ entries: EponymeExportEntry[];
61
+ }
62
+ export interface EponymeImportResult {
63
+ dryRun: boolean;
64
+ created: number;
65
+ updated: number;
66
+ skipped: Array<{
67
+ name: string;
68
+ reason: string;
69
+ }>;
70
+ }
71
+ /** The import was refused as a whole, before any write. */
72
+ export interface EponymeImportRejection {
73
+ errors: string[];
74
+ /** Names whose configured schema differs from the one the file was exported with. */
75
+ schemaMismatch?: string[];
76
+ }
77
+ export type EponymeSortDirection = 'asc' | 'desc';
78
+ export interface EponymeCollectionQuery {
79
+ take?: number;
80
+ skip?: number;
81
+ orderBy?: string;
82
+ order?: EponymeSortDirection;
83
+ }
84
+ export interface EponymeCollectionPage<Data extends Record<string, unknown> = Record<string, unknown>> {
85
+ entries: EponymeCollectionEntry<Data>[];
86
+ /** Matching entries before `take` and `skip`, so it can drive a pager. */
87
+ total: number;
88
+ }
89
+ export declare const COLLECTION_METADATA_KEYS: readonly ["updatedAt", "publishedAt", "title", "slug"];
41
90
  export type PrismaEponymeRow = {
42
91
  name: string;
43
92
  data: unknown;
44
93
  updatedAt?: Date | string;
94
+ deletedAt?: Date | string | null;
95
+ };
96
+ /** `null` keeps the live rows, `{ not: null }` the trashed ones. */
97
+ export type PrismaDeletedAtFilter = null | {
98
+ not: null;
45
99
  };
46
100
  export type PrismaEponymeVersionRow = {
47
101
  id: number;
@@ -83,9 +137,11 @@ export type PrismaEponymeClient = {
83
137
  where: {
84
138
  name: string;
85
139
  updatedAt?: Date | string;
140
+ deletedAt?: PrismaDeletedAtFilter;
86
141
  };
87
142
  data: {
88
- data: Record<string, unknown>;
143
+ data?: Record<string, unknown>;
144
+ deletedAt?: Date | null;
89
145
  };
90
146
  }): Promise<{
91
147
  count: number;
@@ -106,6 +162,7 @@ export type PrismaEponymeClient = {
106
162
  name: {
107
163
  startsWith: string;
108
164
  };
165
+ deletedAt?: PrismaDeletedAtFilter;
109
166
  };
110
167
  orderBy: Array<{
111
168
  updatedAt: 'desc';
@@ -175,20 +232,49 @@ export declare class EponymeService {
175
232
  /** Content as it was in a given version, for previewing without restoring it. */
176
233
  private getVersionResult;
177
234
  statuses(): Promise<Record<string, EponymeStatus>>;
178
- listCollection(name: string, version?: EponymeVersion): Promise<EponymeCollectionEntry[] | undefined>;
235
+ /** Metadata keys that can be sorted on, alongside every field of the collection. */
236
+ collectionSortKeys(name: string): string[] | undefined;
237
+ listCollection(name: string, version?: EponymeVersion, query?: EponymeCollectionQuery): Promise<EponymeCollectionPage | undefined>;
179
238
  /**
180
239
  * Builds the public URL list configured by `previewPaths`.
181
240
  * Collections are resolved directly from their stored rows so this performs
182
241
  * one query per collection rather than one query per entry.
183
242
  */
184
243
  getSitemapEntries(previewPaths: Record<string, string>): Promise<EponymeSitemapEntry[]>;
244
+ /**
245
+ * Every singleton and every live collection entry, as a portable file.
246
+ * Collections are read with one query each rather than one per entry.
247
+ */
248
+ exportContent(): Promise<EponymeExportFile>;
249
+ /**
250
+ * Applies an export file on top of the current content: every entry it carries is
251
+ * upserted, everything else is left alone. Nothing is ever deleted.
252
+ *
253
+ * The schema check runs first and refuses the whole file, so an import can never
254
+ * land half of its entries into a configuration that no longer matches.
255
+ */
256
+ importContent(file: unknown, options?: {
257
+ dryRun?: boolean;
258
+ actorId?: string;
259
+ }): Promise<EponymeImportResult | EponymeImportRejection>;
185
260
  createCollectionEntry(name: string, payload: unknown, actorId?: string): Promise<(EponymeResult & {
186
261
  slug: string;
187
262
  errors?: never;
188
263
  }) | {
189
264
  errors: ValidationErrors;
190
265
  } | undefined>;
266
+ /**
267
+ * A trashed entry keeps its row, so it keeps its slug: reusing one has to be
268
+ * refused with an error the editor can act on rather than the generic message.
269
+ */
270
+ private slugTakenError;
271
+ /** Moves an entry to the trash. Its history is kept, so it stays restorable. */
191
272
  deleteCollectionEntry(name: string): Promise<boolean>;
273
+ restoreCollectionEntry(name: string): Promise<boolean>;
274
+ /** Definitive: `onDelete: Cascade` takes the entry's whole history with it. */
275
+ purgeCollectionEntry(name: string): Promise<boolean>;
276
+ /** Trashed entries of a collection, most recently updated first. */
277
+ listCollectionTrash(name: string): Promise<EponymeCollectionPage | undefined>;
192
278
  patch(name: string, payload: unknown, action?: EponymeAction, actorId?: string): Promise<(EponymeResult & {
193
279
  errors?: never;
194
280
  conflict?: never;
@@ -199,4 +285,6 @@ export declare class EponymeService {
199
285
  history(name: string, limit?: number): Promise<EponymeHistoryEntry[] | undefined>;
200
286
  restore(name: string, versionId: number, actorId?: string): Promise<EponymeResult | EponymeConflict | undefined>;
201
287
  }
288
+ /** Stable hash of a schema's shape, embedded in an export and checked on import. */
289
+ export declare function schemaFingerprint(schema: EponymeSchema): string;
202
290
  export declare function isPrismaError(error: unknown, code: string): boolean;