@karibsen/eponyme 0.1.1 → 0.3.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 (77) hide show
  1. package/README.md +159 -12
  2. package/dist/eponyme.mjs +2 -1
  3. package/dist/module.d.mts +6 -0
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +55 -36
  6. package/dist/runtime/components/editor/EponymeCollectionPage.vue +80 -18
  7. package/dist/runtime/components/editor/EponymeForm.vue +2 -2
  8. package/dist/runtime/components/editor/EponymeFormPage.vue +7 -3
  9. package/dist/runtime/components/editor/EponymeSidebar.vue +24 -32
  10. package/dist/runtime/components/editor/EponymeSidebarTree.d.vue.ts +2 -0
  11. package/dist/runtime/components/editor/EponymeSidebarTree.vue +3 -1
  12. package/dist/runtime/components/editor/EponymeSidebarTree.vue.d.ts +2 -0
  13. package/dist/runtime/components/fields/RichTextField.vue +51 -10
  14. package/dist/runtime/components/ui/EPLinkDialog.d.vue.ts +15 -0
  15. package/dist/runtime/components/ui/EPLinkDialog.vue +142 -0
  16. package/dist/runtime/components/ui/EPLinkDialog.vue.d.ts +15 -0
  17. package/dist/runtime/components/ui/EPLinkEditor.vue +10 -91
  18. package/dist/runtime/composables/useEponyme.js +5 -3
  19. package/dist/runtime/composables/useEponymeCollection.js +3 -2
  20. package/dist/runtime/composables/useEponymeForm.d.ts +4 -0
  21. package/dist/runtime/composables/useEponymeForm.js +11 -1
  22. package/dist/runtime/composables/useEponymeHistory.d.ts +2 -2
  23. package/dist/runtime/composables/useEponymeHistory.js +2 -1
  24. package/dist/runtime/pages/EponymeDetailPage.vue +1 -1
  25. package/dist/runtime/pages/EponymeIndexPage.vue +173 -10
  26. package/dist/runtime/server/api/eponyme/[name].patch.js +17 -1
  27. package/dist/runtime/server/api/eponyme-collections/[path].delete.js +7 -2
  28. package/dist/runtime/server/api/eponyme-export.get.d.ts +2 -0
  29. package/dist/runtime/server/api/eponyme-export.get.js +11 -0
  30. package/dist/runtime/server/api/eponyme-forms/[path].post.js +29 -1
  31. package/dist/runtime/server/api/eponyme-history/[path].patch.d.ts +1 -1
  32. package/dist/runtime/server/api/eponyme-history/[path].patch.js +14 -2
  33. package/dist/runtime/server/api/eponyme-import.post.d.ts +2 -0
  34. package/dist/runtime/server/api/eponyme-import.post.js +31 -0
  35. package/dist/runtime/server/api/eponyme-trash/[path].delete.d.ts +4 -0
  36. package/dist/runtime/server/api/eponyme-trash/[path].delete.js +16 -0
  37. package/dist/runtime/server/api/eponyme-trash/[path].get.d.ts +2 -0
  38. package/dist/runtime/server/api/eponyme-trash/[path].get.js +11 -0
  39. package/dist/runtime/server/api/eponyme-trash/[path].patch.d.ts +4 -0
  40. package/dist/runtime/server/api/eponyme-trash/[path].patch.js +16 -0
  41. package/dist/runtime/server/services/eponyme-form-store.js +2 -1
  42. package/dist/runtime/server/services/eponyme-service.js +8 -1
  43. package/dist/runtime/server/services/eponyme-store.d.ts +118 -5
  44. package/dist/runtime/server/services/eponyme-store.js +344 -45
  45. package/dist/runtime/server/utils/eponyme-captcha.d.ts +6 -0
  46. package/dist/runtime/server/utils/eponyme-captcha.js +14 -0
  47. package/dist/runtime/server/utils/eponyme-entry.d.ts +9 -0
  48. package/dist/runtime/server/utils/eponyme-entry.js +7 -0
  49. package/dist/runtime/server/utils/eponyme-hooks.d.ts +16 -0
  50. package/dist/runtime/server/utils/eponyme-hooks.js +24 -0
  51. package/dist/runtime/types/captcha.d.ts +16 -0
  52. package/dist/runtime/types/captcha.js +0 -0
  53. package/dist/runtime/types/eponyme-config.d.ts +7 -0
  54. package/dist/runtime/types/eponyme.d.ts +3 -0
  55. package/dist/runtime/types/field.d.ts +1 -0
  56. package/dist/runtime/types/hooks.d.ts +67 -0
  57. package/dist/runtime/types/hooks.js +0 -0
  58. package/dist/runtime/types/index.d.ts +1 -0
  59. package/dist/runtime/types/index.js +1 -0
  60. package/dist/runtime/types/nuxt.d.ts +10 -0
  61. package/dist/runtime/utils/build-navigation-tree.d.ts +22 -0
  62. package/dist/runtime/utils/build-navigation-tree.js +30 -0
  63. package/dist/runtime/utils/date-locale.d.ts +10 -0
  64. package/dist/runtime/utils/date-locale.js +1 -0
  65. package/dist/runtime/utils/downloadable-link.d.ts +2 -0
  66. package/dist/runtime/utils/downloadable-link.js +35 -0
  67. package/dist/runtime/utils/filter-navigation-tree.d.ts +7 -0
  68. package/dist/runtime/utils/filter-navigation-tree.js +39 -0
  69. package/dist/runtime/utils/get-field-default-value.js +1 -1
  70. package/dist/runtime/utils/hydration-cache.d.ts +11 -0
  71. package/dist/runtime/utils/hydration-cache.js +3 -0
  72. package/dist/runtime/utils/no-captcha.d.ts +7 -0
  73. package/dist/runtime/utils/no-captcha.js +5 -0
  74. package/dist/runtime/utils/validate-eponyme-data.js +1 -1
  75. package/dist/runtime/utils/variables.d.ts +8 -1
  76. package/dist/runtime/utils/variables.js +17 -4
  77. package/package.json +3 -1
