@xleddyl/nuxt-cms 0.1.9 → 0.1.11
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 +61 -1
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +18 -7
- package/dist/runtime/app/components/cms/BlocksField.vue +59 -16
- package/dist/runtime/app/components/cms/ConfirmModal.d.vue.ts +20 -0
- package/dist/runtime/app/components/cms/ConfirmModal.vue +43 -0
- package/dist/runtime/app/components/cms/ConfirmModal.vue.d.ts +20 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue +9 -2
- package/dist/runtime/app/components/cms/RelationField.vue +56 -10
- package/dist/runtime/app/components/cms/RichTextField.vue +63 -2
- package/dist/runtime/app/composables/cms-confirm.d.ts +4 -0
- package/dist/runtime/app/composables/cms-confirm.js +7 -0
- package/dist/runtime/app/i18n/en.json +32 -2
- package/dist/runtime/app/i18n/it.json +32 -2
- package/dist/runtime/app/pages/admin-collection.vue +89 -14
- package/dist/runtime/app/pages/admin-entry.vue +12 -5
- package/dist/runtime/server/api/collection.get.d.ts +1 -3
- package/dist/runtime/server/api/collection.get.js +34 -8
- package/dist/runtime/server/api/collection.post.js +11 -7
- package/dist/runtime/server/api/collection.put.js +11 -7
- package/dist/runtime/server/api/item.get.js +3 -2
- package/dist/runtime/server/api/item.put.js +12 -8
- package/dist/runtime/server/api/media-presign.post.js +2 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/plugins/migrate-libsql.d.ts +2 -0
- package/dist/runtime/server/plugins/migrate-libsql.js +17 -0
- package/dist/runtime/server/plugins/migrate-postgres.js +9 -1
- package/dist/runtime/server/plugins/migrate-sqlite.js +9 -1
- package/dist/runtime/server/routes/auth/login.post.js +1 -1
- package/dist/runtime/server/utils/db-libsql.d.ts +7 -0
- package/dist/runtime/server/utils/db-libsql.js +18 -0
- package/dist/runtime/server/utils/db-postgres.d.ts +4 -0
- package/dist/runtime/server/utils/db-postgres.js +3 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
- package/dist/runtime/server/utils/db-sqlite.js +19 -0
- package/dist/runtime/server/utils/graphql.js +10 -4
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +27 -0
- package/dist/runtime/server/utils/relations.d.ts +3 -2
- package/dist/runtime/server/utils/relations.js +2 -4
- package/dist/runtime/server/utils/require-admin.js +15 -1
- package/dist/runtime/shared/index.d.ts +69 -1
- package/package.json +7 -1
|
@@ -2,3 +2,5 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
export declare function useDb(): import("drizzle-orm/better-sqlite3").BetterSQLite3Database<Record<string, unknown>> & {
|
|
3
3
|
$client: Database.Database;
|
|
4
4
|
};
|
|
5
|
+
export type CmsDb = ReturnType<typeof useDb>;
|
|
6
|
+
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
import { sql } from "drizzle-orm";
|
|
4
5
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
5
6
|
import { useRuntimeConfig } from "#imports";
|
|
6
7
|
let _db = null;
|
|
@@ -15,3 +16,21 @@ export function useDb() {
|
|
|
15
16
|
}
|
|
16
17
|
return _db;
|
|
17
18
|
}
|
|
19
|
+
let txQueue = Promise.resolve();
|
|
20
|
+
export function withTransaction(fn) {
|
|
21
|
+
const db = useDb();
|
|
22
|
+
const run = txQueue.then(async () => {
|
|
23
|
+
await db.run(sql`begin immediate`);
|
|
24
|
+
try {
|
|
25
|
+
const result = await fn(db);
|
|
26
|
+
await db.run(sql`commit`);
|
|
27
|
+
return result;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
await db.run(sql`rollback`);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
txQueue = run.catch(() => {
|
|
34
|
+
});
|
|
35
|
+
return run;
|
|
36
|
+
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
inArray,
|
|
9
9
|
isNotNull,
|
|
10
10
|
isNull,
|
|
11
|
-
like,
|
|
12
11
|
lt,
|
|
13
12
|
lte,
|
|
14
13
|
ne,
|
|
@@ -100,15 +99,22 @@ const OPERATORS = {
|
|
|
100
99
|
gte: (column, value) => gte(column, value),
|
|
101
100
|
lt: (column, value) => lt(column, value),
|
|
102
101
|
lte: (column, value) => lte(column, value),
|
|
103
|
-
like: (column, value) =>
|
|
102
|
+
like: (column, value) => {
|
|
103
|
+
const pattern = String(value).replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_").replaceAll("*", "%");
|
|
104
|
+
return sql`${column} like ${pattern} escape '\\'`;
|
|
105
|
+
},
|
|
104
106
|
in: (column, value) => inArray(column, value)
|
|
105
107
|
};
|
|
108
|
+
function columnFor(table, key) {
|
|
109
|
+
const columns = tableColumns(table);
|
|
110
|
+
return Object.hasOwn(columns, key) ? columns[key] : void 0;
|
|
111
|
+
}
|
|
106
112
|
function filterConditions(table, filters) {
|
|
107
113
|
if (!filters) return [];
|
|
108
114
|
const conditions = [];
|
|
109
115
|
for (const [key, ops] of Object.entries(filters)) {
|
|
110
116
|
if (!ops) continue;
|
|
111
|
-
const column =
|
|
117
|
+
const column = columnFor(table, key);
|
|
112
118
|
if (!column) throw new GraphQLError(`Unknown filter field: ${key}`);
|
|
113
119
|
for (const [op, value] of Object.entries(ops)) {
|
|
114
120
|
if (value === void 0) continue;
|
|
@@ -130,7 +136,7 @@ function filterConditions(table, filters) {
|
|
|
130
136
|
function sortOrder(table, sort) {
|
|
131
137
|
const order = [];
|
|
132
138
|
for (const part of sort ?? []) {
|
|
133
|
-
const column =
|
|
139
|
+
const column = columnFor(table, part.field);
|
|
134
140
|
if (!column) throw new GraphQLError(`Unknown sort field: ${part.field}`);
|
|
135
141
|
order.push(part.direction === "desc" ? desc(column) : asc(column));
|
|
136
142
|
}
|
|
@@ -9,6 +9,7 @@ interface MediaConfig {
|
|
|
9
9
|
accessKeyId: string;
|
|
10
10
|
secretAccessKey: string;
|
|
11
11
|
}
|
|
12
|
+
export declare function assertUploadContentType(contentType: string): void;
|
|
12
13
|
export declare function encodeKey(key: string): string;
|
|
13
14
|
export declare function useMediaConfig(event: H3Event): {
|
|
14
15
|
media: MediaConfig;
|
|
@@ -2,6 +2,33 @@ import { AwsClient } from "aws4fetch";
|
|
|
2
2
|
import { createError } from "h3";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
4
|
import { mediaPublicUrl, mediaTypeFor } from "../../shared/index.js";
|
|
5
|
+
const UPLOAD_TYPE_PREFIXES = ["image/", "video/", "audio/", "font/"];
|
|
6
|
+
const UPLOAD_TYPE_BLOCKLIST = /* @__PURE__ */ new Set(["image/svg+xml"]);
|
|
7
|
+
const UPLOAD_TYPES = /* @__PURE__ */ new Set([
|
|
8
|
+
"application/pdf",
|
|
9
|
+
"application/zip",
|
|
10
|
+
"application/gzip",
|
|
11
|
+
"application/json",
|
|
12
|
+
"text/plain",
|
|
13
|
+
"text/csv",
|
|
14
|
+
"text/markdown",
|
|
15
|
+
"application/msword",
|
|
16
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17
|
+
"application/vnd.ms-excel",
|
|
18
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
19
|
+
"application/vnd.ms-powerpoint",
|
|
20
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
21
|
+
]);
|
|
22
|
+
export function assertUploadContentType(contentType) {
|
|
23
|
+
const type = contentType.toLowerCase();
|
|
24
|
+
const allowed = !UPLOAD_TYPE_BLOCKLIST.has(type) && (UPLOAD_TYPES.has(type) || UPLOAD_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix)));
|
|
25
|
+
if (!allowed) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 415,
|
|
28
|
+
statusMessage: `Unsupported content type: ${contentType}`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
5
32
|
export function encodeKey(key) {
|
|
6
33
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
7
34
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CmsDb } from '#cms-db';
|
|
1
2
|
import type { CmsEntry } from '../../shared/index.js';
|
|
2
3
|
type Row = Record<string, unknown>;
|
|
3
4
|
export declare function splitRelationValues(entry: CmsEntry, body: Row): {
|
|
@@ -7,6 +8,6 @@ export declare function splitRelationValues(entry: CmsEntry, body: Row): {
|
|
|
7
8
|
lists: Record<string, string[]>;
|
|
8
9
|
};
|
|
9
10
|
export declare function assertRelationTargets(entry: CmsEntry, lists: Record<string, string[]>): Promise<void>;
|
|
10
|
-
export declare function saveManyToMany(name: string, sourceId: string, lists: Record<string, string[]>): Promise<void>;
|
|
11
|
-
export declare function attachManyToMany<T extends Row>(name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
|
|
11
|
+
export declare function saveManyToMany(db: CmsDb, name: string, sourceId: string, lists: Record<string, string[]>): Promise<void>;
|
|
12
|
+
export declare function attachManyToMany<T extends Row>(db: CmsDb, name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
|
|
12
13
|
export {};
|
|
@@ -48,8 +48,7 @@ export async function assertRelationTargets(entry, lists) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
export async function saveManyToMany(name, sourceId, lists) {
|
|
52
|
-
const db = useDb();
|
|
51
|
+
export async function saveManyToMany(db, name, sourceId, lists) {
|
|
53
52
|
for (const [key, ids] of Object.entries(lists)) {
|
|
54
53
|
const join = joinTable(name, key);
|
|
55
54
|
await db.delete(join.table).where(eq(join.sourceId, sourceId));
|
|
@@ -58,11 +57,10 @@ export async function saveManyToMany(name, sourceId, lists) {
|
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
|
-
export async function attachManyToMany(name, entry, rows) {
|
|
60
|
+
export async function attachManyToMany(db, name, entry, rows) {
|
|
62
61
|
const keys = manyToManyKeys(entry);
|
|
63
62
|
const ids = rows.map((row) => row.id).filter((id) => id != null);
|
|
64
63
|
if (!keys.length || !ids.length) return rows;
|
|
65
|
-
const db = useDb();
|
|
66
64
|
for (const key of keys) {
|
|
67
65
|
const join = joinTable(name, key);
|
|
68
66
|
const links = await db.select().from(join.table).where(inArray(join.sourceId, ids)).orderBy(asc(join.position));
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { createError } from "h3";
|
|
1
|
+
import { createError, getRequestHeader, getRequestHost } from "h3";
|
|
2
2
|
import { getUserSession, useRuntimeConfig } from "#imports";
|
|
3
|
+
function assertSameOrigin(event) {
|
|
4
|
+
const origin = getRequestHeader(event, "origin");
|
|
5
|
+
if (!origin) return;
|
|
6
|
+
let originHost;
|
|
7
|
+
try {
|
|
8
|
+
originHost = new URL(origin).host;
|
|
9
|
+
} catch {
|
|
10
|
+
originHost = void 0;
|
|
11
|
+
}
|
|
12
|
+
if (!originHost || originHost !== getRequestHost(event, { xForwardedHost: true })) {
|
|
13
|
+
throw createError({ statusCode: 403, statusMessage: "Cross-origin request rejected" });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
3
16
|
export async function requireAdmin(event) {
|
|
17
|
+
assertSameOrigin(event);
|
|
4
18
|
const session = await getUserSession(event);
|
|
5
19
|
const email = session.user?.email;
|
|
6
20
|
if (!email) {
|
|
@@ -59,4 +59,72 @@ export interface CmsEntry {
|
|
|
59
59
|
table?: CmsTable;
|
|
60
60
|
}
|
|
61
61
|
export type CmsConfig = Record<string, CmsEntry>;
|
|
62
|
-
|
|
62
|
+
interface FieldInputBase {
|
|
63
|
+
label: string;
|
|
64
|
+
required?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface TextFieldInput extends FieldInputBase {
|
|
67
|
+
type: 'text';
|
|
68
|
+
textarea?: boolean;
|
|
69
|
+
translatable?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface RichtextFieldInput extends FieldInputBase {
|
|
72
|
+
type: 'richtext';
|
|
73
|
+
translatable?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface NumberFieldInput extends FieldInputBase {
|
|
76
|
+
type: 'number';
|
|
77
|
+
integer?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface BooleanFieldInput extends FieldInputBase {
|
|
80
|
+
type: 'boolean';
|
|
81
|
+
}
|
|
82
|
+
export interface DateFieldInput extends FieldInputBase {
|
|
83
|
+
type: 'date';
|
|
84
|
+
}
|
|
85
|
+
export interface EmailFieldInput extends FieldInputBase {
|
|
86
|
+
type: 'email';
|
|
87
|
+
}
|
|
88
|
+
export interface SlugFieldInput extends FieldInputBase {
|
|
89
|
+
type: 'slug';
|
|
90
|
+
from: string;
|
|
91
|
+
}
|
|
92
|
+
export interface SelectFieldInput extends FieldInputBase {
|
|
93
|
+
type: 'select';
|
|
94
|
+
options: string[];
|
|
95
|
+
}
|
|
96
|
+
export interface JsonFieldInput extends FieldInputBase {
|
|
97
|
+
type: 'json';
|
|
98
|
+
}
|
|
99
|
+
export interface MediaFieldInput extends FieldInputBase {
|
|
100
|
+
type: 'media';
|
|
101
|
+
mediaType?: MediaType;
|
|
102
|
+
accept?: string[];
|
|
103
|
+
}
|
|
104
|
+
export interface RelationFieldInput extends FieldInputBase {
|
|
105
|
+
type: 'relation';
|
|
106
|
+
to: string;
|
|
107
|
+
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
108
|
+
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
109
|
+
}
|
|
110
|
+
export type BlockFieldInput = Omit<TextFieldInput, 'translatable'> | Omit<RichtextFieldInput, 'translatable'> | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput;
|
|
111
|
+
export interface BlockInput {
|
|
112
|
+
label: string;
|
|
113
|
+
fields: Record<string, BlockFieldInput>;
|
|
114
|
+
}
|
|
115
|
+
export interface BlocksFieldInput extends FieldInputBase {
|
|
116
|
+
type: 'blocks';
|
|
117
|
+
blocks: Record<string, BlockInput>;
|
|
118
|
+
}
|
|
119
|
+
export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
|
|
120
|
+
export interface CmsEntryInput {
|
|
121
|
+
id: string;
|
|
122
|
+
label: string;
|
|
123
|
+
kind: 'collection' | 'single';
|
|
124
|
+
titleField?: string;
|
|
125
|
+
drafts?: boolean;
|
|
126
|
+
fields: Record<string, CmsFieldInput>;
|
|
127
|
+
}
|
|
128
|
+
export type CmsConfigInput = Record<string, CmsEntryInput>;
|
|
129
|
+
export declare function defineCmsConfig<T extends CmsConfigInput>(config: T): T;
|
|
130
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"headless-cms",
|
|
21
21
|
"self-hosted",
|
|
22
22
|
"sqlite",
|
|
23
|
+
"libsql",
|
|
24
|
+
"turso",
|
|
23
25
|
"postgres",
|
|
24
26
|
"drizzle",
|
|
25
27
|
"graphql"
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"dist"
|
|
51
53
|
],
|
|
52
54
|
"dependencies": {
|
|
55
|
+
"@libsql/client": "^0.17.4",
|
|
53
56
|
"@nuxt/kit": "^4.4.8",
|
|
54
57
|
"@nuxt/ui": "^4.0.0",
|
|
55
58
|
"@nuxtjs/i18n": "^10.4.0",
|
|
@@ -75,10 +78,12 @@
|
|
|
75
78
|
"@types/node": "latest",
|
|
76
79
|
"@types/pg": "^8.11.0",
|
|
77
80
|
"changelogen": "^0.6.0",
|
|
81
|
+
"h3": "^1.15.11",
|
|
78
82
|
"nuxt": "^4.4.8",
|
|
79
83
|
"prettier": "3.1.1",
|
|
80
84
|
"prettier-plugin-tailwindcss": "0.6.11",
|
|
81
85
|
"typescript": "~6.0.3",
|
|
86
|
+
"vitest": "^4.1.9",
|
|
82
87
|
"vue-tsc": "^3.3.3"
|
|
83
88
|
},
|
|
84
89
|
"scripts": {
|
|
@@ -86,6 +91,7 @@
|
|
|
86
91
|
"dev:build": "nuxt build playground",
|
|
87
92
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
88
93
|
"db:studio": "drizzle-kit studio --config=playground/.nuxt/cms/drizzle.config.ts",
|
|
94
|
+
"test": "vitest run",
|
|
89
95
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
90
96
|
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,html,json,sql,vue}\" --log-level error",
|
|
91
97
|
"release": "changelogen --release --push"
|