@karibsen/eponyme 0.6.0 → 0.8.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.
- package/README.md +109 -45
- package/dist/eponyme.mjs +2 -3
- package/dist/module.d.mts +109 -13
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -6
- package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
- package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
- package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
- package/dist/runtime/components/fields/FieldRenderer.vue +22 -0
- package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
- package/dist/runtime/components/fields/PhoneField.vue +61 -0
- package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/RichTextField.vue +37 -1
- package/dist/runtime/components/fields/TagsField.d.vue.ts +21 -0
- package/dist/runtime/components/fields/TagsField.vue +55 -0
- package/dist/runtime/components/fields/TagsField.vue.d.ts +21 -0
- package/dist/runtime/components/ui/EPAlertDialog.d.vue.ts +37 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue +66 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue.d.ts +37 -0
- package/dist/runtime/components/ui/EPDialog.d.vue.ts +1 -0
- package/dist/runtime/components/ui/EPDialog.vue +6 -2
- package/dist/runtime/components/ui/EPDialog.vue.d.ts +1 -0
- package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
- package/dist/runtime/components/ui/EPTagsInput.d.vue.ts +27 -0
- package/dist/runtime/components/ui/EPTagsInput.vue +101 -0
- package/dist/runtime/components/ui/EPTagsInput.vue.d.ts +27 -0
- package/dist/runtime/composables/useEponymeCollection.d.ts +13 -1
- package/dist/runtime/composables/useEponymeCollection.js +29 -4
- package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
- package/dist/runtime/composables/useEponymeForm.js +1 -8
- package/dist/runtime/fields/index.d.ts +4 -0
- package/dist/runtime/fields/index.js +3 -1
- package/dist/runtime/fields/phone.d.ts +2 -0
- package/dist/runtime/fields/phone.js +3 -0
- package/dist/runtime/fields/tags.d.ts +6 -0
- package/dist/runtime/fields/tags.js +3 -0
- package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
- package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
- package/dist/runtime/pages/EponymeIndexPage.vue +0 -3
- package/dist/runtime/server/api/eponyme-collections/[path].get.js +40 -1
- package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
- package/dist/runtime/server/plugins/eponyme-sync.js +11 -1
- package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
- package/dist/runtime/server/services/eponyme-form-store.js +4 -3
- package/dist/runtime/server/services/eponyme-store.d.ts +289 -30
- package/dist/runtime/server/services/eponyme-store.js +405 -73
- package/dist/runtime/server/utils/eponyme-reindex.d.ts +13 -0
- package/dist/runtime/server/utils/eponyme-reindex.js +4 -0
- package/dist/runtime/types/eponyme-config.d.ts +0 -7
- package/dist/runtime/types/eponyme.d.ts +54 -6
- package/dist/runtime/types/field.d.ts +82 -4
- package/dist/runtime/types/index.d.ts +0 -1
- package/dist/runtime/types/index.js +0 -1
- package/dist/runtime/utils/eponyme-entry-index.d.ts +39 -0
- package/dist/runtime/utils/eponyme-entry-index.js +51 -0
- package/dist/runtime/utils/get-field-default-value.js +2 -0
- package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
- package/dist/runtime/utils/middle-ellipsis.js +8 -0
- package/dist/runtime/utils/normalize-eponyme-values.d.ts +13 -0
- package/dist/runtime/utils/normalize-eponyme-values.js +40 -0
- package/dist/runtime/utils/normalize-phone.d.ts +26 -0
- package/dist/runtime/utils/normalize-phone.js +17 -0
- package/dist/runtime/utils/normalize-tags.d.ts +10 -0
- package/dist/runtime/utils/normalize-tags.js +17 -0
- package/dist/runtime/utils/validate-eponyme-data.js +45 -3
- package/dist/runtime/utils/variables.d.ts +4 -0
- package/dist/runtime/utils/variables.js +8 -1
- package/package.json +4 -2
- package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
- package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
- package/dist/runtime/types/captcha.d.ts +0 -16
- package/dist/runtime/types/captcha.js +0 -0
- package/dist/runtime/utils/no-captcha.d.ts +0 -7
- package/dist/runtime/utils/no-captcha.js +0 -5
|
@@ -10,14 +10,12 @@ function resolveForm(name) {
|
|
|
10
10
|
if (!definition) throw new Error(`[Eponyme] Unknown form "${name}". Declare it with form() in eponyme.config.ts.`);
|
|
11
11
|
return definition;
|
|
12
12
|
}
|
|
13
|
-
const CAPTCHA_TOKEN_KEY = "_eponyme_captcha";
|
|
14
13
|
export function useEponymeForm(name, options = {}) {
|
|
15
14
|
const definition = resolveForm(name);
|
|
16
15
|
const requestFetch = useRequestFetch();
|
|
17
16
|
const serverErrors = ref({});
|
|
18
17
|
const pending = ref(false);
|
|
19
18
|
const submitted = ref(false);
|
|
20
|
-
const captchaToken = ref("");
|
|
21
19
|
const values = ref(createDefaultEponymeData(definition.fields));
|
|
22
20
|
const validation = useEponymeValidation(definition.fields, values, ref({}), { serverErrors });
|
|
23
21
|
const fields = computed(() => Object.entries(definition.fields).map(([fieldName, fieldDefinition]) => ({
|
|
@@ -35,7 +33,6 @@ export function useEponymeForm(name, options = {}) {
|
|
|
35
33
|
values.value = createDefaultEponymeData(definition.fields);
|
|
36
34
|
serverErrors.value = {};
|
|
37
35
|
submitted.value = false;
|
|
38
|
-
captchaToken.value = "";
|
|
39
36
|
validation.reset();
|
|
40
37
|
}
|
|
41
38
|
async function submit() {
|
|
@@ -52,9 +49,7 @@ export function useEponymeForm(name, options = {}) {
|
|
|
52
49
|
console.warn(`[Eponyme] Form "${name}" is managed, so its onSubmit handler is ignored.`);
|
|
53
50
|
await requestFetch(`/api/eponyme-forms/${name}`, {
|
|
54
51
|
method: "POST",
|
|
55
|
-
|
|
56
|
-
// transport, and the schema only knows about declared fields.
|
|
57
|
-
body: definition.captcha ? { ...values.value, [CAPTCHA_TOKEN_KEY]: captchaToken.value } : values.value
|
|
52
|
+
body: values.value
|
|
58
53
|
});
|
|
59
54
|
}
|
|
60
55
|
submitted.value = true;
|
|
@@ -77,8 +72,6 @@ export function useEponymeForm(name, options = {}) {
|
|
|
77
72
|
pending,
|
|
78
73
|
submitted,
|
|
79
74
|
honeypot: definition.honeypot,
|
|
80
|
-
requiresCaptcha: definition.captcha,
|
|
81
|
-
captchaToken,
|
|
82
75
|
submit,
|
|
83
76
|
reset
|
|
84
77
|
};
|
|
@@ -6,12 +6,14 @@ import { date, today } from './date.js';
|
|
|
6
6
|
import { email } from './email.js';
|
|
7
7
|
import { image } from './image.js';
|
|
8
8
|
import { number } from './number.js';
|
|
9
|
+
import { phone } from './phone.js';
|
|
9
10
|
import { radio } from './radio.js';
|
|
10
11
|
import { richText } from './rich-text.js';
|
|
11
12
|
import { section } from './section.js';
|
|
12
13
|
import { select } from './select.js';
|
|
13
14
|
import { slug } from './slug.js';
|
|
14
15
|
import { string } from './string.js';
|
|
16
|
+
import { tags } from './tags.js';
|
|
15
17
|
import { tab } from './tab.js';
|
|
16
18
|
import { textarea } from './textarea.js';
|
|
17
19
|
import { url } from './url.js';
|
|
@@ -19,6 +21,7 @@ export declare const field: {
|
|
|
19
21
|
string: typeof string;
|
|
20
22
|
slug: typeof slug;
|
|
21
23
|
email: typeof email;
|
|
24
|
+
phone: typeof phone;
|
|
22
25
|
url: typeof url;
|
|
23
26
|
textarea: typeof textarea;
|
|
24
27
|
richText: typeof richText;
|
|
@@ -28,6 +31,7 @@ export declare const field: {
|
|
|
28
31
|
select: typeof select;
|
|
29
32
|
radio: typeof radio;
|
|
30
33
|
checkboxGroup: typeof checkboxGroup;
|
|
34
|
+
tags: typeof tags;
|
|
31
35
|
date: typeof date;
|
|
32
36
|
color: typeof color;
|
|
33
37
|
array: typeof array;
|
|
@@ -6,14 +6,16 @@ import { date, today } from "./date.js";
|
|
|
6
6
|
import { email } from "./email.js";
|
|
7
7
|
import { image } from "./image.js";
|
|
8
8
|
import { number } from "./number.js";
|
|
9
|
+
import { phone } from "./phone.js";
|
|
9
10
|
import { radio } from "./radio.js";
|
|
10
11
|
import { richText } from "./rich-text.js";
|
|
11
12
|
import { section } from "./section.js";
|
|
12
13
|
import { select } from "./select.js";
|
|
13
14
|
import { slug } from "./slug.js";
|
|
14
15
|
import { string } from "./string.js";
|
|
16
|
+
import { tags } from "./tags.js";
|
|
15
17
|
import { tab } from "./tab.js";
|
|
16
18
|
import { textarea } from "./textarea.js";
|
|
17
19
|
import { url } from "./url.js";
|
|
18
|
-
export const field = { string, slug, email, url, textarea, richText, number, boolean, image, select, radio, checkboxGroup, date, color, array, section, tab };
|
|
20
|
+
export const field = { string, slug, email, phone, url, textarea, richText, number, boolean, image, select, radio, checkboxGroup, tags, date, color, array, section, tab };
|
|
19
21
|
export { today };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TagsFieldDefinition, TagsFieldOptions } from '../types/field.js';
|
|
2
|
+
/**
|
|
3
|
+
* The whole options object is the type parameter, so the literals an author writes —
|
|
4
|
+
* the suggestions and `allowCustom` — survive into the value type. See `TagsValueOf`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function tags<const O extends TagsFieldOptions>(options?: O): TagsFieldDefinition<O>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("#app").RouteMiddleware;
|
|
2
2
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("#app").RouteMiddleware;
|
|
2
2
|
export default _default;
|
|
@@ -104,9 +104,6 @@ function closePreview() {
|
|
|
104
104
|
<section class="ep:mx-auto ep:w-full ep:max-w-3xl ep:px-6 ep:py-8 ep:md:px-10 ep:md:py-12">
|
|
105
105
|
<div class="ep:flex ep:flex-wrap ep:items-start ep:justify-between ep:gap-4">
|
|
106
106
|
<div>
|
|
107
|
-
<p class="ep:m-0 ep:text-[11px] ep:font-semibold ep:tracking-widest ep:text-muted-ep ep:uppercase">
|
|
108
|
-
Eponyme
|
|
109
|
-
</p>
|
|
110
107
|
<h1 class="ep:mt-2 ep:text-3xl ep:font-semibold ep:tracking-tight ep:text-white">
|
|
111
108
|
Content entries
|
|
112
109
|
</h1>
|
|
@@ -4,12 +4,50 @@ import { requireEponymeUser } from "../../utils/auth.js";
|
|
|
4
4
|
import { getEponymeCacheTags, setEponymePublicCache } from "../../utils/eponyme-cache.js";
|
|
5
5
|
import { interpolateEponymeContent } from "../../utils/eponyme-variables.js";
|
|
6
6
|
const MAX_TAKE = 200;
|
|
7
|
+
const OPERATORS = ["gte", "lte", "gt", "lt", "contains", "in", "not"];
|
|
8
|
+
const LIST_OPERATORS = /* @__PURE__ */ new Set(["in", "not"]);
|
|
9
|
+
const FILTER_KEY = /^where\[([^\]]+)\](?:\[([^\]]+)\])?$/;
|
|
7
10
|
function readCount(raw, min, max) {
|
|
8
11
|
if (raw === void 0 || raw === "") return void 0;
|
|
9
12
|
const value = Number(raw);
|
|
10
13
|
if (!Number.isFinite(value)) return void 0;
|
|
11
14
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
12
15
|
}
|
|
16
|
+
function readFilter(query, allowed) {
|
|
17
|
+
const where = {};
|
|
18
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
19
|
+
const match = FILTER_KEY.exec(rawKey);
|
|
20
|
+
if (!match) continue;
|
|
21
|
+
const [, key, operator] = match;
|
|
22
|
+
if (!allowed?.includes(key)) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
statusMessage: `Unknown filter key "${key}". Available keys: ${allowed?.join(", ") || "none"}.`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (operator === void 0) {
|
|
29
|
+
const values = (Array.isArray(rawValue) ? rawValue : [rawValue]).map(String).filter(Boolean);
|
|
30
|
+
if (values.length) where[key] = values.length === 1 ? values[0] : values;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!OPERATORS.includes(operator)) {
|
|
34
|
+
throw createError({
|
|
35
|
+
statusCode: 400,
|
|
36
|
+
statusMessage: `Unknown filter operator "${operator}". Available operators: ${OPERATORS.join(", ")}.`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const existing = where[key];
|
|
40
|
+
const operators = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
|
|
41
|
+
if (LIST_OPERATORS.has(operator)) {
|
|
42
|
+
const values = (Array.isArray(rawValue) ? rawValue : [rawValue]).map(String).filter(Boolean);
|
|
43
|
+
if (values.length) operators[operator] = values;
|
|
44
|
+
} else {
|
|
45
|
+
operators[operator] = String(Array.isArray(rawValue) ? rawValue[0] : rawValue);
|
|
46
|
+
}
|
|
47
|
+
where[key] = operators;
|
|
48
|
+
}
|
|
49
|
+
return Object.keys(where).length ? where : void 0;
|
|
50
|
+
}
|
|
13
51
|
export default defineEventHandler(async (event) => {
|
|
14
52
|
const name = decodeURIComponent(getRequestURL(event).pathname.replace(/^\/api\/eponyme-collections\//, ""));
|
|
15
53
|
const query = getQuery(event);
|
|
@@ -33,7 +71,8 @@ export default defineEventHandler(async (event) => {
|
|
|
33
71
|
take: readCount(query.take, 1, MAX_TAKE),
|
|
34
72
|
skip: readCount(query.skip, 0, Number.MAX_SAFE_INTEGER),
|
|
35
73
|
orderBy,
|
|
36
|
-
order: query.order === "asc" ? "asc" : query.order === "desc" ? "desc" : void 0
|
|
74
|
+
order: query.order === "asc" ? "asc" : query.order === "desc" ? "desc" : void 0,
|
|
75
|
+
where: readFilter(query, service.collectionFilterKeys(name))
|
|
37
76
|
});
|
|
38
77
|
if (!page) throw createError({ statusCode: 404, statusMessage: "Eponyme collection not found." });
|
|
39
78
|
return query.raw ? page : interpolateEponymeContent(page);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getRequestHeader, readRawBody, setResponseHeader, setResponseStatus } from "h3";
|
|
2
2
|
import { useEponymeFormService } from "../../services/eponyme-form-service.js";
|
|
3
3
|
import { readEponymeFormRoute } from "../../utils/form-route.js";
|
|
4
|
-
import { CAPTCHA_TOKEN_KEY, isEponymeCaptchaConfigured, verifyEponymeCaptcha } from "../../utils/eponyme-captcha.js";
|
|
5
4
|
import { callEponymeBlockingHook, callEponymeHook } from "../../utils/eponyme-hooks.js";
|
|
6
5
|
export default defineEventHandler(async (event) => {
|
|
7
6
|
const route = readEponymeFormRoute(event);
|
|
@@ -27,20 +26,6 @@ export default defineEventHandler(async (event) => {
|
|
|
27
26
|
setResponseStatus(event, 201);
|
|
28
27
|
return { submitted: true };
|
|
29
28
|
}
|
|
30
|
-
if (definition.captcha) {
|
|
31
|
-
if (!isEponymeCaptchaConfigured()) {
|
|
32
|
-
throw createError({
|
|
33
|
-
statusCode: 500,
|
|
34
|
-
statusMessage: `Form "${route.name}" requires a captcha but no adapter is installed. Add one, such as @karibsen/eponyme-captcha.`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
const token = payload && typeof payload === "object" ? payload[CAPTCHA_TOKEN_KEY] : void 0;
|
|
38
|
-
const result2 = await verifyEponymeCaptcha(event, token);
|
|
39
|
-
if (!result2.success) {
|
|
40
|
-
setResponseStatus(event, 422);
|
|
41
|
-
return { errors: { _form: ["Captcha verification failed. Please try again."] } };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
29
|
const validated = service.validate(route.name, payload);
|
|
45
30
|
if (validated && "errors" in validated) {
|
|
46
31
|
setResponseStatus(event, 422);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNitroPlugin } from "nitropack/runtime";
|
|
1
|
+
import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
|
|
2
2
|
import { useEponymeAuthService } from "../services/eponyme-auth-service.js";
|
|
3
3
|
import { useEponymeService } from "../services/eponyme-service.js";
|
|
4
4
|
export default defineNitroPlugin(async () => {
|
|
@@ -11,4 +11,14 @@ export default defineNitroPlugin(async () => {
|
|
|
11
11
|
);
|
|
12
12
|
}
|
|
13
13
|
await useEponymeService().syncAll();
|
|
14
|
+
const content = useRuntimeConfig().eponymeContent;
|
|
15
|
+
if (content?.autoReindex === false) return;
|
|
16
|
+
try {
|
|
17
|
+
await useEponymeService().syncIndexState();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"[Eponyme] Index rebuild failed. Apply the EponymeEntryIndex and EponymeIndexState Prisma migrations before starting the application, or set `eponyme.autoReindex: false` to start without it and run `reindexEponymeEntries()` by hand.",
|
|
21
|
+
{ cause: error }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
14
24
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EponymeConfig, EponymeFormDefinitionBase } from '../../types/index.js';
|
|
2
2
|
import { type ValidationErrors } from '../../utils/validate-eponyme-data.js';
|
|
3
|
+
/** Kept here rather than imported, so the store stays free of H3 imports. */
|
|
3
4
|
type DateValue = Date | string;
|
|
4
5
|
export interface EponymeFormSubmission {
|
|
5
6
|
id: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { normalizeEponymeValues } from "../../utils/normalize-eponyme-values.js";
|
|
2
3
|
import { getEponymeForms } from "../../utils/get-eponyme-schemas.js";
|
|
3
4
|
import { validateEponymeData } from "../../utils/validate-eponyme-data.js";
|
|
4
|
-
const CAPTCHA_TOKEN_KEY = "_eponyme_captcha";
|
|
5
5
|
const DEFAULT_PER_PAGE = 25;
|
|
6
6
|
const MAX_PER_PAGE = 100;
|
|
7
7
|
export class EponymeFormService {
|
|
@@ -37,10 +37,11 @@ export class EponymeFormService {
|
|
|
37
37
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
38
38
|
return { errors: { _form: ["Body must be an object."] } };
|
|
39
39
|
const input = payload;
|
|
40
|
-
const { [definition.honeypot || ""]: _honeypot,
|
|
41
|
-
const unknownKeys = Object.keys(
|
|
40
|
+
const { [definition.honeypot || ""]: _honeypot, ...submitted } = input;
|
|
41
|
+
const unknownKeys = Object.keys(submitted).filter((key) => !(key in definition.fields));
|
|
42
42
|
if (unknownKeys.length)
|
|
43
43
|
return { errors: Object.fromEntries(unknownKeys.map((key) => [key, ["Unknown field."]])) };
|
|
44
|
+
const rest = normalizeEponymeValues(definition.fields, submitted);
|
|
44
45
|
const errors = validateEponymeData(definition.fields, rest, "publish");
|
|
45
46
|
if (Object.keys(errors).length) return { errors };
|
|
46
47
|
return { data: rest };
|