@@ -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,11 +33,47 @@ 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
+ }
41
77
  export type EponymeSortDirection = 'asc' | 'desc';
42
78
  export interface EponymeCollectionQuery {
43
79
  take?: number;
@@ -55,6 +91,11 @@ export type PrismaEponymeRow = {
55
91
  name: string;
56
92
  data: unknown;
57
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;
58
99
  };
59
100
  export type PrismaEponymeVersionRow = {
60
101
  id: number;
@@ -96,9 +137,11 @@ export type PrismaEponymeClient = {
96
137
  where: {
97
138
  name: string;
98
139
  updatedAt?: Date | string;
140
+ deletedAt?: PrismaDeletedAtFilter;
99
141
  };
100
142
  data: {
101
- data: Record<string, unknown>;
143
+ data?: Record<string, unknown>;
144
+ deletedAt?: Date | null;
102
145
  };
103
146
  }): Promise<{
104
147
  count: number;
@@ -119,6 +162,7 @@ export type PrismaEponymeClient = {
119
162
  name: {
120
163
  startsWith: string;
121
164
  };
165
+ deletedAt?: PrismaDeletedAtFilter;
122
166
  };
123
167
  orderBy: Array<{
124
168
  updatedAt: 'desc';
@@ -165,17 +209,51 @@ export declare class EponymeService {
165
209
  private readonly client;
166
210
  private readonly schemas;
167
211
  private readonly collections;
168
- constructor(config: EponymeConfig, client: PrismaEponymeClient);
212
+ /** Entries a read has already persisted the normalized shape for, so it only attempts it once. */
213
+ private readonly healedByRead;
214
+ /**
215
+ * Read-path cache of normalized rows, so a page that reads the same entry from a
216
+ * singleton, a listing and a layout pays one round trip instead of one per read.
217
+ * Writers never read from it, so the `updatedAt` they lock on always comes from the
218
+ * database and a stale key can never turn into a spurious conflict.
219
+ */
220
+ private readonly cache;
221
+ private readonly cacheMs;
222
+ constructor(config: EponymeConfig, client: PrismaEponymeClient, options?: {
223
+ cacheSeconds?: number;
224
+ });
225
+ private cached;
226
+ /**
227
+ * Drops what a write to `name` invalidates: the entry itself, and the listing of the
228
+ * collection that contains it, whose row set just changed.
229
+ */
230
+ private invalidate;
169
231
  getSchema(name: string): EponymeSchema | undefined;
170
232
  getCollection(name: string): EponymeCollectionDefinitionBase | undefined;
171
233
  private getCollectionEntry;
172
- /** Reconcile every configured eponyme at application startup. */
234
+ /** Reconcile every configured eponyme at application startup. This is the one place that heals. */
173
235
  syncAll(): Promise<void>;
174
236
  private reconcile;
175
237
  private createState;
176
238
  private normalizeState;
177
- /** Reads the stored state along with the `updatedAt` it was read at, used as the optimistic lock token. */
239
+ /**
240
+ * A singleton row is created on first read, but only when it is genuinely missing.
241
+ * An `upsert` would send a write on every read instead, and each one costs a database
242
+ * round trip on the critical path of a public page.
243
+ */
244
+ private loadSingletonRow;
245
+ /**
246
+ * Reads the stored state along with the `updatedAt` it was read at, used as the optimistic lock token.
247
+ *
248
+ * `heal` persists the normalized state when it differs from what is stored. It has to stay on for
249
+ * writers, because healing moves `updatedAt` and so decides the lock token they are about to compare.
250
+ *
251
+ * `cache` is only ever set for published content. Draft reads serve the preview panel, which has to
252
+ * show a save immediately, and they come from the dashboard rather than from a public page — so
253
+ * there is nothing to gain by caching them and a stale preview to lose.
254
+ */
178
255
  private loadRow;
256
+ private readRow;
179
257
  private loadState;
180
258
  /**
181
259
  * Writes a new state only if the row still carries the `updatedAt` we read.
@@ -183,6 +261,12 @@ export declare class EponymeService {
183
261
  * instead of silently discarding their changes.
184
262
  */
185
263
  private writeState;
264
+ /**
265
+ * The live rows of a collection, shared by the public listing and the sitemap so both
266
+ * read them once. The trash and the export deliberately query around it: one wants the
267
+ * rows this filter excludes, the other must not see a cached view of the content.
268
+ */
269
+ private liveCollectionRows;
186
270
  get(name: string, version?: EponymeVersionSelector): Promise<Record<string, unknown> | undefined>;
187
271
  getResult(name: string, version?: EponymeVersionSelector): Promise<EponymeResult | undefined>;
188
272
  /** Content as it was in a given version, for previewing without restoring it. */
@@ -197,13 +281,40 @@ export declare class EponymeService {
197
281
  * one query per collection rather than one query per entry.
198
282
  */
199
283
  getSitemapEntries(previewPaths: Record<string, string>): Promise<EponymeSitemapEntry[]>;
284
+ /**
285
+ * Every singleton and every live collection entry, as a portable file.
286
+ * Collections are read with one query each rather than one per entry.
287
+ */
288
+ exportContent(): Promise<EponymeExportFile>;
289
+ /**
290
+ * Applies an export file on top of the current content: every entry it carries is
291
+ * upserted, everything else is left alone. Nothing is ever deleted.
292
+ *
293
+ * The schema check runs first and refuses the whole file, so an import can never
294
+ * land half of its entries into a configuration that no longer matches.
295
+ */
296
+ importContent(file: unknown, options?: {
297
+ dryRun?: boolean;
298
+ actorId?: string;
299
+ }): Promise<EponymeImportResult | EponymeImportRejection>;
200
300
  createCollectionEntry(name: string, payload: unknown, actorId?: string): Promise<(EponymeResult & {
201
301
  slug: string;
202
302
  errors?: never;
203
303
  }) | {
204
304
  errors: ValidationErrors;
205
305
  } | undefined>;
306
+ /**
307
+ * A trashed entry keeps its row, so it keeps its slug: reusing one has to be
308
+ * refused with an error the editor can act on rather than the generic message.
309
+ */
310
+ private slugTakenError;
311
+ /** Moves an entry to the trash. Its history is kept, so it stays restorable. */
206
312
  deleteCollectionEntry(name: string): Promise<boolean>;
313
+ restoreCollectionEntry(name: string): Promise<boolean>;
314
+ /** Definitive: `onDelete: Cascade` takes the entry's whole history with it. */
315
+ purgeCollectionEntry(name: string): Promise<boolean>;
316
+ /** Trashed entries of a collection, most recently updated first. */
317
+ listCollectionTrash(name: string): Promise<EponymeCollectionPage | undefined>;
207
318
  patch(name: string, payload: unknown, action?: EponymeAction, actorId?: string): Promise<(EponymeResult & {
208
319
  errors?: never;
209
320
  conflict?: never;
@@ -214,4 +325,6 @@ export declare class EponymeService {
214
325
  history(name: string, limit?: number): Promise<EponymeHistoryEntry[] | undefined>;
215
326
  restore(name: string, versionId: number, actorId?: string): Promise<EponymeResult | EponymeConflict | undefined>;
216
327
  }
328
+ /** Stable hash of a schema's shape, embedded in an export and checked on import. */
329
+ export declare function schemaFingerprint(schema: EponymeSchema): string;
217
330
  export declare function isPrismaError(error: unknown, code: string): boolean;