@karibsen/eponyme 0.1.0 → 0.1.1
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.
- package/README.md +72 -4
- package/dist/eponyme.d.mts +22 -2
- package/dist/eponyme.mjs +5 -1
- package/dist/module.d.mts +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -8
- package/dist/runtime/components/editor/EponymeCollectionPage.vue +1 -1
- package/dist/runtime/components/editor/EponymeForm.vue +1 -1
- package/dist/runtime/components/editor/EponymeSidebar.vue +1 -1
- package/dist/runtime/components/fields/RichTextField.vue +31 -0
- package/dist/runtime/composables/useEponyme.d.ts +2 -0
- package/dist/runtime/composables/useEponyme.js +3 -3
- package/dist/runtime/composables/useEponymeCollection.d.ts +12 -2
- package/dist/runtime/composables/useEponymeCollection.js +12 -3
- package/dist/runtime/composables/useEponymeVariables.d.ts +3 -0
- package/dist/runtime/composables/useEponymeVariables.js +5 -0
- package/dist/runtime/server/api/eponyme/[name].get.d.ts +5 -1
- package/dist/runtime/server/api/eponyme/[name].get.js +3 -1
- package/dist/runtime/server/api/eponyme-collections/[path].get.d.ts +1 -3
- package/dist/runtime/server/api/eponyme-collections/[path].get.js +31 -4
- package/dist/runtime/server/services/eponyme-store.d.ts +16 -1
- package/dist/runtime/server/services/eponyme-store.js +40 -2
- package/dist/runtime/server/utils/eponyme-variables.d.ts +5 -0
- package/dist/runtime/server/utils/eponyme-variables.js +5 -0
- package/dist/runtime/types/eponyme-config.d.ts +7 -0
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/index.js +1 -0
- package/dist/runtime/types/variables.d.ts +17 -0
- package/dist/runtime/types/variables.js +0 -0
- package/dist/runtime/utils/empty-variables.d.ts +2 -0
- package/dist/runtime/utils/empty-variables.js +1 -0
- package/dist/runtime/utils/variables.d.ts +18 -0
- package/dist/runtime/utils/variables.js +52 -0
- package/dist/types.d.mts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Add it to `nuxt.config.ts`:
|
|
|
46
46
|
export default defineNuxtConfig({
|
|
47
47
|
modules: ['@karibsen/eponyme'],
|
|
48
48
|
eponyme: {
|
|
49
|
-
prismaClient: '
|
|
49
|
+
prismaClient: '~~/server/utils/prisma',
|
|
50
50
|
dashboardPath: '/__eponyme',
|
|
51
51
|
previewPaths: {
|
|
52
52
|
homepage: '/',
|
|
@@ -153,7 +153,10 @@ field.string({
|
|
|
153
153
|
|
|
154
154
|
Eponyme uses the Prisma client owned by your application. It does not create the connection or run migrations.
|
|
155
155
|
|
|
156
|
-
Export an initialized client from the path configured in `nuxt.config.ts
|
|
156
|
+
Export an initialized client from the path configured in `nuxt.config.ts`. Note the
|
|
157
|
+
double tilde: `~~/` points at the project root, where `server/` lives, while `~/` points
|
|
158
|
+
at the source directory (`app/` in Nuxt 4). A relative path such as
|
|
159
|
+
`./server/utils/prisma` works too.
|
|
157
160
|
|
|
158
161
|
```ts
|
|
159
162
|
// server/utils/prisma.ts
|
|
@@ -312,6 +315,37 @@ GET /api/eponyme-collections/articles
|
|
|
312
315
|
|
|
313
316
|
It returns published entries by default. Pass `version=draft` from an authenticated request to read dashboard content.
|
|
314
317
|
|
|
318
|
+
### Sorting and limiting
|
|
319
|
+
|
|
320
|
+
Collections accept `take`, `skip`, `orderBy` and `order`:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const { entries, total } = useEponymeCollection('articles', {
|
|
324
|
+
orderBy: 'date',
|
|
325
|
+
order: 'desc',
|
|
326
|
+
take: 4,
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`orderBy` accepts the `updatedAt`, `publishedAt`, `title` and `slug` metadata as well
|
|
331
|
+
as **any field of the collection**, which is what lets articles be ordered by their own
|
|
332
|
+
date rather than by when they were last edited. The key is typed from the schema, so a
|
|
333
|
+
misspelled field fails to compile.
|
|
334
|
+
|
|
335
|
+
`total` counts every matching entry *before* `take` and `skip`, so it can drive a pager.
|
|
336
|
+
Sorting runs after unpublished entries are filtered out, meaning a limit never spends a
|
|
337
|
+
slot on an entry that would have been dropped. Entries with a missing or empty value sort
|
|
338
|
+
last in both directions. The default order remains `updatedAt` descending.
|
|
339
|
+
|
|
340
|
+
The same options work over HTTP, where the response is `{ entries, total }`:
|
|
341
|
+
|
|
342
|
+
```http
|
|
343
|
+
GET /api/eponyme-collections/articles?orderBy=date&order=desc&take=4
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`take` is capped at 200, and an unknown `orderBy` answers `400` with the accepted keys
|
|
347
|
+
rather than returning an arbitrary order.
|
|
348
|
+
|
|
315
349
|
## Entries
|
|
316
350
|
|
|
317
351
|
`useEponyme()` is auto-imported and infers its data from `eponyme.config.ts`:
|
|
@@ -345,6 +379,40 @@ The dashboard uses explicit saves. `Ctrl+S` or `Cmd+S` stores a draft. Publish r
|
|
|
345
379
|
|
|
346
380
|
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
381
|
|
|
382
|
+
## Content variables
|
|
383
|
+
|
|
384
|
+
Editors can drop `{{ currentYear }}` into any text or rich-text field. Variables are
|
|
385
|
+
resolved when the page is served, so a year stays current without anyone re-saving the
|
|
386
|
+
content.
|
|
387
|
+
|
|
388
|
+
The built-ins cover dates: `currentYear`, `nextYear`, `previousYear`, `currentMonth`,
|
|
389
|
+
`currentDay`, `today` and `currentDate`.
|
|
390
|
+
|
|
391
|
+
Add your own in `eponyme.variables.ts` at the project root:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
export default defineEponymeVariables({
|
|
395
|
+
clubName: 'AS Chelles Athlétisme',
|
|
396
|
+
season: {
|
|
397
|
+
label: 'Season',
|
|
398
|
+
description: 'The season spanning two calendar years.',
|
|
399
|
+
value: () => `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
A plain value is fixed; a function is called on every read. Declaring a name that
|
|
405
|
+
matches a built-in overrides it. The rich text editor lists every variable behind a
|
|
406
|
+
toolbar button, with a preview of what it currently resolves to.
|
|
407
|
+
|
|
408
|
+
**Names only, never expressions.** `{{ currentYear }}` works, `{{ new Date().getFullYear() }}`
|
|
409
|
+
is left as-is. Content comes from the database and is editable from the dashboard, so
|
|
410
|
+
evaluating it would grant arbitrary code execution on the server. An unknown name is also
|
|
411
|
+
left untouched, so a typo is visible on the page instead of silently deleting text.
|
|
412
|
+
|
|
413
|
+
The dashboard reads content with `?raw=1` and therefore shows the source form, keeping
|
|
414
|
+
variables editable. Public reads receive the resolved values.
|
|
415
|
+
|
|
348
416
|
## Preview
|
|
349
417
|
|
|
350
418
|
Map a configured entry to its public route:
|
|
@@ -352,7 +420,7 @@ Map a configured entry to its public route:
|
|
|
352
420
|
```ts
|
|
353
421
|
export default defineNuxtConfig({
|
|
354
422
|
eponyme: {
|
|
355
|
-
prismaClient: '
|
|
423
|
+
prismaClient: '~~/server/utils/prisma',
|
|
356
424
|
previewPaths: {
|
|
357
425
|
'homepage': '/',
|
|
358
426
|
'pages/contact': '/contact',
|
|
@@ -391,7 +459,7 @@ Singleton entries use their exact `previewPaths` value. Collections use a dynami
|
|
|
391
459
|
```ts
|
|
392
460
|
export default defineNuxtConfig({
|
|
393
461
|
eponyme: {
|
|
394
|
-
prismaClient: '
|
|
462
|
+
prismaClient: '~~/server/utils/prisma',
|
|
395
463
|
previewPaths: {
|
|
396
464
|
homepage: '/',
|
|
397
465
|
articles: '/articles/:slug',
|
package/dist/eponyme.d.mts
CHANGED
|
@@ -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
|
-
|
|
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
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
package/dist/module.mjs
CHANGED
|
@@ -46,6 +46,7 @@ const module$1 = defineNuxtModule({
|
|
|
46
46
|
"mingcute:align-arrow-right-line",
|
|
47
47
|
"mingcute:link-2-line",
|
|
48
48
|
"mingcute:settings-3-line",
|
|
49
|
+
"mingcute:code-line",
|
|
49
50
|
"mingcute:refresh-2-line",
|
|
50
51
|
"mingcute:external-link-line",
|
|
51
52
|
"mingcute:paragraph-line",
|
|
@@ -70,13 +71,20 @@ const module$1 = defineNuxtModule({
|
|
|
70
71
|
logger.info("No prismaClient configured, skipping wiring (prepare).");
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const prismaSpecifier = options.prismaClient.replace(/^[~@]{2}\//, `${nuxt.options.rootDir}/`).replace(/^[~@]\//, `${nuxt.options.srcDir}/`);
|
|
75
|
+
const prismaPath = await findPath(resolve(nuxt.options.rootDir, prismaSpecifier));
|
|
76
|
+
if (!prismaPath) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`${pc.red("[Eponyme]")} Prisma client module not found: ${options.prismaClient}
|
|
79
|
+
Expected a module exporting a PrismaClient as its default export, for example '~~/server/utils/prisma'.
|
|
80
|
+
Note that '~/' points at ${nuxt.options.srcDir}, so a server module needs '~~/'.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
76
83
|
addImports([
|
|
77
84
|
{ name: "defineEponymeConfig", from: resolver.resolve("./config/config") },
|
|
78
85
|
{ name: "collection", from: resolver.resolve("./config/collection") },
|
|
79
86
|
{ name: "form", from: resolver.resolve("./config/form") },
|
|
87
|
+
{ name: "defineEponymeVariables", from: resolver.resolve("./config/variables") },
|
|
80
88
|
{ name: "field", from: resolver.resolve("./runtime/fields") },
|
|
81
89
|
{ name: "today", from: resolver.resolve("./runtime/fields") }
|
|
82
90
|
]);
|
|
@@ -96,6 +104,12 @@ const module$1 = defineNuxtModule({
|
|
|
96
104
|
path: resolver.resolve("./runtime/middleware/eponyme-owner")
|
|
97
105
|
});
|
|
98
106
|
nuxt.options.build.transpile.push("reka-ui");
|
|
107
|
+
nuxt.options.vite.optimizeDeps ??= {};
|
|
108
|
+
nuxt.options.vite.optimizeDeps.include = [
|
|
109
|
+
...nuxt.options.vite.optimizeDeps.include ?? [],
|
|
110
|
+
"slugify",
|
|
111
|
+
"sortablejs"
|
|
112
|
+
];
|
|
99
113
|
nuxt.options.css.push(resolver.resolve("./runtime/assets/dashboard.css"));
|
|
100
114
|
nuxt.hook("vite:extendConfig", (viteConfig) => {
|
|
101
115
|
const plugins = viteConfig.plugins;
|
|
@@ -109,12 +123,18 @@ const module$1 = defineNuxtModule({
|
|
|
109
123
|
}
|
|
110
124
|
nuxt.options.alias["#eponyme/config"] = configPath;
|
|
111
125
|
nuxt.options.alias["#eponyme/prisma"] = prismaPath;
|
|
126
|
+
const variablesPath = await findPath(resolve(nuxt.options.rootDir, "eponyme.variables.ts"));
|
|
127
|
+
nuxt.options.alias["#eponyme/variables"] = variablesPath ?? resolver.resolve("./runtime/utils/empty-variables");
|
|
112
128
|
addTypeTemplate({
|
|
113
129
|
filename: "types/eponyme-config.d.ts",
|
|
114
130
|
getContents: () => `declare module '#eponyme/config' {
|
|
115
131
|
const config: typeof import(${JSON.stringify(configPath)})['default']
|
|
116
132
|
export default config
|
|
117
133
|
}
|
|
134
|
+
declare module '#eponyme/variables' {
|
|
135
|
+
const variables: typeof import(${JSON.stringify(nuxt.options.alias["#eponyme/variables"])})['default']
|
|
136
|
+
export default variables
|
|
137
|
+
}
|
|
118
138
|
`
|
|
119
139
|
});
|
|
120
140
|
addServerHandler({ route: "/api/eponyme/**", handler: resolver.resolve("./runtime/server/api/eponyme/[name].get") });
|
|
@@ -140,11 +160,11 @@ const module$1 = defineNuxtModule({
|
|
|
140
160
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/eponyme-sync"));
|
|
141
161
|
nuxt.hook("pages:extend", (pages) => {
|
|
142
162
|
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"] } }
|
|
163
|
+
{ name: "eponyme-login", path: `${dashboardPath}/login`, file: resolver.resolve("./runtime/pages/EponymeLoginPage.vue"), meta: { layout: false } },
|
|
164
|
+
{ name: "eponyme-change-password", path: `${dashboardPath}/change-password`, file: resolver.resolve("./runtime/pages/EponymeChangePasswordPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } },
|
|
165
|
+
{ name: "eponyme-users", path: `${dashboardPath}/users`, file: resolver.resolve("./runtime/pages/EponymeUsersPage.vue"), meta: { layout: false, middleware: ["eponyme-auth", "eponyme-owner"] } },
|
|
166
|
+
{ name: "eponyme-index", path: dashboardPath, file: resolver.resolve("./runtime/pages/EponymeIndexPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } },
|
|
167
|
+
{ name: "eponyme-detail", path: `${dashboardPath}/:eponyme(.*)*`, file: resolver.resolve("./runtime/pages/EponymeDetailPage.vue"), meta: { layout: false, middleware: ["eponyme-auth"] } }
|
|
148
168
|
);
|
|
149
169
|
});
|
|
150
170
|
logger.debug(`config loaded: ${configPath}`);
|
|
@@ -27,7 +27,7 @@ const endpoint = computed(() => `/api/eponyme-collections/${props.name}`);
|
|
|
27
27
|
const auth = useEponymeAuth();
|
|
28
28
|
const { data: response, pending, refresh } = useAsyncData(
|
|
29
29
|
`eponyme:collection:${props.name}`,
|
|
30
|
-
() => requestFetch(endpoint.value, { query: { version: "draft" } })
|
|
30
|
+
() => requestFetch(endpoint.value, { query: { version: "draft", raw: 1 } })
|
|
31
31
|
);
|
|
32
32
|
const entries = computed(() => response.value?.entries ?? []);
|
|
33
33
|
const label = computed(() => props.definition.label || humanize(props.name.split("/").at(-1) || props.name));
|
|
@@ -30,7 +30,7 @@ const previewPaths = runtimeConfig.public.eponyme?.previewPaths ?? {};
|
|
|
30
30
|
const collectionNames = Object.keys(getEponymeCollections(useEponymeConfig()));
|
|
31
31
|
const previewPath = computed(() => resolvePreviewPath(previewPaths, collectionNames, props.name));
|
|
32
32
|
const auth = useEponymeAuth();
|
|
33
|
-
const { data: eponymeData, errors: serverErrors, pending, status, publishedAt, refresh, save: persist } = useEponyme(props.name, { version: "draft" });
|
|
33
|
+
const { data: eponymeData, errors: serverErrors, pending, status, publishedAt, refresh, save: persist } = useEponyme(props.name, { version: "draft", raw: true });
|
|
34
34
|
const data = ref({});
|
|
35
35
|
const savedData = ref({});
|
|
36
36
|
const validation = useEponymeValidation(props.schema, data, savedData, { serverErrors });
|
|
@@ -39,7 +39,7 @@ onMounted(async () => {
|
|
|
39
39
|
await restoreNavigationScroll();
|
|
40
40
|
const [statusResponse, ...collectionResponses] = await Promise.all([
|
|
41
41
|
requestFetch("/api/eponyme-statuses"),
|
|
42
|
-
...Object.keys(collections).map((name) => requestFetch(`/api/eponyme-collections/${name}`, { query: { version: "draft" } }))
|
|
42
|
+
...Object.keys(collections).map((name) => requestFetch(`/api/eponyme-collections/${name}`, { query: { version: "draft", raw: 1 } }))
|
|
43
43
|
]);
|
|
44
44
|
statuses.value = { ...statusResponse.statuses };
|
|
45
45
|
collectionEntries.value = Object.fromEntries(Object.keys(collections).map((name, index) => [name, collectionResponses[index]?.entries ?? []]));
|
|
@@ -5,6 +5,8 @@ import StarterKit from "@tiptap/starter-kit";
|
|
|
5
5
|
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
|
6
6
|
import { computed, ref, watch } from "vue";
|
|
7
7
|
import EPFormField from "../ui/EPFormField.vue";
|
|
8
|
+
import EPDropdownMenu from "../ui/EPDropdownMenu.vue";
|
|
9
|
+
import { useEponymeVariables } from "../../composables/useEponymeVariables";
|
|
8
10
|
const props = defineProps({
|
|
9
11
|
id: { type: String, required: true },
|
|
10
12
|
modelValue: { type: null, required: false },
|
|
@@ -83,6 +85,14 @@ function setImage() {
|
|
|
83
85
|
const alt = window.prompt("Alt text (describe the image)", "") ?? "";
|
|
84
86
|
instance.chain().focus().setImage({ src: src.trim(), alt: alt.trim() }).run();
|
|
85
87
|
}
|
|
88
|
+
const variables = useEponymeVariables();
|
|
89
|
+
const variableItems = computed(() => variables.map((variable) => ({
|
|
90
|
+
label: variable.preview ? `${variable.label} \u2014 ${variable.preview}` : variable.label,
|
|
91
|
+
value: variable.name
|
|
92
|
+
})));
|
|
93
|
+
function insertVariable(name) {
|
|
94
|
+
editor.value?.chain().focus().insertContent(`{{ ${name} }}`).run();
|
|
95
|
+
}
|
|
86
96
|
const tools = computed(() => {
|
|
87
97
|
const instance = editor.value;
|
|
88
98
|
if (!instance) return [];
|
|
@@ -148,6 +158,27 @@ const tools = computed(() => {
|
|
|
148
158
|
/>
|
|
149
159
|
</button>
|
|
150
160
|
</template>
|
|
161
|
+
<EPDropdownMenu
|
|
162
|
+
v-if="variables.length"
|
|
163
|
+
:items="variableItems"
|
|
164
|
+
@select="insertVariable"
|
|
165
|
+
>
|
|
166
|
+
<template #trigger>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
class="rich-text-tool"
|
|
170
|
+
:disabled="disabled"
|
|
171
|
+
title="Insert a variable"
|
|
172
|
+
aria-label="Insert a variable"
|
|
173
|
+
>
|
|
174
|
+
<Icon
|
|
175
|
+
name="mingcute:code-line"
|
|
176
|
+
size="18"
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
/>
|
|
179
|
+
</button>
|
|
180
|
+
</template>
|
|
181
|
+
</EPDropdownMenu>
|
|
151
182
|
</div>
|
|
152
183
|
<EditorContent
|
|
153
184
|
v-if="editor"
|
|
@@ -6,6 +6,8 @@ import type { ValidationErrors } from '../utils/validate-eponyme-data.js';
|
|
|
6
6
|
type ConfigEponymeName = EponymeName<typeof eponymeConfig>;
|
|
7
7
|
export interface UseEponymeOptions {
|
|
8
8
|
version?: EponymeVersionSelector;
|
|
9
|
+
/** Keep `{{ variables }}` unresolved. The dashboard editor needs the source text. */
|
|
10
|
+
raw?: boolean;
|
|
9
11
|
}
|
|
10
12
|
/** Read and save a configured eponyme through the public or draft API. */
|
|
11
13
|
export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): {
|
|
@@ -9,8 +9,8 @@ export function useEponyme(name, options = {}) {
|
|
|
9
9
|
const saving = ref(false);
|
|
10
10
|
const requestFetch = useRequestFetch();
|
|
11
11
|
const { data: response, pending: loading, error, refresh: load } = useAsyncData(
|
|
12
|
-
`eponyme:${name}:${version}`,
|
|
13
|
-
() => requestFetch(`/api/eponyme/${name}`, { query: { version }, cache: "no-store" }),
|
|
12
|
+
`eponyme:${name}:${version}${options.raw ? ":raw" : ""}`,
|
|
13
|
+
() => requestFetch(`/api/eponyme/${name}`, { query: { version, raw: options.raw ? 1 : void 0 }, cache: "no-store" }),
|
|
14
14
|
{ getCachedData: () => void 0 }
|
|
15
15
|
);
|
|
16
16
|
const data = computed(() => response.value?.data);
|
|
@@ -36,7 +36,7 @@ export function useEponyme(name, options = {}) {
|
|
|
36
36
|
const slug = parts.pop();
|
|
37
37
|
const collectionName = parts.join("/");
|
|
38
38
|
if (collectionName && slug) {
|
|
39
|
-
clearNuxtData(`eponyme:collection:public:${collectionName}`);
|
|
39
|
+
clearNuxtData((key) => key.startsWith(`eponyme:collection:public:${collectionName}`));
|
|
40
40
|
clearNuxtData((key) => key.startsWith(`eponyme:collection-entry:${collectionName}:${slug}:`));
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { ComputedRef, Ref } from 'vue';
|
|
2
2
|
import type eponymeConfig from '#eponyme/config';
|
|
3
3
|
import type { EponymeCollectionDataByName, EponymeCollectionName } from '../types/index.js';
|
|
4
|
-
import type { EponymeCollectionEntry, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
|
|
4
|
+
import type { EponymeCollectionEntry, EponymeSortDirection, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
|
|
5
5
|
type ConfigCollectionName = EponymeCollectionName<typeof eponymeConfig>;
|
|
6
|
+
/** Sortable keys: the entry metadata plus every field of the collection. */
|
|
7
|
+
export type EponymeCollectionSortKey<Name extends ConfigCollectionName> = 'updatedAt' | 'publishedAt' | 'title' | 'slug' | (keyof EponymeCollectionDataByName<typeof eponymeConfig, Name> & string);
|
|
8
|
+
export interface UseEponymeCollectionOptions<Name extends ConfigCollectionName> {
|
|
9
|
+
take?: number;
|
|
10
|
+
skip?: number;
|
|
11
|
+
orderBy?: EponymeCollectionSortKey<Name>;
|
|
12
|
+
order?: EponymeSortDirection;
|
|
13
|
+
}
|
|
6
14
|
export interface UseEponymeCollectionResult<Data extends Record<string, unknown>> {
|
|
7
15
|
entries: ComputedRef<EponymeCollectionEntry<Data>[]>;
|
|
16
|
+
/** Matching entries before `take` and `skip`, for building a pager. */
|
|
17
|
+
total: ComputedRef<number>;
|
|
8
18
|
pending: Ref<boolean>;
|
|
9
19
|
error: Ref<Error | null | undefined>;
|
|
10
20
|
refresh: () => Promise<void>;
|
|
@@ -22,6 +32,6 @@ export interface UseEponymeCollectionEntryResult<Data extends Record<string, unk
|
|
|
22
32
|
error: Ref<Error | null | undefined>;
|
|
23
33
|
refresh: () => Promise<void>;
|
|
24
34
|
}
|
|
25
|
-
export declare function useEponymeCollection<const Name extends ConfigCollectionName>(name: Name): UseEponymeCollectionResult<EponymeCollectionDataByName<typeof eponymeConfig, Name>>;
|
|
35
|
+
export declare function useEponymeCollection<const Name extends ConfigCollectionName>(name: Name, options?: UseEponymeCollectionOptions<Name>): UseEponymeCollectionResult<EponymeCollectionDataByName<typeof eponymeConfig, Name>>;
|
|
26
36
|
export declare function useEponymeCollectionEntry<const Name extends ConfigCollectionName>(name: Name, slug: string, options?: UseEponymeCollectionEntryOptions): UseEponymeCollectionEntryResult<EponymeCollectionDataByName<typeof eponymeConfig, Name>>;
|
|
27
37
|
export {};
|
|
@@ -2,17 +2,26 @@ import { useAsyncData, useRequestFetch, useRoute } from "#app";
|
|
|
2
2
|
import { useEventListener } from "@vueuse/core";
|
|
3
3
|
import { computed } from "vue";
|
|
4
4
|
import { readPreviewQuery, readPreviewVersion } from "../utils/preview.js";
|
|
5
|
-
export function useEponymeCollection(name) {
|
|
5
|
+
export function useEponymeCollection(name, options = {}) {
|
|
6
6
|
const requestFetch = useRequestFetch();
|
|
7
|
+
const query = {
|
|
8
|
+
take: options.take,
|
|
9
|
+
skip: options.skip,
|
|
10
|
+
orderBy: options.orderBy,
|
|
11
|
+
order: options.order
|
|
12
|
+
};
|
|
7
13
|
const result = useAsyncData(
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
// The options belong in the key, otherwise two differently sorted calls to the
|
|
15
|
+
// same collection would share one cache entry.
|
|
16
|
+
`eponyme:collection:public:${name}:${options.take ?? ""}:${options.skip ?? ""}:${options.orderBy ?? ""}:${options.order ?? ""}`,
|
|
17
|
+
() => requestFetch(`/api/eponyme-collections/${name}`, { query, cache: "no-store" }),
|
|
10
18
|
{ getCachedData: () => void 0 }
|
|
11
19
|
);
|
|
12
20
|
const entries = computed(() => result.data.value?.entries ?? []);
|
|
13
21
|
if (import.meta.client) useEventListener(window, "focus", () => void result.refresh());
|
|
14
22
|
return {
|
|
15
23
|
entries,
|
|
24
|
+
total: computed(() => result.data.value?.total ?? 0),
|
|
16
25
|
pending: result.pending,
|
|
17
26
|
error: result.error,
|
|
18
27
|
refresh: async () => {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
|
+
data: Record<string, unknown>;
|
|
3
|
+
status: import("../../services/eponyme-store.js").EponymeStatus;
|
|
4
|
+
publishedAt: string | null;
|
|
5
|
+
} | {
|
|
2
6
|
data: Record<string, unknown>;
|
|
3
7
|
}>>;
|
|
4
8
|
export default _default;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getQuery, getRequestURL, setResponseHeader } 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";
|
|
4
5
|
function readVersion(raw) {
|
|
5
6
|
if (raw === "draft") return "draft";
|
|
6
7
|
const id = Number(raw);
|
|
@@ -16,5 +17,6 @@ export default defineEventHandler(async (event) => {
|
|
|
16
17
|
}
|
|
17
18
|
const result = name ? await useEponymeService().getResult(name, version) : void 0;
|
|
18
19
|
if (!result) throw createError({ statusCode: 404, statusMessage: "Eponyme entry not found." });
|
|
19
|
-
|
|
20
|
+
const data = getQuery(event).raw ? result.data : interpolateEponymeContent(result.data);
|
|
21
|
+
return requestedVersion ? { ...result, data } : { data };
|
|
20
22
|
});
|
|
@@ -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
|
|
14
|
+
const query = getQuery(event);
|
|
15
|
+
const version = query.version === "draft" ? "draft" : "published";
|
|
7
16
|
if (version === "draft") await requireEponymeUser(event);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
});
|
|
@@ -38,6 +38,19 @@ export interface EponymeSitemapEntry {
|
|
|
38
38
|
loc: string;
|
|
39
39
|
lastmod?: string;
|
|
40
40
|
}
|
|
41
|
+
export type EponymeSortDirection = 'asc' | 'desc';
|
|
42
|
+
export interface EponymeCollectionQuery {
|
|
43
|
+
take?: number;
|
|
44
|
+
skip?: number;
|
|
45
|
+
orderBy?: string;
|
|
46
|
+
order?: EponymeSortDirection;
|
|
47
|
+
}
|
|
48
|
+
export interface EponymeCollectionPage<Data extends Record<string, unknown> = Record<string, unknown>> {
|
|
49
|
+
entries: EponymeCollectionEntry<Data>[];
|
|
50
|
+
/** Matching entries before `take` and `skip`, so it can drive a pager. */
|
|
51
|
+
total: number;
|
|
52
|
+
}
|
|
53
|
+
export declare const COLLECTION_METADATA_KEYS: readonly ["updatedAt", "publishedAt", "title", "slug"];
|
|
41
54
|
export type PrismaEponymeRow = {
|
|
42
55
|
name: string;
|
|
43
56
|
data: unknown;
|
|
@@ -175,7 +188,9 @@ export declare class EponymeService {
|
|
|
175
188
|
/** Content as it was in a given version, for previewing without restoring it. */
|
|
176
189
|
private getVersionResult;
|
|
177
190
|
statuses(): Promise<Record<string, EponymeStatus>>;
|
|
178
|
-
|
|
191
|
+
/** Metadata keys that can be sorted on, alongside every field of the collection. */
|
|
192
|
+
collectionSortKeys(name: string): string[] | undefined;
|
|
193
|
+
listCollection(name: string, version?: EponymeVersion, query?: EponymeCollectionQuery): Promise<EponymeCollectionPage | undefined>;
|
|
179
194
|
/**
|
|
180
195
|
* Builds the public URL list configured by `previewPaths`.
|
|
181
196
|
* Collections are resolved directly from their stored rows so this performs
|
|
@@ -4,6 +4,7 @@ import { getEponymeCollections, getEponymeSchemas } from "../../utils/get-eponym
|
|
|
4
4
|
import { normalizeSlug } from "../../utils/normalize-slug.js";
|
|
5
5
|
import { applyPreviewSlug } from "../../utils/preview.js";
|
|
6
6
|
import { validateEponymeData, validateEponymePatch } from "../../utils/validate-eponyme-data.js";
|
|
7
|
+
export const COLLECTION_METADATA_KEYS = ["updatedAt", "publishedAt", "title", "slug"];
|
|
7
8
|
export class EponymeService {
|
|
8
9
|
constructor(config, client) {
|
|
9
10
|
this.client = client;
|
|
@@ -127,7 +128,13 @@ export class EponymeService {
|
|
|
127
128
|
}));
|
|
128
129
|
return Object.fromEntries(entries);
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
/** Metadata keys that can be sorted on, alongside every field of the collection. */
|
|
132
|
+
collectionSortKeys(name) {
|
|
133
|
+
const definition = this.collections[name];
|
|
134
|
+
if (!definition) return void 0;
|
|
135
|
+
return [.../* @__PURE__ */ new Set([...COLLECTION_METADATA_KEYS, ...Object.keys(definition.fields)])];
|
|
136
|
+
}
|
|
137
|
+
async listCollection(name, version = "published", query = {}) {
|
|
131
138
|
const definition = this.collections[name];
|
|
132
139
|
if (!definition) return void 0;
|
|
133
140
|
const rows = await this.client.eponyme.findMany({
|
|
@@ -149,7 +156,14 @@ export class EponymeService {
|
|
|
149
156
|
updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null
|
|
150
157
|
};
|
|
151
158
|
}));
|
|
152
|
-
|
|
159
|
+
const visible = version === "draft" ? entries : entries.filter((entry) => entry.publishedAt);
|
|
160
|
+
const sorted = query.orderBy ? sortCollectionEntries(visible, query.orderBy, query.order ?? "desc") : visible;
|
|
161
|
+
const skip = Math.max(0, Math.trunc(query.skip ?? 0) || 0);
|
|
162
|
+
const take = query.take === void 0 ? void 0 : Math.max(0, Math.trunc(query.take) || 0);
|
|
163
|
+
return {
|
|
164
|
+
entries: take === void 0 ? sorted.slice(skip) : sorted.slice(skip, skip + take),
|
|
165
|
+
total: sorted.length
|
|
166
|
+
};
|
|
153
167
|
}
|
|
154
168
|
/**
|
|
155
169
|
* Builds the public URL list configured by `previewPaths`.
|
|
@@ -333,6 +347,30 @@ export function isPrismaError(error, code) {
|
|
|
333
347
|
function sameData(left, right) {
|
|
334
348
|
return isDeepStrictEqual(left, right);
|
|
335
349
|
}
|
|
350
|
+
function sortValue(entry, key) {
|
|
351
|
+
if (COLLECTION_METADATA_KEYS.includes(key))
|
|
352
|
+
return entry[key];
|
|
353
|
+
return entry.data[key];
|
|
354
|
+
}
|
|
355
|
+
function isEmptyValue(value) {
|
|
356
|
+
return value === void 0 || value === null || value === "";
|
|
357
|
+
}
|
|
358
|
+
function sortCollectionEntries(entries, key, direction) {
|
|
359
|
+
const factor = direction === "asc" ? 1 : -1;
|
|
360
|
+
return [...entries].sort((left, right) => {
|
|
361
|
+
const a = sortValue(left, key);
|
|
362
|
+
const b = sortValue(right, key);
|
|
363
|
+
if (isEmptyValue(a) || isEmptyValue(b)) {
|
|
364
|
+
if (isEmptyValue(a) && isEmptyValue(b)) return left.slug.localeCompare(right.slug);
|
|
365
|
+
return isEmptyValue(a) ? 1 : -1;
|
|
366
|
+
}
|
|
367
|
+
if (typeof a === "number" && typeof b === "number")
|
|
368
|
+
return (a - b) * factor || left.slug.localeCompare(right.slug);
|
|
369
|
+
if (typeof a === "boolean" && typeof b === "boolean")
|
|
370
|
+
return (Number(a) - Number(b)) * factor || left.slug.localeCompare(right.slug);
|
|
371
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true }) * factor || left.slug.localeCompare(right.slug);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
336
374
|
function withLastmod(loc, publishedAt) {
|
|
337
375
|
return publishedAt ? { loc, lastmod: publishedAt } : { loc };
|
|
338
376
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import eponymeVariables from "#eponyme/variables";
|
|
2
|
+
import { interpolateEponymeValue, resolveEponymeVariables } from "../../utils/variables.js";
|
|
3
|
+
export function interpolateEponymeContent(value) {
|
|
4
|
+
return interpolateEponymeValue(value, resolveEponymeVariables(eponymeVariables));
|
|
5
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A variable resolves to a fixed value, or is computed on every read. */
|
|
2
|
+
export type EponymeVariableValue = string | number | (() => string | number);
|
|
3
|
+
export interface EponymeVariableDefinition {
|
|
4
|
+
/** Shown in the editor's variable menu. Defaults to the variable name. */
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
value: EponymeVariableValue;
|
|
8
|
+
}
|
|
9
|
+
export type EponymeVariables = Record<string, EponymeVariableValue | EponymeVariableDefinition>;
|
|
10
|
+
/** What the editor needs to list a variable without being able to resolve it. */
|
|
11
|
+
export interface EponymeVariableSummary {
|
|
12
|
+
name: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Value at build time, shown as an example in the menu. */
|
|
16
|
+
preview: string;
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EponymeVariableDefinition, EponymeVariables } from '../types/variables.js';
|
|
2
|
+
/** Always available, whatever the host application configures. */
|
|
3
|
+
export declare function builtinEponymeVariables(now?: Date): Record<string, EponymeVariableDefinition>;
|
|
4
|
+
/** Host variables win, so an application can override a built-in for its own wording. */
|
|
5
|
+
export declare function resolveEponymeVariables(custom?: EponymeVariables, now?: Date): Record<string, string>;
|
|
6
|
+
export declare function summariseEponymeVariables(custom?: EponymeVariables): Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
preview: string;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Replaces every `{{ name }}` occurrence. An unknown name is left untouched rather
|
|
14
|
+
* than blanked, so a typo shows up on the page instead of silently deleting text.
|
|
15
|
+
*/
|
|
16
|
+
export declare function interpolateEponymeText(text: string, variables: Record<string, string>): string;
|
|
17
|
+
/** Walks strings nested in objects and arrays, leaving every other value alone. */
|
|
18
|
+
export declare function interpolateEponymeValue<T>(value: T, variables: Record<string, string>): T;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const VARIABLE_PATTERN = /\{\{\s*([A-Z_]\w*)\s*\}\}/gi;
|
|
2
|
+
export function builtinEponymeVariables(now = /* @__PURE__ */ new Date()) {
|
|
3
|
+
return {
|
|
4
|
+
currentYear: { label: "Current year", description: "The year at the time the page is served.", value: () => now.getFullYear() },
|
|
5
|
+
nextYear: { label: "Next year", description: "Useful for a season spanning two years.", value: () => now.getFullYear() + 1 },
|
|
6
|
+
previousYear: { label: "Previous year", value: () => now.getFullYear() - 1 },
|
|
7
|
+
currentMonth: { label: "Current month", value: () => now.toLocaleDateString(void 0, { month: "long" }) },
|
|
8
|
+
currentDay: { label: "Current day", value: () => String(now.getDate()) },
|
|
9
|
+
today: { label: "Today", description: "Localised date, for example 29 July 2026.", value: () => now.toLocaleDateString(void 0, { dateStyle: "long" }) },
|
|
10
|
+
currentDate: { label: "Today (ISO)", description: "Machine-readable date, for example 2026-07-29.", value: () => now.toISOString().slice(0, 10) }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function toDefinition(entry) {
|
|
14
|
+
return typeof entry === "object" && entry !== null && "value" in entry ? entry : { value: entry };
|
|
15
|
+
}
|
|
16
|
+
export function resolveEponymeVariables(custom = {}, now) {
|
|
17
|
+
const merged = { ...builtinEponymeVariables(now) };
|
|
18
|
+
for (const [name, entry] of Object.entries(custom)) merged[name] = toDefinition(entry);
|
|
19
|
+
return Object.fromEntries(Object.entries(merged).map(([name, definition]) => {
|
|
20
|
+
try {
|
|
21
|
+
const value = typeof definition.value === "function" ? definition.value() : definition.value;
|
|
22
|
+
return [name, String(value)];
|
|
23
|
+
} catch {
|
|
24
|
+
return [name, ""];
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
export function summariseEponymeVariables(custom = {}) {
|
|
29
|
+
const merged = { ...builtinEponymeVariables() };
|
|
30
|
+
for (const [name, entry] of Object.entries(custom)) merged[name] = toDefinition(entry);
|
|
31
|
+
const resolved = resolveEponymeVariables(custom);
|
|
32
|
+
return Object.entries(merged).map(([name, definition]) => ({
|
|
33
|
+
name,
|
|
34
|
+
label: definition.label ?? name,
|
|
35
|
+
description: definition.description,
|
|
36
|
+
preview: resolved[name] ?? ""
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
export function interpolateEponymeText(text, variables) {
|
|
40
|
+
if (!text.includes("{{")) return text;
|
|
41
|
+
return text.replace(VARIABLE_PATTERN, (match, name) => variables[name] ?? match);
|
|
42
|
+
}
|
|
43
|
+
export function interpolateEponymeValue(value, variables) {
|
|
44
|
+
if (typeof value === "string") return interpolateEponymeText(value, variables);
|
|
45
|
+
if (Array.isArray(value)) return value.map((item) => interpolateEponymeValue(item, variables));
|
|
46
|
+
if (value && typeof value === "object") {
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(value).map(([key, nested]) => [key, interpolateEponymeValue(nested, variables)])
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { type collection, type defineEponymeConfig, type form } from './eponyme.mjs'
|
|
1
|
+
export { type collection, type defineEponymeConfig, type defineEponymeVariables, type form } from './eponyme.mjs'
|
|
2
2
|
|
|
3
3
|
export { type field, type today } from '../dist/runtime/fields/index.js'
|
|
4
4
|
|
|
5
5
|
export { type EPONYME_ROLES, type canEditEponyme, type isEponymeRole } from '../dist/runtime/types/auth.js'
|
|
6
6
|
|
|
7
|
-
export { type ArrayFieldDefinition, type ArrayFieldOptions, type ArrayItemDefinition, type ArrayItemFieldDefinition, type ArrayItemSchema, type ArrayItemValue, type BooleanFieldDefinition, type BooleanFieldOptions, type CheckboxGroupFieldDefinition, type CheckboxGroupFieldOptions, type CollectionSlugField, type ColorFieldDefinition, type ColorFieldOptions, type DateFieldDefinition, type DateFieldOptions, type EmailFieldDefinition, type EmailFieldOptions, type EponymeAuthSession, type EponymeAuthUser, type EponymeCollectionDataByName, type EponymeCollectionDefinition, type EponymeCollectionDefinitionBase, type EponymeCollectionName, type EponymeCollectionOptions, type EponymeConfig, type EponymeData, type EponymeDataByName, type EponymeFormDataByName, type EponymeFormDefinition, type EponymeFormDefinitionBase, type EponymeFormFieldDefinition, type EponymeFormMode, type EponymeFormName, type EponymeFormOptions, type EponymeFormSchema, type EponymeManagedUserResult, type EponymeName, type EponymeRole, type EponymeSchema, type FieldDefinition, type FieldValidationResult, type FieldValidator, type FieldVisibilityCondition, type ImageFieldDefinition, type ImageFieldOptions, type NumberFieldDefinition, type NumberFieldOptions, type RadioFieldDefinition, type RadioFieldOptions, type RichTextFieldDefinition, type RichTextFieldOptions, type SectionFieldDefinition, type SectionFieldOptions, type SectionItemFieldDefinition, type SectionItemValue, type SectionSchema, type SectionValue, type SelectFieldDefinition, type SelectFieldOptions, type SelectOption, type SlugFieldDefinition, type SlugFieldOptions, type StringFieldDefinition, type StringFieldOptions, type TabDefinition, type TabFieldDefinition, type TabFieldOptions, type TabsSchema, type TabsValue, type TextareaFieldDefinition, type TextareaFieldOptions, type TodayDateDefault, type UrlFieldDefinition, type UrlFieldOptions, type UrlType, type UrlValue } from '../dist/runtime/types/index.js'
|
|
7
|
+
export { type ArrayFieldDefinition, type ArrayFieldOptions, type ArrayItemDefinition, type ArrayItemFieldDefinition, type ArrayItemSchema, type ArrayItemValue, type BooleanFieldDefinition, type BooleanFieldOptions, type CheckboxGroupFieldDefinition, type CheckboxGroupFieldOptions, type CollectionSlugField, type ColorFieldDefinition, type ColorFieldOptions, type DateFieldDefinition, type DateFieldOptions, type EmailFieldDefinition, type EmailFieldOptions, type EponymeAuthSession, type EponymeAuthUser, type EponymeCollectionDataByName, type EponymeCollectionDefinition, type EponymeCollectionDefinitionBase, type EponymeCollectionName, type EponymeCollectionOptions, type EponymeConfig, type EponymeData, type EponymeDataByName, type EponymeFormDataByName, type EponymeFormDefinition, type EponymeFormDefinitionBase, type EponymeFormFieldDefinition, type EponymeFormMode, type EponymeFormName, type EponymeFormOptions, type EponymeFormSchema, type EponymeManagedUserResult, type EponymeName, type EponymeRole, type EponymeSchema, type EponymeVariableDefinition, type EponymeVariableSummary, type EponymeVariableValue, type EponymeVariables, type FieldDefinition, type FieldValidationResult, type FieldValidator, type FieldVisibilityCondition, type ImageFieldDefinition, type ImageFieldOptions, type NumberFieldDefinition, type NumberFieldOptions, type RadioFieldDefinition, type RadioFieldOptions, type RichTextFieldDefinition, type RichTextFieldOptions, type SectionFieldDefinition, type SectionFieldOptions, type SectionItemFieldDefinition, type SectionItemValue, type SectionSchema, type SectionValue, type SelectFieldDefinition, type SelectFieldOptions, type SelectOption, type SlugFieldDefinition, type SlugFieldOptions, type StringFieldDefinition, type StringFieldOptions, type TabDefinition, type TabFieldDefinition, type TabFieldOptions, type TabsSchema, type TabsValue, type TextareaFieldDefinition, type TextareaFieldOptions, type TodayDateDefault, type UrlFieldDefinition, type UrlFieldOptions, type UrlType, type UrlValue } from '../dist/runtime/types/index.js'
|
|
8
8
|
|
|
9
9
|
export { default } from './module.mjs'
|
|
10
10
|
|