@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
package/README.md CHANGED
@@ -15,11 +15,13 @@ Define your content in `eponyme.config.ts`. Eponyme provides defaults, validatio
15
15
  - Declarative defaults and validation
16
16
  - Conditional fields, character counters, and sortable arrays
17
17
  - Private drafts with explicit publishing
18
- - Collections for articles, pages, and other repeatable content
18
+ - Collections for articles, pages, and other repeatable content, with sorting, limiting and pagination
19
19
  - Public forms with typed schemas, server-side validation, and stored submissions
20
+ - Content variables such as `{{ currentYear }}`, resolved when the page is served
20
21
  - Persistent version history with dashboard restoration
21
22
  - Draft previews for configured public routes
22
23
  - A general sitemap metadata endpoint for configured public routes
24
+ - Content export and import between environments, guarded by a schema fingerprint
23
25
  - A generated dashboard at `/__eponyme`
24
26
  - Server sessions with `owner`, `editor`, and `viewer` roles
25
27
  - PostgreSQL and Prisma persistence using JSONB
@@ -46,7 +48,7 @@ Add it to `nuxt.config.ts`:
46
48
  export default defineNuxtConfig({
47
49
  modules: ['@karibsen/eponyme'],
48
50
  eponyme: {
49
- prismaClient: '~/server/utils/prisma',
51
+ prismaClient: '~~/server/utils/prisma',
50
52
  dashboardPath: '/__eponyme',
51
53
  previewPaths: {
52
54
  homepage: '/',
@@ -89,6 +91,7 @@ export default defineEponymeConfig({
89
91
  href: '/contact',
90
92
  type: 'internal',
91
93
  openInNewTab: false,
94
+ download: false,
92
95
  },
93
96
  }),
94
97
  published: field.boolean({
@@ -153,7 +156,10 @@ field.string({
153
156
 
154
157
  Eponyme uses the Prisma client owned by your application. It does not create the connection or run migrations.
155
158
 
156
- Export an initialized client from the path configured in `nuxt.config.ts`:
159
+ Export an initialized client from the path configured in `nuxt.config.ts`. Note the
160
+ double tilde: `~~/` points at the project root, where `server/` lives, while `~/` points
161
+ at the source directory (`app/` in Nuxt 4). A relative path such as
162
+ `./server/utils/prisma` works too.
157
163
 
158
164
  ```ts
159
165
  // server/utils/prisma.ts
@@ -168,12 +174,14 @@ Add the required models to your Prisma schema:
168
174
 
169
175
  ```prisma
170
176
  model Eponyme {
171
- name String @id
172
- data Json @db.JsonB
173
- createdAt DateTime @default(now())
174
- updatedAt DateTime @updatedAt
177
+ name String @id
178
+ data Json @db.JsonB
179
+ createdAt DateTime @default(now())
180
+ updatedAt DateTime @updatedAt
181
+ deletedAt DateTime?
175
182
  versions EponymeVersion[]
176
183
 
184
+ @@index([deletedAt])
177
185
  @@map("eponyme_entries")
178
186
  }
179
187
 
@@ -238,6 +246,9 @@ model EponymeFormSubmission {
238
246
 
239
247
  Run the migration with the workflow used by your application.
240
248
 
249
+ Upgrading from 0.1.1 requires one migration: `Eponyme.deletedAt`, which backs the trash.
250
+ Deleting a collection entry no longer destroys it.
251
+
241
252
  The playground includes the same models in `playground/prisma/schema.prisma`. Copy `playground/.env.example` to `playground/.env`, set `DATABASE_URL`, then run:
242
253
 
243
254
  ```bash
@@ -312,6 +323,58 @@ GET /api/eponyme-collections/articles
312
323
 
313
324
  It returns published entries by default. Pass `version=draft` from an authenticated request to read dashboard content.
314
325
 
326
+ ### Sorting and limiting
327
+
328
+ Collections accept `take`, `skip`, `orderBy` and `order`:
329
+
330
+ ```ts
331
+ const { entries, total } = useEponymeCollection('articles', {
332
+ orderBy: 'date',
333
+ order: 'desc',
334
+ take: 4,
335
+ })
336
+ ```
337
+
338
+ `orderBy` accepts the `updatedAt`, `publishedAt`, `title` and `slug` metadata as well
339
+ as **any field of the collection**, which is what lets articles be ordered by their own
340
+ date rather than by when they were last edited. The key is typed from the schema, so a
341
+ misspelled field fails to compile.
342
+
343
+ `total` counts every matching entry *before* `take` and `skip`, so it can drive a pager.
344
+ Sorting runs after unpublished entries are filtered out, meaning a limit never spends a
345
+ slot on an entry that would have been dropped. Entries with a missing or empty value sort
346
+ last in both directions. The default order remains `updatedAt` descending.
347
+
348
+ The same options work over HTTP, where the response is `{ entries, total }`:
349
+
350
+ ```http
351
+ GET /api/eponyme-collections/articles?orderBy=date&order=desc&take=4
352
+ ```
353
+
354
+ `take` is capped at 200, and an unknown `orderBy` answers `400` with the accepted keys
355
+ rather than returning an arbitrary order.
356
+
357
+ ### Trash
358
+
359
+ Deleting a collection entry moves it to a trash rather than destroying it. The entry
360
+ disappears from the dashboard, from the public API and from the sitemap, but its content
361
+ and its whole version history are kept, so it can be brought back.
362
+
363
+ The collection page lists the trash behind a **Trash** button and offers **Restore** and
364
+ **Delete for good**. Restoring is available to editors and owners; deleting for good is
365
+ reserved to owners, since it is the only irreversible content operation — the entry's
366
+ history goes with it.
367
+
368
+ ```http
369
+ GET /api/eponyme-trash/articles # trashed entries of a collection
370
+ PATCH /api/eponyme-trash/articles/my-article # restore
371
+ DELETE /api/eponyme-trash/articles/my-article # delete for good, owner only
372
+ ```
373
+
374
+ A trashed entry keeps its row, so it keeps its slug. Creating an entry with that slug is
375
+ refused with a message pointing at the trash rather than silently overwriting what is
376
+ waiting there. Restore the entry or delete it for good to free its slug.
377
+
315
378
  ## Entries
316
379
 
317
380
  `useEponyme()` is auto-imported and infers its data from `eponyme.config.ts`:
@@ -345,6 +408,67 @@ The dashboard uses explicit saves. `Ctrl+S` or `Cmd+S` stores a draft. Publish r
345
408
 
346
409
  The `errors` ref contains field errors returned with HTTP `422`. Eponyme creates missing singleton rows from configured defaults. It also reconciles stored JSONB data when fields are added or removed from the schema.
347
410
 
411
+ ### Export and import
412
+
413
+ The dashboard overview at `/__eponyme` carries **Export** and **Import**, so content
414
+ prepared on one environment can be moved to another instead of being retyped.
415
+
416
+ Export downloads a JSON file holding every singleton and every live collection entry
417
+ with its complete state — draft, published version, status and publication date — plus a
418
+ fingerprint of the schema each entry was written against. The trash, form submissions and
419
+ users are never part of it.
420
+
421
+ Import applies that file on top of the current content: each entry it carries overwrites
422
+ its counterpart, entries it does not mention are left untouched, and nothing is ever
423
+ deleted. Before the first write, the fingerprints are compared with the local
424
+ configuration; a single divergence refuses the whole file and names what diverged, so an
425
+ import can never land half of its entries into a schema that no longer matches. The
426
+ dashboard first runs the import as a dry run and shows what it would create, overwrite
427
+ and skip. An entry whose slug waits in the trash is skipped rather than resurrected.
428
+
429
+ Every imported entry is written to the version history, so an import stays reversible
430
+ entry by entry from the timeline.
431
+
432
+ ```http
433
+ GET /api/eponyme-export # editors and owners
434
+ POST /api/eponyme-import # owners only
435
+ POST /api/eponyme-import?dryRun=1 # report the counts without writing
436
+ ```
437
+
438
+ ## Content variables
439
+
440
+ Editors can drop `{{ currentYear }}` into any text or rich-text field. Variables are
441
+ resolved when the page is served, so a year stays current without anyone re-saving the
442
+ content.
443
+
444
+ The built-ins cover dates: `currentYear`, `nextYear`, `previousYear`, `currentMonth`,
445
+ `currentDay`, `today` and `currentDate`.
446
+
447
+ Add your own in `eponyme.variables.ts` at the project root:
448
+
449
+ ```ts
450
+ export default defineEponymeVariables({
451
+ clubName: 'AS Chelles Athlétisme',
452
+ season: {
453
+ label: 'Season',
454
+ description: 'The season spanning two calendar years.',
455
+ value: () => `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
456
+ },
457
+ })
458
+ ```
459
+
460
+ A plain value is fixed; a function is called on every read. Declaring a name that
461
+ matches a built-in overrides it. The rich text editor lists every variable behind a
462
+ toolbar button, with a preview of what it currently resolves to.
463
+
464
+ **Names only, never expressions.** `{{ currentYear }}` works, `{{ new Date().getFullYear() }}`
465
+ is left as-is. Content comes from the database and is editable from the dashboard, so
466
+ evaluating it would grant arbitrary code execution on the server. An unknown name is also
467
+ left untouched, so a typo is visible on the page instead of silently deleting text.
468
+
469
+ The dashboard reads content with `?raw=1` and therefore shows the source form, keeping
470
+ variables editable. Public reads receive the resolved values.
471
+
348
472
  ## Preview
349
473
 
350
474
  Map a configured entry to its public route:
@@ -352,7 +476,7 @@ Map a configured entry to its public route:
352
476
  ```ts
353
477
  export default defineNuxtConfig({
354
478
  eponyme: {
355
- prismaClient: '~/server/utils/prisma',
479
+ prismaClient: '~~/server/utils/prisma',
356
480
  previewPaths: {
357
481
  'homepage': '/',
358
482
  'pages/contact': '/contact',
@@ -391,7 +515,7 @@ Singleton entries use their exact `previewPaths` value. Collections use a dynami
391
515
  ```ts
392
516
  export default defineNuxtConfig({
393
517
  eponyme: {
394
- prismaClient: '~/server/utils/prisma',
518
+ prismaClient: '~~/server/utils/prisma',
395
519
  previewPaths: {
396
520
  homepage: '/',
397
521
  articles: '/articles/:slug',
@@ -544,17 +668,75 @@ Two protections apply to the public submission route:
544
668
  - a **body size limit**, 64 KB by default, configurable with `maxBodyBytes`. An oversized
545
669
  body is refused with `413` before it is parsed.
546
670
 
547
- Rate limiting, CAPTCHA support and file uploads are not implemented yet.
671
+ A captcha is not part of the module. A public form is a normal POST route, so adding
672
+ Turnstile, hCaptcha or anything else is done in the host application: render the widget
673
+ on the page, send its token, and verify it in your own handler before calling
674
+ `validateEponymeForm()`.
675
+
676
+ Rate limiting and file uploads are not implemented yet.
677
+
678
+ ## Hooks
679
+
680
+ Eponyme emits Nitro hooks around every content write and form submission. Listen to
681
+ them from a server plugin:
682
+
683
+ ```ts
684
+ // server/plugins/eponyme.ts
685
+ export default defineNitroPlugin((nitroApp) => {
686
+ nitroApp.hooks.hook('eponyme:entry:published', async ({ name, collection, data }) => {
687
+ await purgeCdnCache(collection ? `/${collection.name}/${collection.slug}` : `/${name}`)
688
+ })
689
+
690
+ nitroApp.hooks.hook('eponyme:form:submitted', async ({ form, data, id }) => {
691
+ if (form === 'contact') await sendEmail(data)
692
+ })
693
+ })
694
+ ```
695
+
696
+ | Hook | When |
697
+ |---|---|
698
+ | `eponyme:entry:beforeSave` | Before a draft save or a publication |
699
+ | `eponyme:entry:saved` | After a draft save |
700
+ | `eponyme:entry:published` | After a publication |
701
+ | `eponyme:entry:restored` | After a history version was restored |
702
+ | `eponyme:entry:trashed` | After a collection entry was moved to the trash |
703
+ | `eponyme:entry:untrashed` | After a collection entry was taken back out of the trash |
704
+ | `eponyme:entry:purged` | After a collection entry and its history were deleted for good |
705
+ | `eponyme:form:beforeSubmit` | Before a managed submission is stored |
706
+ | `eponyme:form:submitted` | After a managed submission was stored |
707
+
708
+ The `before` hooks can **reject or amend** the operation. Throwing rejects it with a
709
+ `422` carrying your message, which is the way to enforce a rule the schema cannot
710
+ express. Mutating `context.data` changes what gets written:
711
+
712
+ ```ts
713
+ nitroApp.hooks.hook('eponyme:entry:beforeSave', ({ data, collection }) => {
714
+ if (collection?.name === 'articles' && !data.excerpt) throw new Error('An excerpt is required.')
715
+ data.updatedBy = 'automation'
716
+ })
717
+ ```
718
+
719
+ `eponyme:form:beforeSubmit` runs **after** validation, so a listener never sees a
720
+ payload the schema would have rejected.
721
+
722
+ The other hooks are notifications: the write already happened, so a listener that
723
+ throws is logged and swallowed. A failing webhook must not report a successful save as
724
+ an error the editor cannot act on.
725
+
726
+ Entries belonging to a collection carry `collection: { name, slug }`, so a listener can
727
+ branch without re-parsing the entry name. The three trash hooks only ever concern
728
+ collection entries, so their `collection` is always present; they carry no content, since
729
+ a trash move leaves the entry itself untouched.
548
730
 
549
731
  ## Current status
550
732
 
551
- Eponyme is at version `0.1.0`. It is ready for controlled projects and production pilots. A few workflows still need hardening before a broad public release:
733
+ Eponyme is at version `0.1.1`. It is ready for controlled projects and production pilots. A few workflows still need hardening before a broad public release:
552
734
 
553
- - Dynamic preview routes for collection entries
554
- - Reliable preview version switching and cache invalidation
555
- - Client revision tokens for long-running concurrent edits
556
- - Recoverable deletion
557
- - Public form submissions
735
+ - Client revision tokens for long-running concurrent edits: the current `updatedAt` check protects overlapping writes within one request, not two editors who loaded the same older revision. A deletion is not guarded by a revision either
736
+ - Retention controls for the trash: entries stay there until someone empties it
737
+ - Rate limiting and file uploads for public forms
738
+ - Filtering, export and retention controls for stored submissions
739
+ - Pagination or a sitemap index beyond 50,000 URLs
558
740
 
559
741
  ## Development
560
742
 
@@ -1,5 +1,6 @@
1
1
  import { EponymeConfig, EponymeSchema, EponymeCollectionOptions, CollectionSlugField, EponymeCollectionDefinition, EponymeFormSchema, EponymeFormOptions, EponymeFormDefinition } from '../dist/runtime/types/index.js';
2
- export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
2
+ export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, EponymeVariableDefinition, EponymeVariableSummary, EponymeVariableValue, EponymeVariables, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
3
+ import { EponymeVariables } from '../dist/runtime/types/variables.js';
3
4
  export { field, today } from '../dist/runtime/fields/index.js';
4
5
  export { EPONYME_ROLES, canEditEponyme, isEponymeRole } from '../dist/runtime/types/auth.js';
5
6
 
@@ -12,4 +13,23 @@ declare function collection<const T extends EponymeSchema>(options: CollectionOp
12
13
 
13
14
  declare function form<const T extends EponymeFormSchema>(options: EponymeFormOptions<T>): EponymeFormDefinition<T>;
14
15
 
15
- export { collection, defineEponymeConfig, form };
16
+ /**
17
+ * Declares the variables editors can drop into rich text with `{{ name }}`.
18
+ * Place them in `eponyme.variables.ts` at the project root:
19
+ *
20
+ * ```ts
21
+ * export default defineEponymeVariables({
22
+ * clubName: 'AS Chelles Athlétisme',
23
+ * season: {
24
+ * label: 'Season',
25
+ * value: () => `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
26
+ * },
27
+ * })
28
+ * ```
29
+ *
30
+ * A function is called on every read, so the value stays current without anyone
31
+ * re-saving the content.
32
+ */
33
+ declare function defineEponymeVariables<const T extends EponymeVariables>(variables: T): T;
34
+
35
+ export { collection, defineEponymeConfig, defineEponymeVariables, form };
package/dist/eponyme.mjs CHANGED
@@ -35,4 +35,8 @@ function form(options) {
35
35
  };
36
36
  }
37
37
 
38
- export { collection, defineEponymeConfig, form };
38
+ function defineEponymeVariables(variables) {
39
+ return variables;
40
+ }
41
+
42
+ export { collection, defineEponymeConfig, defineEponymeVariables, form };
package/dist/module.d.mts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- export { collection, defineEponymeConfig, form } from './eponyme.mjs';
2
+ export { collection, defineEponymeConfig, defineEponymeVariables, form } from './eponyme.mjs';
3
3
  export { field, today } from '../dist/runtime/fields/index.js';
4
4
  export { EPONYME_ROLES, canEditEponyme, isEponymeRole } from '../dist/runtime/types/auth.js';
5
- export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
5
+ export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, EponymeVariableDefinition, EponymeVariableSummary, EponymeVariableValue, EponymeVariables, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
6
+ import '../dist/runtime/types/variables.js';
6
7
 
7
8
  interface ModuleOptions {
8
9
  dashboardPath?: string;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@karibsen/eponyme",
3
3
  "configKey": "eponyme",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, useLogger, createResolver, installModule, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin } from '@nuxt/kit';
2
2
  import pc from 'picocolors';
3
3
  import tailwindcss from '@tailwindcss/vite';
4
4
  import { resolve } from 'node:path';
@@ -17,6 +17,46 @@ const module$1 = defineNuxtModule({
17
17
  sessionDurationDays: 7
18
18
  }
19
19
  },
20
+ moduleDependencies: {
21
+ "@nuxt/icon": {
22
+ overrides: {
23
+ serverBundle: { collections: ["mingcute"] },
24
+ clientBundle: {
25
+ icons: [
26
+ "mingcute:history-anticlockwise-line",
27
+ "mingcute:eye-2-line",
28
+ "mingcute:layout-leftbar-close-line",
29
+ "mingcute:layout-leftbar-open-line",
30
+ "mingcute:down-small-line",
31
+ "mingcute:search-line",
32
+ "mingcute:close-line",
33
+ "mingcute:arrow-right-line",
34
+ "mingcute:align-arrow-right-line",
35
+ "mingcute:link-2-line",
36
+ "mingcute:settings-3-line",
37
+ "mingcute:code-line",
38
+ "mingcute:refresh-2-line",
39
+ "mingcute:external-link-line",
40
+ "mingcute:paragraph-line",
41
+ "mingcute:heading-2-line",
42
+ "mingcute:heading-3-line",
43
+ "mingcute:bold-line",
44
+ "mingcute:italic-line",
45
+ "mingcute:strikethrough-line",
46
+ "mingcute:link-line",
47
+ "mingcute:pic-line",
48
+ "mingcute:list-check-line",
49
+ "mingcute:list-ordered-line",
50
+ "mingcute:quote-left-line",
51
+ "mingcute:back-2-line",
52
+ "mingcute:forward-2-line",
53
+ "mingcute:download-2-line",
54
+ "mingcute:upload-2-line"
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ },
20
60
  async setup(options, nuxt) {
21
61
  const logger = useLogger("eponyme");
22
62
  const resolver = createResolver(import.meta.url);
@@ -30,53 +70,26 @@ const module$1 = defineNuxtModule({
30
70
  nuxt.options.runtimeConfig.eponymeAuth = {
31
71
  sessionDurationDays: options.auth?.sessionDurationDays ?? 7
32
72
  };
33
- await installModule("@nuxt/icon", {
34
- serverBundle: { collections: ["mingcute"] },
35
- clientBundle: {
36
- // Explicit list: an icon missing here is not bundled and renders blank in production.
37
- icons: [
38
- "mingcute:history-anticlockwise-line",
39
- "mingcute:eye-2-line",
40
- "mingcute:layout-leftbar-close-line",
41
- "mingcute:layout-leftbar-open-line",
42
- "mingcute:down-small-line",
43
- "mingcute:search-line",
44
- "mingcute:close-line",
45
- "mingcute:arrow-right-line",
46
- "mingcute:align-arrow-right-line",
47
- "mingcute:link-2-line",
48
- "mingcute:settings-3-line",
49
- "mingcute:refresh-2-line",
50
- "mingcute:external-link-line",
51
- "mingcute:paragraph-line",
52
- "mingcute:heading-2-line",
53
- "mingcute:heading-3-line",
54
- "mingcute:bold-line",
55
- "mingcute:italic-line",
56
- "mingcute:strikethrough-line",
57
- "mingcute:link-line",
58
- "mingcute:pic-line",
59
- "mingcute:list-check-line",
60
- "mingcute:list-ordered-line",
61
- "mingcute:quote-left-line",
62
- "mingcute:back-2-line",
63
- "mingcute:forward-2-line"
64
- ]
65
- }
66
- }, nuxt);
67
73
  const preparing = Boolean(nuxt.options._prepare);
68
74
  if (!options.prismaClient) {
69
75
  if (!preparing) throw new Error(`${pc.red("[Eponyme]")} eponyme.prismaClient is required.`);
70
76
  logger.info("No prismaClient configured, skipping wiring (prepare).");
71
77
  return;
72
78
  }
73
- const prismaPath = await findPath(resolve(nuxt.options.rootDir, options.prismaClient));
74
- if (!prismaPath)
75
- throw new Error(`${pc.red("[Eponyme]")} Prisma client module not found: ${options.prismaClient}`);
79
+ const prismaSpecifier = options.prismaClient.replace(/^[~@]{2}\//, `${nuxt.options.rootDir}/`).replace(/^[~@]\//, `${nuxt.options.srcDir}/`);
80
+ const prismaPath = await findPath(resolve(nuxt.options.rootDir, prismaSpecifier));
81
+ if (!prismaPath) {
82
+ throw new Error(
83
+ `${pc.red("[Eponyme]")} Prisma client module not found: ${options.prismaClient}
84
+ Expected a module exporting a PrismaClient as its default export, for example '~~/server/utils/prisma'.
85
+ Note that '~/' points at ${nuxt.options.srcDir}, so a server module needs '~~/'.`
86
+ );
87
+ }
76
88
  addImports([
77
89
  { name: "defineEponymeConfig", from: resolver.resolve("./config/config") },
78
90
  { name: "collection", from: resolver.resolve("./config/collection") },
79
91
  { name: "form", from: resolver.resolve("./config/form") },
92
+ { name: "defineEponymeVariables", from: resolver.resolve("./config/variables") },
80
93
  { name: "field", from: resolver.resolve("./runtime/fields") },
81
94
  { name: "today", from: resolver.resolve("./runtime/fields") }
82
95
  ]);
@@ -96,6 +109,12 @@ const module$1 = defineNuxtModule({
96
109
  path: resolver.resolve("./runtime/middleware/eponyme-owner")
97
110
  });
98
111
  nuxt.options.build.transpile.push("reka-ui");
112
+ nuxt.options.vite.optimizeDeps ??= {};
113
+ nuxt.options.vite.optimizeDeps.include = [
114
+ ...nuxt.options.vite.optimizeDeps.include ?? [],
115
+ "slugify",
116
+ "sortablejs"
117
+ ];
99
118
  nuxt.options.css.push(resolver.resolve("./runtime/assets/dashboard.css"));
100
119
  nuxt.hook("vite:extendConfig", (viteConfig) => {
101
120
  const plugins = viteConfig.plugins;
@@ -109,23 +128,34 @@ const module$1 = defineNuxtModule({
109
128
  }
110
129
  nuxt.options.alias["#eponyme/config"] = configPath;
111
130
  nuxt.options.alias["#eponyme/prisma"] = prismaPath;
131
+ const variablesPath = await findPath(resolve(nuxt.options.rootDir, "eponyme.variables.ts"));
132
+ nuxt.options.alias["#eponyme/variables"] = variablesPath ?? resolver.resolve("./runtime/utils/empty-variables");
112
133
  addTypeTemplate({
113
134
  filename: "types/eponyme-config.d.ts",
114
135
  getContents: () => `declare module '#eponyme/config' {
115
136
  const config: typeof import(${JSON.stringify(configPath)})['default']
116
137
  export default config
117
138
  }
139
+ declare module '#eponyme/variables' {
140
+ const variables: typeof import(${JSON.stringify(nuxt.options.alias["#eponyme/variables"])})['default']
141
+ export default variables
142
+ }
118
143
  `
119
144
  });
120
145
  addServerHandler({ route: "/api/eponyme/**", handler: resolver.resolve("./runtime/server/api/eponyme/[name].get") });
121
146
  addServerHandler({ route: "/api/eponyme/**", method: "patch", handler: resolver.resolve("./runtime/server/api/eponyme/[name].patch") });
122
147
  addServerHandler({ route: "/api/eponyme-statuses", handler: resolver.resolve("./runtime/server/api/eponyme-statuses.get") });
123
148
  addServerHandler({ route: "/api/eponyme-sitemap", handler: resolver.resolve("./runtime/server/api/eponyme-sitemap.get") });
149
+ addServerHandler({ route: "/api/eponyme-export", handler: resolver.resolve("./runtime/server/api/eponyme-export.get") });
150
+ addServerHandler({ route: "/api/eponyme-import", method: "post", handler: resolver.resolve("./runtime/server/api/eponyme-import.post") });
124
151
  addServerHandler({ route: "/api/eponyme-history/**", handler: resolver.resolve("./runtime/server/api/eponyme-history/[path].get") });
125
152
  addServerHandler({ route: "/api/eponyme-history/**", method: "patch", handler: resolver.resolve("./runtime/server/api/eponyme-history/[path].patch") });
126
153
  addServerHandler({ route: "/api/eponyme-collections/**", handler: resolver.resolve("./runtime/server/api/eponyme-collections/[path].get") });
127
154
  addServerHandler({ route: "/api/eponyme-collections/**", method: "post", handler: resolver.resolve("./runtime/server/api/eponyme-collections/[path].post") });
128
155
  addServerHandler({ route: "/api/eponyme-collections/**", method: "delete", handler: resolver.resolve("./runtime/server/api/eponyme-collections/[path].delete") });
156
+ addServerHandler({ route: "/api/eponyme-trash/**", handler: resolver.resolve("./runtime/server/api/eponyme-trash/[path].get") });
157
+ addServerHandler({ route: "/api/eponyme-trash/**", method: "patch", handler: resolver.resolve("./runtime/server/api/eponyme-trash/[path].patch") });
158
+ addServerHandler({ route: "/api/eponyme-trash/**", method: "delete", handler: resolver.resolve("./runtime/server/api/eponyme-trash/[path].delete") });
129
159
  addServerHandler({ route: "/api/eponyme-forms/**", handler: resolver.resolve("./runtime/server/api/eponyme-forms/[path].get") });
130
160
  addServerHandler({ route: "/api/eponyme-forms/**", method: "post", handler: resolver.resolve("./runtime/server/api/eponyme-forms/[path].post") });
131
161
  addServerHandler({ route: "/api/eponyme-forms/**", method: "delete", handler: resolver.resolve("./runtime/server/api/eponyme-forms/[path].delete") });
@@ -140,11 +170,11 @@ const module$1 = defineNuxtModule({
140
170
  addServerPlugin(resolver.resolve("./runtime/server/plugins/eponyme-sync"));
141
171
  nuxt.hook("pages:extend", (pages) => {
142
172
  pages.push(
143
- { name: "eponyme-login", path: `${dashboardPath}/login`, file: resolver.resolve("./runtime/pages/EponymeLoginPage.vue") },
144
- { name: "eponyme-change-password", path: `${dashboardPath}/change-password`, file: resolver.resolve("./runtime/pages/EponymeChangePasswordPage.vue"), meta: { middleware: ["eponyme-auth"] } },
145
- { name: "eponyme-users", path: `${dashboardPath}/users`, file: resolver.resolve("./runtime/pages/EponymeUsersPage.vue"), meta: { middleware: ["eponyme-auth", "eponyme-owner"] } },
146
- { name: "eponyme-index", path: dashboardPath, file: resolver.resolve("./runtime/pages/EponymeIndexPage.vue"), meta: { middleware: ["eponyme-auth"] } },
147
- { name: "eponyme-detail", path: `${dashboardPath}/:eponyme(.*)*`, file: resolver.resolve("./runtime/pages/EponymeDetailPage.vue"), meta: { middleware: ["eponyme-auth"] } }
173
+ { name: "eponyme-login", path: `${dashboardPath}/login`, file: resolver.resolve("./runtime/pages/EponymeLoginPage.vue"), meta: { layout: false } },
174
+ { name: "eponyme-change-password", path: `${dashboardPath}/change-password`, file: resolver.resolve("./runtime/pages/EponymeChangePasswordPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } },
175
+ { name: "eponyme-users", path: `${dashboardPath}/users`, file: resolver.resolve("./runtime/pages/EponymeUsersPage.vue"), meta: { layout: false, middleware: ["eponyme-auth", "eponyme-owner"] } },
176
+ { name: "eponyme-index", path: dashboardPath, file: resolver.resolve("./runtime/pages/EponymeIndexPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } },
177
+ { name: "eponyme-detail", path: `${dashboardPath}/:eponyme(.*)*`, file: resolver.resolve("./runtime/pages/EponymeDetailPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } }
148
178
  );
149
179
  });
150
180
  logger.debug(`config loaded: ${configPath}`);