@xleddyl/nuxt-cms 0.1.9
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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/module.d.mts +36 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +765 -0
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +15 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +110 -0
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +15 -0
- package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/EmptyState.vue +20 -0
- package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +23 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +78 -0
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +23 -0
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +85 -0
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/JsonField.d.vue.ts +10 -0
- package/dist/runtime/app/components/cms/JsonField.vue +51 -0
- package/dist/runtime/app/components/cms/JsonField.vue.d.ts +10 -0
- package/dist/runtime/app/components/cms/MediaField.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/MediaField.vue +81 -0
- package/dist/runtime/app/components/cms/MediaField.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +19 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue +309 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +19 -0
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +14 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +132 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +14 -0
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +19 -0
- package/dist/runtime/app/components/cms/PageHeader.vue +23 -0
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +19 -0
- package/dist/runtime/app/components/cms/RelationField.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/RelationField.vue +87 -0
- package/dist/runtime/app/components/cms/RelationField.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/RichTextField.d.vue.ts +10 -0
- package/dist/runtime/app/components/cms/RichTextField.vue +123 -0
- package/dist/runtime/app/components/cms/RichTextField.vue.d.ts +10 -0
- package/dist/runtime/app/components/cms/SlugField.d.vue.ts +14 -0
- package/dist/runtime/app/components/cms/SlugField.vue +44 -0
- package/dist/runtime/app/components/cms/SlugField.vue.d.ts +14 -0
- package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +15 -0
- package/dist/runtime/app/components/cms/TranslatableField.vue +64 -0
- package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +15 -0
- package/dist/runtime/app/composables/cms-query.d.ts +4 -0
- package/dist/runtime/app/composables/cms-query.js +18 -0
- package/dist/runtime/app/composables/cms-runtime.d.ts +5 -0
- package/dist/runtime/app/composables/cms-runtime.js +4 -0
- package/dist/runtime/app/i18n/en.json +93 -0
- package/dist/runtime/app/i18n/it.json +93 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/cms-admin.vue +97 -0
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +13 -0
- package/dist/runtime/app/middleware/cms-auth.d.ts +2 -0
- package/dist/runtime/app/middleware/cms-auth.js +7 -0
- package/dist/runtime/app/pages/admin-collection.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-collection.vue +273 -0
- package/dist/runtime/app/pages/admin-collection.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-entry.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-entry.vue +156 -0
- package/dist/runtime/app/pages/admin-entry.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-index.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-index.vue +11 -0
- package/dist/runtime/app/pages/admin-index.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-login.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-login.vue +87 -0
- package/dist/runtime/app/pages/admin-login.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-media.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-media.vue +13 -0
- package/dist/runtime/app/pages/admin-media.vue.d.ts +3 -0
- package/dist/runtime/app/utils/api.d.ts +7 -0
- package/dist/runtime/app/utils/api.js +3 -0
- package/dist/runtime/app/utils/ui.d.ts +8 -0
- package/dist/runtime/app/utils/ui.js +9 -0
- package/dist/runtime/assets/main.css +1 -0
- package/dist/runtime/server/api/collection.get.d.ts +6 -0
- package/dist/runtime/server/api/collection.get.js +19 -0
- package/dist/runtime/server/api/collection.post.d.ts +2 -0
- package/dist/runtime/server/api/collection.post.js +30 -0
- package/dist/runtime/server/api/collection.put.d.ts +2 -0
- package/dist/runtime/server/api/collection.put.js +28 -0
- package/dist/runtime/server/api/item.delete.d.ts +4 -0
- package/dist/runtime/server/api/item.delete.js +19 -0
- package/dist/runtime/server/api/item.get.d.ts +2 -0
- package/dist/runtime/server/api/item.get.js +18 -0
- package/dist/runtime/server/api/item.put.d.ts +2 -0
- package/dist/runtime/server/api/item.put.js +40 -0
- package/dist/runtime/server/api/media-presign.post.d.ts +11 -0
- package/dist/runtime/server/api/media-presign.post.js +51 -0
- package/dist/runtime/server/api/media.delete.d.ts +4 -0
- package/dist/runtime/server/api/media.delete.js +25 -0
- package/dist/runtime/server/api/media.get.d.ts +5 -0
- package/dist/runtime/server/api/media.get.js +13 -0
- package/dist/runtime/server/api/media.post.d.ts +3 -0
- package/dist/runtime/server/api/media.post.js +32 -0
- package/dist/runtime/server/api/media.put.d.ts +3 -0
- package/dist/runtime/server/api/media.put.js +28 -0
- package/dist/runtime/server/plugins/migrate-postgres.d.ts +2 -0
- package/dist/runtime/server/plugins/migrate-postgres.js +9 -0
- package/dist/runtime/server/plugins/migrate-sqlite.d.ts +2 -0
- package/dist/runtime/server/plugins/migrate-sqlite.js +9 -0
- package/dist/runtime/server/plugins/session-check.d.ts +2 -0
- package/dist/runtime/server/plugins/session-check.js +9 -0
- package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/login.post.js +62 -0
- package/dist/runtime/server/routes/graphql.d.ts +2 -0
- package/dist/runtime/server/routes/graphql.js +34 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/custom-id.d.ts +2 -0
- package/dist/runtime/server/utils/custom-id.js +10 -0
- package/dist/runtime/server/utils/db-errors.d.ts +1 -0
- package/dist/runtime/server/utils/db-errors.js +31 -0
- package/dist/runtime/server/utils/db-postgres.d.ts +3 -0
- package/dist/runtime/server/utils/db-postgres.js +16 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +4 -0
- package/dist/runtime/server/utils/db-sqlite.js +17 -0
- package/dist/runtime/server/utils/graphql-depth.d.ts +4 -0
- package/dist/runtime/server/utils/graphql-depth.js +72 -0
- package/dist/runtime/server/utils/graphql.d.ts +14 -0
- package/dist/runtime/server/utils/graphql.js +317 -0
- package/dist/runtime/server/utils/media.d.ts +24 -0
- package/dist/runtime/server/utils/media.js +38 -0
- package/dist/runtime/server/utils/registry.d.ts +20 -0
- package/dist/runtime/server/utils/registry.js +47 -0
- package/dist/runtime/server/utils/relations.d.ts +12 -0
- package/dist/runtime/server/utils/relations.js +80 -0
- package/dist/runtime/server/utils/require-admin.d.ts +2 -0
- package/dist/runtime/server/utils/require-admin.js +13 -0
- package/dist/runtime/shared/empty-config.d.ts +2 -0
- package/dist/runtime/shared/empty-config.js +1 -0
- package/dist/runtime/shared/graphql-sdl.d.ts +5 -0
- package/dist/runtime/shared/graphql-sdl.js +147 -0
- package/dist/runtime/shared/index.d.ts +62 -0
- package/dist/runtime/shared/index.js +55 -0
- package/dist/runtime/shared/validation.d.ts +16 -0
- package/dist/runtime/shared/validation.js +82 -0
- package/dist/types.d.mts +3 -0
- package/package.json +93 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isTranslatableField } from "./index.js";
|
|
2
|
+
export function typeName(name) {
|
|
3
|
+
return name.replace(/(?:^|_)([a-z0-9])/gi, (_, c) => c.toUpperCase());
|
|
4
|
+
}
|
|
5
|
+
export function blockUnionName(entryName, fieldKey) {
|
|
6
|
+
return `${typeName(entryName)}${typeName(fieldKey)}Block`;
|
|
7
|
+
}
|
|
8
|
+
export function blockTypeName(entryName, fieldKey, blockName) {
|
|
9
|
+
return `${typeName(entryName)}${typeName(fieldKey)}${typeName(blockName)}`;
|
|
10
|
+
}
|
|
11
|
+
function scalarFor(field) {
|
|
12
|
+
switch (field.type) {
|
|
13
|
+
case "number":
|
|
14
|
+
return field.integer ? "Int" : "Float";
|
|
15
|
+
case "boolean":
|
|
16
|
+
return "Boolean";
|
|
17
|
+
case "json":
|
|
18
|
+
return "JSON";
|
|
19
|
+
default:
|
|
20
|
+
return "String";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function filterScalarFor(field) {
|
|
24
|
+
if (isTranslatableField(field)) return null;
|
|
25
|
+
switch (field.type) {
|
|
26
|
+
case "json":
|
|
27
|
+
return null;
|
|
28
|
+
case "blocks":
|
|
29
|
+
return null;
|
|
30
|
+
case "relation":
|
|
31
|
+
return field.cardinality === "many-to-many" ? null : "StringFilter";
|
|
32
|
+
case "number":
|
|
33
|
+
return field.integer ? "IntFilter" : "FloatFilter";
|
|
34
|
+
case "boolean":
|
|
35
|
+
return "BooleanFilter";
|
|
36
|
+
default:
|
|
37
|
+
return "StringFilter";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function filterableKeys(entry) {
|
|
41
|
+
const keys = [["id", "StringFilter"]];
|
|
42
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
43
|
+
const scalar = filterScalarFor(field);
|
|
44
|
+
if (scalar) keys.push([key, scalar]);
|
|
45
|
+
}
|
|
46
|
+
keys.push(["createdAt", "StringFilter"], ["updatedAt", "StringFilter"]);
|
|
47
|
+
return keys;
|
|
48
|
+
}
|
|
49
|
+
function fieldSdl(config, entryName, key, field) {
|
|
50
|
+
if (field.type === "relation") {
|
|
51
|
+
const target = typeName(field.to);
|
|
52
|
+
if (field.cardinality === "many-to-many") return ` ${key}: [${target}!]!`;
|
|
53
|
+
const nonNull = field.required && !config[field.to]?.drafts;
|
|
54
|
+
return ` ${key}: ${target}${nonNull ? "!" : ""}`;
|
|
55
|
+
}
|
|
56
|
+
if (field.type === "media") return ` ${key}: CmsMedia`;
|
|
57
|
+
if (field.type === "blocks")
|
|
58
|
+
return ` ${key}: [${blockUnionName(entryName, key)}!]${field.required ? "!" : ""}`;
|
|
59
|
+
return ` ${key}: ${scalarFor(field)}${field.required ? "!" : ""}`;
|
|
60
|
+
}
|
|
61
|
+
function entrySdl(config, name, entry) {
|
|
62
|
+
const lines = [" id: ID!"];
|
|
63
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
64
|
+
lines.push(fieldSdl(config, name, key, field));
|
|
65
|
+
}
|
|
66
|
+
if (entry.kind === "collection") lines.push(" createdAt: String!");
|
|
67
|
+
lines.push(" updatedAt: String!");
|
|
68
|
+
return `type ${typeName(name)} {
|
|
69
|
+
${lines.join("\n")}
|
|
70
|
+
}`;
|
|
71
|
+
}
|
|
72
|
+
function blocksSdl(name, key, field) {
|
|
73
|
+
const defs = [];
|
|
74
|
+
const members = [];
|
|
75
|
+
for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
|
|
76
|
+
const gqlType = blockTypeName(name, key, blockName);
|
|
77
|
+
members.push(gqlType);
|
|
78
|
+
const lines = [" type: String!"];
|
|
79
|
+
for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
|
|
80
|
+
if (blockField.type === "media") {
|
|
81
|
+
lines.push(` ${blockFieldKey}: CmsMedia`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
lines.push(` ${blockFieldKey}: ${scalarFor(blockField)}${blockField.required ? "!" : ""}`);
|
|
85
|
+
}
|
|
86
|
+
defs.push(`type ${gqlType} {
|
|
87
|
+
${lines.join("\n")}
|
|
88
|
+
}`);
|
|
89
|
+
}
|
|
90
|
+
if (members.length) defs.push(`union ${blockUnionName(name, key)} = ${members.join(" | ")}`);
|
|
91
|
+
return defs;
|
|
92
|
+
}
|
|
93
|
+
function filterSdl(name, entry) {
|
|
94
|
+
const gqlType = typeName(name);
|
|
95
|
+
const keys = filterableKeys(entry);
|
|
96
|
+
const filters = keys.map(([key, scalar]) => ` ${key}: ${scalar}`).join("\n");
|
|
97
|
+
const sortFields = keys.map(([key]) => ` ${key}`).join("\n");
|
|
98
|
+
return [
|
|
99
|
+
`input ${gqlType}Filters {
|
|
100
|
+
${filters}
|
|
101
|
+
}`,
|
|
102
|
+
`enum ${gqlType}SortField {
|
|
103
|
+
${sortFields}
|
|
104
|
+
}`,
|
|
105
|
+
`input ${gqlType}Sort {
|
|
106
|
+
field: ${gqlType}SortField!
|
|
107
|
+
direction: SortDirection = asc
|
|
108
|
+
}`
|
|
109
|
+
].join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
const COMMON_SDL = [
|
|
112
|
+
"scalar JSON",
|
|
113
|
+
"enum SortDirection {\n asc\n desc\n}",
|
|
114
|
+
"input IntFilter {\n eq: Int\n neq: Int\n gt: Int\n gte: Int\n lt: Int\n lte: Int\n in: [Int!]\n isNull: Boolean\n}",
|
|
115
|
+
"input FloatFilter {\n eq: Float\n neq: Float\n gt: Float\n gte: Float\n lt: Float\n lte: Float\n in: [Float!]\n isNull: Boolean\n}",
|
|
116
|
+
"input StringFilter {\n eq: String\n neq: String\n gt: String\n gte: String\n lt: String\n lte: String\n like: String\n in: [String!]\n isNull: Boolean\n}",
|
|
117
|
+
"input BooleanFilter {\n eq: Boolean\n neq: Boolean\n isNull: Boolean\n}",
|
|
118
|
+
"type CmsMedia {\n key: String!\n url: String\n type: String!\n alt: String\n folder: String\n mime: String\n size: Int\n width: Int\n height: Int\n}"
|
|
119
|
+
];
|
|
120
|
+
export function renderGraphqlSdl(config) {
|
|
121
|
+
const queryLines = [];
|
|
122
|
+
const types = [];
|
|
123
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
124
|
+
const gqlType = typeName(name);
|
|
125
|
+
types.push(entrySdl(config, name, entry));
|
|
126
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
127
|
+
if (field.type === "blocks") types.push(...blocksSdl(name, key, field));
|
|
128
|
+
}
|
|
129
|
+
if (entry.kind === "single") {
|
|
130
|
+
queryLines.push(` ${name}(locale: String): ${gqlType}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
types.push(filterSdl(name, entry));
|
|
134
|
+
queryLines.push(
|
|
135
|
+
` ${name}(filters: ${gqlType}Filters, sort: [${gqlType}Sort!], limit: Int, offset: Int, locale: String): [${gqlType}!]!`
|
|
136
|
+
);
|
|
137
|
+
queryLines.push(` ${name}ById(id: ID!, locale: String): ${gqlType}`);
|
|
138
|
+
queryLines.push(` ${name}Count(filters: ${gqlType}Filters): Int!`);
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
...COMMON_SDL,
|
|
142
|
+
...types,
|
|
143
|
+
`type Query {
|
|
144
|
+
${queryLines.length ? queryLines.join("\n") : " _empty: Boolean"}
|
|
145
|
+
}`
|
|
146
|
+
].join("\n\n");
|
|
147
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
2
|
+
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
export type CmsTable = SQLiteTable | PgTable;
|
|
4
|
+
export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'email' | 'slug' | 'select' | 'json' | 'media' | 'relation' | 'blocks';
|
|
5
|
+
export interface CmsI18n {
|
|
6
|
+
locales: string[];
|
|
7
|
+
defaultLocale: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const MEDIA_TYPES: readonly ["image", "video", "file"];
|
|
10
|
+
export type MediaType = (typeof MEDIA_TYPES)[number];
|
|
11
|
+
export declare function mediaTypeForKey(key: string): MediaType;
|
|
12
|
+
export declare function mediaFilename(key: string): string;
|
|
13
|
+
export declare function mediaTypeFor(mime: string | null | undefined, key: string): MediaType;
|
|
14
|
+
export declare function mediaIconFor(type: MediaType): string;
|
|
15
|
+
export declare function mediaPublicUrl(baseUrl: string | null | undefined, key: string): string | null;
|
|
16
|
+
export declare function slugify(value: string): string;
|
|
17
|
+
export interface MediaItem {
|
|
18
|
+
id: number;
|
|
19
|
+
key: string;
|
|
20
|
+
alt: string | null;
|
|
21
|
+
folder: string | null;
|
|
22
|
+
mime: string | null;
|
|
23
|
+
size: number | null;
|
|
24
|
+
width: number | null;
|
|
25
|
+
height: number | null;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
type: MediaType;
|
|
28
|
+
url: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface BlockConfig {
|
|
31
|
+
label: string;
|
|
32
|
+
fields: Record<string, FieldConfig>;
|
|
33
|
+
}
|
|
34
|
+
export interface FieldConfig {
|
|
35
|
+
label: string;
|
|
36
|
+
type: FieldType;
|
|
37
|
+
required?: boolean;
|
|
38
|
+
textarea?: boolean;
|
|
39
|
+
integer?: boolean;
|
|
40
|
+
translatable?: boolean;
|
|
41
|
+
options?: string[];
|
|
42
|
+
from?: string;
|
|
43
|
+
blocks?: Record<string, BlockConfig>;
|
|
44
|
+
mediaType?: MediaType;
|
|
45
|
+
accept?: string[];
|
|
46
|
+
to?: string;
|
|
47
|
+
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
48
|
+
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
49
|
+
}
|
|
50
|
+
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
51
|
+
export declare function translatableFieldKeys(entry: CmsEntry): string[];
|
|
52
|
+
export interface CmsEntry {
|
|
53
|
+
id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
kind: 'collection' | 'single';
|
|
56
|
+
titleField?: string;
|
|
57
|
+
drafts?: boolean;
|
|
58
|
+
fields: Record<string, FieldConfig>;
|
|
59
|
+
table?: CmsTable;
|
|
60
|
+
}
|
|
61
|
+
export type CmsConfig = Record<string, CmsEntry>;
|
|
62
|
+
export declare function defineCmsConfig<T extends CmsConfig>(config: T): T;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const MEDIA_TYPES = ["image", "video", "file"];
|
|
2
|
+
const MEDIA_EXTENSIONS = {
|
|
3
|
+
image: [
|
|
4
|
+
"apng",
|
|
5
|
+
"avif",
|
|
6
|
+
"bmp",
|
|
7
|
+
"gif",
|
|
8
|
+
"heic",
|
|
9
|
+
"ico",
|
|
10
|
+
"jpeg",
|
|
11
|
+
"jpg",
|
|
12
|
+
"png",
|
|
13
|
+
"svg",
|
|
14
|
+
"tif",
|
|
15
|
+
"tiff",
|
|
16
|
+
"webp"
|
|
17
|
+
],
|
|
18
|
+
video: ["avi", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogv", "webm"]
|
|
19
|
+
};
|
|
20
|
+
export function mediaTypeForKey(key) {
|
|
21
|
+
const dot = key.lastIndexOf(".");
|
|
22
|
+
if (dot === -1) return "file";
|
|
23
|
+
const ext = key.slice(dot + 1).toLowerCase();
|
|
24
|
+
if (MEDIA_EXTENSIONS.image.includes(ext)) return "image";
|
|
25
|
+
if (MEDIA_EXTENSIONS.video.includes(ext)) return "video";
|
|
26
|
+
return "file";
|
|
27
|
+
}
|
|
28
|
+
export function mediaFilename(key) {
|
|
29
|
+
const base = key.split("/").pop() ?? key;
|
|
30
|
+
return base.replace(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/i, "");
|
|
31
|
+
}
|
|
32
|
+
export function mediaTypeFor(mime, key) {
|
|
33
|
+
if (mime?.startsWith("image/")) return "image";
|
|
34
|
+
if (mime?.startsWith("video/")) return "video";
|
|
35
|
+
if (mime) return "file";
|
|
36
|
+
return mediaTypeForKey(key);
|
|
37
|
+
}
|
|
38
|
+
export function mediaIconFor(type) {
|
|
39
|
+
return type === "image" ? "i-lucide-image" : type === "video" ? "i-lucide-film" : "i-lucide-file";
|
|
40
|
+
}
|
|
41
|
+
export function mediaPublicUrl(baseUrl, key) {
|
|
42
|
+
return baseUrl ? `${baseUrl.replace(/\/+$/, "")}/${key}` : null;
|
|
43
|
+
}
|
|
44
|
+
export function slugify(value) {
|
|
45
|
+
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
46
|
+
}
|
|
47
|
+
export function isTranslatableField(field) {
|
|
48
|
+
return !!field.translatable && (field.type === "text" || field.type === "richtext");
|
|
49
|
+
}
|
|
50
|
+
export function translatableFieldKeys(entry) {
|
|
51
|
+
return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
52
|
+
}
|
|
53
|
+
export function defineCmsConfig(config) {
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CmsEntry, CmsI18n } from './index.js';
|
|
3
|
+
export declare const objectKeySchema: z.ZodString;
|
|
4
|
+
export interface ValidationMessages {
|
|
5
|
+
required: string;
|
|
6
|
+
invalidDate: string;
|
|
7
|
+
invalidEmail: string;
|
|
8
|
+
invalidSlug: string;
|
|
9
|
+
unknownLocale: string;
|
|
10
|
+
requiredLocale: (locale: string) => string;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildEntrySchema(entry: Pick<CmsEntry, 'fields'> & {
|
|
13
|
+
drafts?: boolean;
|
|
14
|
+
}, i18n: CmsI18n, messages?: Partial<ValidationMessages>): z.ZodObject<{
|
|
15
|
+
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isTranslatableField } from "./index.js";
|
|
3
|
+
export const objectKeySchema = z.string().min(1).max(1024).refine((k) => !k.includes("..") && !k.startsWith("/"), "Invalid object key");
|
|
4
|
+
const DEFAULT_MESSAGES = {
|
|
5
|
+
required: "Required field",
|
|
6
|
+
invalidDate: "Invalid date (yyyy-mm-dd)",
|
|
7
|
+
invalidEmail: "Invalid email",
|
|
8
|
+
invalidSlug: "Invalid slug (lowercase letters, numbers, dashes)",
|
|
9
|
+
unknownLocale: "Unknown locale key",
|
|
10
|
+
requiredLocale: (locale) => `Required field (locale '${locale}')`
|
|
11
|
+
};
|
|
12
|
+
function scalarSchema(field, m) {
|
|
13
|
+
switch (field.type) {
|
|
14
|
+
case "number":
|
|
15
|
+
return field.integer ? z.number().int() : z.number();
|
|
16
|
+
case "boolean":
|
|
17
|
+
return z.boolean();
|
|
18
|
+
case "relation":
|
|
19
|
+
return z.string().min(1);
|
|
20
|
+
case "date":
|
|
21
|
+
return z.string().regex(/^\d{4}-\d{2}-\d{2}$/, m.invalidDate);
|
|
22
|
+
case "email":
|
|
23
|
+
return z.email(m.invalidEmail);
|
|
24
|
+
case "slug":
|
|
25
|
+
return z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, m.invalidSlug);
|
|
26
|
+
case "select":
|
|
27
|
+
return z.enum(field.options);
|
|
28
|
+
case "media":
|
|
29
|
+
return objectKeySchema;
|
|
30
|
+
default:
|
|
31
|
+
return field.required ? z.string().trim().min(1, m.required) : z.string();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function optionalize(field, base) {
|
|
35
|
+
return field.required ? base : base.nullish().transform((v) => v ?? null);
|
|
36
|
+
}
|
|
37
|
+
function blocksSchema(field, m) {
|
|
38
|
+
const variants = Object.entries(field.blocks ?? {}).map(
|
|
39
|
+
([type, block]) => z.object({
|
|
40
|
+
type: z.literal(type),
|
|
41
|
+
...Object.fromEntries(
|
|
42
|
+
Object.entries(block.fields).map(([key, blockField]) => [
|
|
43
|
+
key,
|
|
44
|
+
optionalize(blockField, scalarSchema(blockField, m))
|
|
45
|
+
])
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
return z.array(z.discriminatedUnion("type", variants));
|
|
50
|
+
}
|
|
51
|
+
export function buildEntrySchema(entry, i18n, messages) {
|
|
52
|
+
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
53
|
+
const { locales, defaultLocale } = i18n;
|
|
54
|
+
const shape = {};
|
|
55
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
56
|
+
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
57
|
+
const list = z.array(z.string().min(1));
|
|
58
|
+
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (field.type === "blocks") {
|
|
62
|
+
const list = blocksSchema(field, m);
|
|
63
|
+
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? null);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (isTranslatableField(field) && locales.length) {
|
|
67
|
+
const record = z.record(z.string(), z.string()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
|
|
68
|
+
shape[key] = field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (field.type === "json") {
|
|
72
|
+
const json = z.unknown();
|
|
73
|
+
shape[key] = field.required ? json.refine((v) => v !== void 0 && v !== null, m.required) : json.nullish().transform((v) => v ?? null);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
shape[key] = optionalize(field, scalarSchema(field, m));
|
|
77
|
+
}
|
|
78
|
+
if (entry.drafts) {
|
|
79
|
+
shape.status = z.enum(["draft", "published"]).nullish().transform((v) => v ?? void 0);
|
|
80
|
+
}
|
|
81
|
+
return z.object(shape);
|
|
82
|
+
}
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xleddyl/nuxt-cms",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/xleddyl/nuxt-cms"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/xleddyl/nuxt-cms#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/xleddyl/nuxt-cms/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nuxt",
|
|
17
|
+
"nuxt-module",
|
|
18
|
+
"nitro",
|
|
19
|
+
"cms",
|
|
20
|
+
"headless-cms",
|
|
21
|
+
"self-hosted",
|
|
22
|
+
"sqlite",
|
|
23
|
+
"postgres",
|
|
24
|
+
"drizzle",
|
|
25
|
+
"graphql"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.19.0"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/types.d.mts",
|
|
38
|
+
"import": "./dist/module.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"main": "./dist/module.mjs",
|
|
42
|
+
"typesVersions": {
|
|
43
|
+
"*": {
|
|
44
|
+
".": [
|
|
45
|
+
"./dist/types.d.mts"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@nuxt/kit": "^4.4.8",
|
|
54
|
+
"@nuxt/ui": "^4.0.0",
|
|
55
|
+
"@nuxtjs/i18n": "^10.4.0",
|
|
56
|
+
"@tiptap/starter-kit": "^3.0.0",
|
|
57
|
+
"@tiptap/vue-3": "^3.0.0",
|
|
58
|
+
"aws4fetch": "^1.0.20",
|
|
59
|
+
"better-sqlite3": "^12.2.0",
|
|
60
|
+
"drizzle-kit": "^0.31.0",
|
|
61
|
+
"drizzle-orm": "^0.44.0",
|
|
62
|
+
"gql.tada": "^1.11.2",
|
|
63
|
+
"graphql": "^16.11.0",
|
|
64
|
+
"graphql-yoga": "^5.10.0",
|
|
65
|
+
"jiti": "^2.7.0",
|
|
66
|
+
"nuxt-auth-utils": "^0.5.29",
|
|
67
|
+
"pg": "^8.16.0",
|
|
68
|
+
"zod": "^4.0.0"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@nuxt/devtools": "^3.2.4",
|
|
72
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
73
|
+
"@nuxt/schema": "^4.4.8",
|
|
74
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
75
|
+
"@types/node": "latest",
|
|
76
|
+
"@types/pg": "^8.11.0",
|
|
77
|
+
"changelogen": "^0.6.0",
|
|
78
|
+
"nuxt": "^4.4.8",
|
|
79
|
+
"prettier": "3.1.1",
|
|
80
|
+
"prettier-plugin-tailwindcss": "0.6.11",
|
|
81
|
+
"typescript": "~6.0.3",
|
|
82
|
+
"vue-tsc": "^3.3.3"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"dev": "pnpm dev:prepare && nuxt dev playground",
|
|
86
|
+
"dev:build": "nuxt build playground",
|
|
87
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
88
|
+
"db:studio": "drizzle-kit studio --config=playground/.nuxt/cms/drizzle.config.ts",
|
|
89
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
90
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,html,json,sql,vue}\" --log-level error",
|
|
91
|
+
"release": "changelogen --release --push"
|
|
92
|
+
}
|
|
93
|
+
}
|