@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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useOverlay } from "#imports";
|
|
2
|
+
import ConfirmModal from "../components/cms/ConfirmModal.vue";
|
|
3
|
+
export function useCmsConfirm() {
|
|
4
|
+
const overlay = useOverlay();
|
|
5
|
+
const modal = overlay.create(ConfirmModal);
|
|
6
|
+
return async (message, options) => await modal.open({ message, ...options }) === true;
|
|
7
|
+
}
|
|
@@ -27,7 +27,16 @@
|
|
|
27
27
|
"emptyBody": "Entries you create will show up here.",
|
|
28
28
|
"deleteConfirm": "Delete this row?",
|
|
29
29
|
"status": "Status",
|
|
30
|
-
"columns": "Columns"
|
|
30
|
+
"columns": "Columns",
|
|
31
|
+
"loadError": "Could not load entries",
|
|
32
|
+
"retry": "Retry",
|
|
33
|
+
"searchPlaceholder": "Search…",
|
|
34
|
+
"noResults": "No matching entries"
|
|
35
|
+
},
|
|
36
|
+
"confirm": {
|
|
37
|
+
"title": "Are you sure?",
|
|
38
|
+
"confirm": "Confirm",
|
|
39
|
+
"cancel": "Cancel"
|
|
31
40
|
},
|
|
32
41
|
"entry": {
|
|
33
42
|
"unsavedChanges": "You have unsaved changes. Leave anyway?"
|
|
@@ -79,7 +88,28 @@
|
|
|
79
88
|
"mediaChange": "Change",
|
|
80
89
|
"mediaRemove": "Remove",
|
|
81
90
|
"invalidJson": "Invalid JSON",
|
|
82
|
-
"addBlock": "Add block"
|
|
91
|
+
"addBlock": "Add block",
|
|
92
|
+
"moveUp": "Move up",
|
|
93
|
+
"moveDown": "Move down",
|
|
94
|
+
"duplicateBlock": "Duplicate block",
|
|
95
|
+
"removeBlock": "Remove block",
|
|
96
|
+
"collapseBlock": "Collapse block",
|
|
97
|
+
"expandBlock": "Expand block"
|
|
98
|
+
},
|
|
99
|
+
"richtext": {
|
|
100
|
+
"bold": "Bold",
|
|
101
|
+
"italic": "Italic",
|
|
102
|
+
"strike": "Strikethrough",
|
|
103
|
+
"h2": "Heading 2",
|
|
104
|
+
"h3": "Heading 3",
|
|
105
|
+
"bulletList": "Bullet list",
|
|
106
|
+
"orderedList": "Numbered list",
|
|
107
|
+
"blockquote": "Quote",
|
|
108
|
+
"link": "Link",
|
|
109
|
+
"setLink": "Apply",
|
|
110
|
+
"unsetLink": "Remove link",
|
|
111
|
+
"undo": "Undo",
|
|
112
|
+
"redo": "Redo"
|
|
83
113
|
},
|
|
84
114
|
"validation": {
|
|
85
115
|
"required": "Required field",
|
|
@@ -27,7 +27,16 @@
|
|
|
27
27
|
"emptyBody": "Le voci che crei appariranno qui.",
|
|
28
28
|
"deleteConfirm": "Eliminare questa voce?",
|
|
29
29
|
"status": "Stato",
|
|
30
|
-
"columns": "Colonne"
|
|
30
|
+
"columns": "Colonne",
|
|
31
|
+
"loadError": "Impossibile caricare le voci",
|
|
32
|
+
"retry": "Riprova",
|
|
33
|
+
"searchPlaceholder": "Cerca…",
|
|
34
|
+
"noResults": "Nessuna voce corrispondente"
|
|
35
|
+
},
|
|
36
|
+
"confirm": {
|
|
37
|
+
"title": "Sei sicuro?",
|
|
38
|
+
"confirm": "Conferma",
|
|
39
|
+
"cancel": "Annulla"
|
|
31
40
|
},
|
|
32
41
|
"entry": {
|
|
33
42
|
"unsavedChanges": "Ci sono modifiche non salvate. Uscire comunque?"
|
|
@@ -79,7 +88,28 @@
|
|
|
79
88
|
"mediaChange": "Cambia",
|
|
80
89
|
"mediaRemove": "Rimuovi",
|
|
81
90
|
"invalidJson": "JSON non valido",
|
|
82
|
-
"addBlock": "Aggiungi blocco"
|
|
91
|
+
"addBlock": "Aggiungi blocco",
|
|
92
|
+
"moveUp": "Sposta su",
|
|
93
|
+
"moveDown": "Sposta giù",
|
|
94
|
+
"duplicateBlock": "Duplica blocco",
|
|
95
|
+
"removeBlock": "Rimuovi blocco",
|
|
96
|
+
"collapseBlock": "Comprimi blocco",
|
|
97
|
+
"expandBlock": "Espandi blocco"
|
|
98
|
+
},
|
|
99
|
+
"richtext": {
|
|
100
|
+
"bold": "Grassetto",
|
|
101
|
+
"italic": "Corsivo",
|
|
102
|
+
"strike": "Barrato",
|
|
103
|
+
"h2": "Titolo 2",
|
|
104
|
+
"h3": "Titolo 3",
|
|
105
|
+
"bulletList": "Elenco puntato",
|
|
106
|
+
"orderedList": "Elenco numerato",
|
|
107
|
+
"blockquote": "Citazione",
|
|
108
|
+
"link": "Link",
|
|
109
|
+
"setLink": "Applica",
|
|
110
|
+
"unsetLink": "Rimuovi link",
|
|
111
|
+
"undo": "Annulla",
|
|
112
|
+
"redo": "Ripristina"
|
|
83
113
|
},
|
|
84
114
|
"validation": {
|
|
85
115
|
"required": "Campo obbligatorio",
|
|
@@ -32,7 +32,22 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
</CmsPageHeader>
|
|
34
34
|
|
|
35
|
-
<
|
|
35
|
+
<CmsEmptyState
|
|
36
|
+
v-if="error"
|
|
37
|
+
icon="i-lucide-triangle-alert"
|
|
38
|
+
:title="t('cms.collection.loadError')"
|
|
39
|
+
>
|
|
40
|
+
<UButton
|
|
41
|
+
:label="t('cms.collection.retry')"
|
|
42
|
+
icon="i-lucide-refresh-cw"
|
|
43
|
+
variant="subtle"
|
|
44
|
+
class="mt-3 rounded-full px-4"
|
|
45
|
+
:loading="status === 'pending'"
|
|
46
|
+
@click="reload"
|
|
47
|
+
/>
|
|
48
|
+
</CmsEmptyState>
|
|
49
|
+
|
|
50
|
+
<div v-else-if="config.kind === 'single'" class="cms-card p-7">
|
|
36
51
|
<CmsEntryForm
|
|
37
52
|
v-model="formState"
|
|
38
53
|
:fields="config.fields"
|
|
@@ -43,6 +58,14 @@
|
|
|
43
58
|
</div>
|
|
44
59
|
|
|
45
60
|
<template v-else>
|
|
61
|
+
<UInput
|
|
62
|
+
v-if="total || searchTerm"
|
|
63
|
+
v-model="search"
|
|
64
|
+
icon="i-lucide-search"
|
|
65
|
+
:placeholder="t('cms.collection.searchPlaceholder')"
|
|
66
|
+
class="max-w-xs"
|
|
67
|
+
/>
|
|
68
|
+
|
|
46
69
|
<div v-if="rows.length" class="cms-card overflow-hidden">
|
|
47
70
|
<UTable
|
|
48
71
|
v-model:column-visibility="columnVisibility"
|
|
@@ -81,6 +104,19 @@
|
|
|
81
104
|
</UTable>
|
|
82
105
|
</div>
|
|
83
106
|
|
|
107
|
+
<div v-else-if="status === 'pending'" class="flex justify-center py-10">
|
|
108
|
+
<UIcon
|
|
109
|
+
name="i-lucide-loader-circle"
|
|
110
|
+
class="text-(--ui-text-dimmed) size-6 animate-spin"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<CmsEmptyState
|
|
115
|
+
v-else-if="searchTerm"
|
|
116
|
+
icon="i-lucide-search-x"
|
|
117
|
+
:title="t('cms.collection.noResults')"
|
|
118
|
+
/>
|
|
119
|
+
|
|
84
120
|
<CmsEmptyState
|
|
85
121
|
v-else
|
|
86
122
|
icon="i-lucide-sprout"
|
|
@@ -95,6 +131,10 @@
|
|
|
95
131
|
@click="openCreate"
|
|
96
132
|
/>
|
|
97
133
|
</CmsEmptyState>
|
|
134
|
+
|
|
135
|
+
<div v-if="total > PAGE_SIZE" class="flex justify-center">
|
|
136
|
+
<UPagination v-model:page="page" :total="total" :items-per-page="PAGE_SIZE" />
|
|
137
|
+
</div>
|
|
98
138
|
</template>
|
|
99
139
|
</div>
|
|
100
140
|
</template>
|
|
@@ -115,6 +155,7 @@ import {
|
|
|
115
155
|
watch
|
|
116
156
|
} from "#imports";
|
|
117
157
|
import cmsConfig from "#cms-config";
|
|
158
|
+
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
118
159
|
import { useCmsRuntime } from "../composables/cms-runtime";
|
|
119
160
|
import { cmsApi } from "../utils/api";
|
|
120
161
|
import { errorMessage } from "../utils/ui";
|
|
@@ -137,8 +178,37 @@ const fieldKeys = Object.keys(config.fields);
|
|
|
137
178
|
const drafts = config.kind === "collection" && !!config.drafts;
|
|
138
179
|
const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
|
|
139
180
|
const endpoint = `/api/cms/admin/${name}`;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
181
|
+
const PAGE_SIZE = 25;
|
|
182
|
+
const page = ref(1);
|
|
183
|
+
const search = ref("");
|
|
184
|
+
const searchTerm = ref("");
|
|
185
|
+
let searchTimer;
|
|
186
|
+
watch(search, (value) => {
|
|
187
|
+
clearTimeout(searchTimer);
|
|
188
|
+
searchTimer = setTimeout(() => {
|
|
189
|
+
searchTerm.value = value.trim();
|
|
190
|
+
page.value = 1;
|
|
191
|
+
}, 300);
|
|
192
|
+
});
|
|
193
|
+
const listQuery = computed(() => ({
|
|
194
|
+
limit: PAGE_SIZE,
|
|
195
|
+
offset: (page.value - 1) * PAGE_SIZE,
|
|
196
|
+
...searchTerm.value ? { search: searchTerm.value } : {}
|
|
197
|
+
}));
|
|
198
|
+
const { data, refresh, error, status } = await useFetch(endpoint, {
|
|
199
|
+
query: config.kind === "collection" ? listQuery : void 0
|
|
200
|
+
});
|
|
201
|
+
function isList(value) {
|
|
202
|
+
return !!value && Array.isArray(value.items);
|
|
203
|
+
}
|
|
204
|
+
const rows = computed(() => isList(data.value) ? data.value.items : []);
|
|
205
|
+
const total = computed(() => isList(data.value) ? data.value.total : 0);
|
|
206
|
+
const reload = async () => {
|
|
207
|
+
await refresh();
|
|
208
|
+
if (config.kind === "single" && !error.value) {
|
|
209
|
+
formState.value = data.value && !isList(data.value) ? pickFields(data.value) : emptyState();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
142
212
|
const tableUi = {
|
|
143
213
|
thead: "border-b border-(--cms-line)",
|
|
144
214
|
th: "cms-kicker px-5 py-3 font-normal",
|
|
@@ -154,15 +224,16 @@ function pickFields(row) {
|
|
|
154
224
|
return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
|
|
155
225
|
}
|
|
156
226
|
async function submit(action) {
|
|
227
|
+
if (saving.value) return false;
|
|
157
228
|
saving.value = true;
|
|
158
229
|
try {
|
|
159
230
|
await action();
|
|
160
231
|
await refresh();
|
|
161
232
|
return true;
|
|
162
|
-
} catch (
|
|
233
|
+
} catch (error2) {
|
|
163
234
|
toast.add({
|
|
164
235
|
title: t("cms.toast.saveFailed"),
|
|
165
|
-
description: errorMessage(
|
|
236
|
+
description: errorMessage(error2),
|
|
166
237
|
color: "error"
|
|
167
238
|
});
|
|
168
239
|
return false;
|
|
@@ -171,7 +242,7 @@ async function submit(action) {
|
|
|
171
242
|
}
|
|
172
243
|
}
|
|
173
244
|
if (config.kind === "single") {
|
|
174
|
-
formState.value = data.value && !
|
|
245
|
+
formState.value = data.value && !isList(data.value) ? pickFields(data.value) : emptyState();
|
|
175
246
|
}
|
|
176
247
|
async function saveSingle() {
|
|
177
248
|
const ok = await submit(() => cmsApi(endpoint, { method: "PUT", body: formState.value }));
|
|
@@ -249,25 +320,29 @@ const columnItems = computed(
|
|
|
249
320
|
}))
|
|
250
321
|
);
|
|
251
322
|
const kicker = computed(
|
|
252
|
-
() => config.kind === "single" ? t("cms.collection.kickerSingle") : t("cms.collection.kicker",
|
|
323
|
+
() => config.kind === "single" ? t("cms.collection.kickerSingle") : t("cms.collection.kicker", total.value)
|
|
253
324
|
);
|
|
254
325
|
function onSelect(_event, row) {
|
|
255
326
|
navigateTo(`/cms/${name}/${row.original.id}`);
|
|
256
327
|
}
|
|
257
328
|
async function toggleStatus(row) {
|
|
258
329
|
const next = row.status === "published" ? "draft" : "published";
|
|
259
|
-
await submit(
|
|
260
|
-
|
|
330
|
+
await submit(async () => {
|
|
331
|
+
const fresh = await cmsApi(`${endpoint}/${row.id}`);
|
|
332
|
+
await cmsApi(`${endpoint}/${row.id}`, {
|
|
261
333
|
method: "PUT",
|
|
262
|
-
body: { ...pickFields(
|
|
263
|
-
})
|
|
264
|
-
);
|
|
334
|
+
body: { ...pickFields(fresh), status: next }
|
|
335
|
+
});
|
|
336
|
+
});
|
|
265
337
|
}
|
|
266
338
|
function openCreate() {
|
|
267
339
|
navigateTo(`/cms/${name}/new`);
|
|
268
340
|
}
|
|
341
|
+
const confirmAction = useCmsConfirm();
|
|
269
342
|
async function deleteRow(row) {
|
|
270
|
-
if (!
|
|
271
|
-
|
|
343
|
+
if (!await confirmAction(t("cms.collection.deleteConfirm"))) return;
|
|
344
|
+
const lastOnPage = rows.value.length === 1 && page.value > 1;
|
|
345
|
+
const ok = await submit(() => cmsApi(`${endpoint}/${row.id}`, { method: "DELETE" }));
|
|
346
|
+
if (ok && lastOnPage) page.value -= 1;
|
|
272
347
|
}
|
|
273
348
|
</script>
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
watch
|
|
70
70
|
} from "#imports";
|
|
71
71
|
import cmsConfig from "#cms-config";
|
|
72
|
+
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
72
73
|
import { cmsApi } from "../utils/api";
|
|
73
74
|
import { errorMessage } from "../utils/ui";
|
|
74
75
|
const FORM_ID = "cms-entry-form";
|
|
@@ -100,9 +101,14 @@ function pickFields(row) {
|
|
|
100
101
|
}
|
|
101
102
|
const formState = ref(emptyState());
|
|
102
103
|
if (!isNew) {
|
|
103
|
-
const { data } = await useFetch(`${endpoint}/${id}`);
|
|
104
|
-
if (!data.value) {
|
|
105
|
-
|
|
104
|
+
const { data, error } = await useFetch(`${endpoint}/${id}`);
|
|
105
|
+
if (error.value || !data.value) {
|
|
106
|
+
const statusCode = error.value?.statusCode ?? 404;
|
|
107
|
+
throw createError({
|
|
108
|
+
statusCode,
|
|
109
|
+
statusMessage: statusCode === 404 ? "Entry not found" : "Failed to load entry",
|
|
110
|
+
fatal: true
|
|
111
|
+
});
|
|
106
112
|
}
|
|
107
113
|
formState.value = pickFields(data.value);
|
|
108
114
|
}
|
|
@@ -117,8 +123,9 @@ watch(dirty, (value) => {
|
|
|
117
123
|
else window.removeEventListener("beforeunload", onBeforeUnload);
|
|
118
124
|
});
|
|
119
125
|
onBeforeUnmount(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
const confirmAction = useCmsConfirm();
|
|
127
|
+
onBeforeRouteLeave(async () => {
|
|
128
|
+
if (dirty.value && !await confirmAction(t("cms.entry.unsavedChanges"))) return false;
|
|
122
129
|
});
|
|
123
130
|
const published = computed({
|
|
124
131
|
get: () => formState.value.status === "published",
|
|
@@ -1,19 +1,45 @@
|
|
|
1
|
-
import { desc } from "drizzle-orm";
|
|
2
|
-
import { defineEventHandler } from "h3";
|
|
1
|
+
import { count, desc, sql } from "drizzle-orm";
|
|
2
|
+
import { defineEventHandler, getValidatedQuery } from "h3";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { useDb } from "#cms-db";
|
|
4
5
|
import { getRegistryEntry, idColumn, tableColumns } from "../utils/registry.js";
|
|
5
6
|
import { attachManyToMany } from "../utils/relations.js";
|
|
6
7
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
8
|
+
const querySchema = z.object({
|
|
9
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
10
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
11
|
+
search: z.string().trim().max(200).optional(),
|
|
12
|
+
light: z.stringbool().default(false)
|
|
13
|
+
});
|
|
14
|
+
function likePattern(term) {
|
|
15
|
+
return `%${term.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
16
|
+
}
|
|
7
17
|
export default defineEventHandler(async (event) => {
|
|
8
18
|
await requireAdmin(event);
|
|
9
19
|
const { name, entry, table } = getRegistryEntry(event);
|
|
10
20
|
const db = useDb();
|
|
11
21
|
if (entry.kind === "single") {
|
|
12
|
-
const
|
|
13
|
-
await attachManyToMany(name, entry,
|
|
14
|
-
return
|
|
22
|
+
const rows = await db.select().from(table).limit(1);
|
|
23
|
+
await attachManyToMany(db, name, entry, rows);
|
|
24
|
+
return rows[0] ?? null;
|
|
25
|
+
}
|
|
26
|
+
const { limit, offset, search, light } = await getValidatedQuery(event, querySchema.parse);
|
|
27
|
+
const columns = tableColumns(table);
|
|
28
|
+
const titleColumn = entry.titleField && Object.hasOwn(columns, entry.titleField) ? columns[entry.titleField] : void 0;
|
|
29
|
+
let where;
|
|
30
|
+
if (search) {
|
|
31
|
+
const column = titleColumn ?? idColumn(table);
|
|
32
|
+
where = sql`${column} like ${likePattern(search)} escape '\\'`;
|
|
15
33
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
const selection = light ? Object.fromEntries([
|
|
35
|
+
["id", idColumn(table)],
|
|
36
|
+
...entry.titleField && titleColumn ? [[entry.titleField, titleColumn]] : [],
|
|
37
|
+
...entry.drafts && Object.hasOwn(columns, "status") ? [["status", columns.status]] : []
|
|
38
|
+
]) : void 0;
|
|
39
|
+
const orderBy = columns.createdAt ?? idColumn(table);
|
|
40
|
+
const base = selection ? db.select(selection).from(table) : db.select().from(table);
|
|
41
|
+
const items = await (where ? base.where(where) : base).orderBy(desc(orderBy)).limit(limit).offset(offset);
|
|
42
|
+
const [counted] = await (where ? db.select({ total: count() }).from(table).where(where) : db.select({ total: count() }).from(table));
|
|
43
|
+
if (!light) await attachManyToMany(db, name, entry, items);
|
|
44
|
+
return { items, total: counted?.total ?? 0 };
|
|
19
45
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
2
|
-
import {
|
|
2
|
+
import { withTransaction } from "#cms-db";
|
|
3
3
|
import { customId } from "../utils/custom-id.js";
|
|
4
4
|
import { mapConstraintErrors } from "../utils/db-errors.js";
|
|
5
5
|
import { buildValidator, getRegistryEntry } from "../utils/registry.js";
|
|
@@ -21,10 +21,14 @@ export default defineEventHandler(async (event) => {
|
|
|
21
21
|
await assertRelationTargets(entry, lists);
|
|
22
22
|
if (entry.drafts) values.status ??= "draft";
|
|
23
23
|
values.id = customId(entry.id);
|
|
24
|
-
return mapConstraintErrors(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
return mapConstraintErrors(
|
|
25
|
+
() => withTransaction(async (db) => {
|
|
26
|
+
const [row] = await db.insert(table).values(values).returning();
|
|
27
|
+
await saveManyToMany(db, name, row.id, lists);
|
|
28
|
+
const [attached] = await attachManyToMany(db, name, entry, [
|
|
29
|
+
row
|
|
30
|
+
]);
|
|
31
|
+
return attached;
|
|
32
|
+
})
|
|
33
|
+
);
|
|
30
34
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
2
|
-
import {
|
|
2
|
+
import { withTransaction } from "#cms-db";
|
|
3
3
|
import { mapConstraintErrors } from "../utils/db-errors.js";
|
|
4
4
|
import { buildValidator, getRegistryEntry, idColumn, withUpdatedAt } from "../utils/registry.js";
|
|
5
5
|
import {
|
|
@@ -19,10 +19,14 @@ export default defineEventHandler(async (event) => {
|
|
|
19
19
|
const { values, lists } = splitRelationValues(entry, body);
|
|
20
20
|
await assertRelationTargets(entry, lists);
|
|
21
21
|
const set = withUpdatedAt(table, values);
|
|
22
|
-
return mapConstraintErrors(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
return mapConstraintErrors(
|
|
23
|
+
() => withTransaction(async (db) => {
|
|
24
|
+
const [row] = await db.insert(table).values({ id: entry.id, ...set }).onConflictDoUpdate({ target: idColumn(table), set }).returning();
|
|
25
|
+
await saveManyToMany(db, name, entry.id, lists);
|
|
26
|
+
const [attached] = await attachManyToMany(db, name, entry, [
|
|
27
|
+
row
|
|
28
|
+
]);
|
|
29
|
+
return attached;
|
|
30
|
+
})
|
|
31
|
+
);
|
|
28
32
|
});
|
|
@@ -11,8 +11,9 @@ export default defineEventHandler(async (event) => {
|
|
|
11
11
|
throw createError({ statusCode: 404, statusMessage: "Single objects have no items" });
|
|
12
12
|
}
|
|
13
13
|
const id = parseId(event);
|
|
14
|
-
const
|
|
14
|
+
const db = useDb();
|
|
15
|
+
const rows = await db.select().from(table).where(eq(idColumn(table), id)).limit(1);
|
|
15
16
|
if (!rows[0]) throw createError({ statusCode: 404, statusMessage: "Row not found" });
|
|
16
|
-
const [attached] = await attachManyToMany(name, entry, [rows[0]]);
|
|
17
|
+
const [attached] = await attachManyToMany(db, name, entry, [rows[0]]);
|
|
17
18
|
return attached;
|
|
18
19
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
3
|
-
import {
|
|
3
|
+
import { withTransaction } from "#cms-db";
|
|
4
4
|
import {
|
|
5
5
|
buildValidator,
|
|
6
6
|
getRegistryEntry,
|
|
@@ -30,11 +30,15 @@ export default defineEventHandler(async (event) => {
|
|
|
30
30
|
const { values, lists } = splitRelationValues(entry, body);
|
|
31
31
|
await assertRelationTargets(entry, lists);
|
|
32
32
|
const set = withUpdatedAt(table, values);
|
|
33
|
-
return mapConstraintErrors(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
return mapConstraintErrors(
|
|
34
|
+
() => withTransaction(async (db) => {
|
|
35
|
+
const [row] = await db.update(table).set(set).where(eq(idColumn(table), id)).returning();
|
|
36
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Row not found" });
|
|
37
|
+
await saveManyToMany(db, name, id, lists);
|
|
38
|
+
const [attached] = await attachManyToMany(db, name, entry, [
|
|
39
|
+
row
|
|
40
|
+
]);
|
|
41
|
+
return attached;
|
|
42
|
+
})
|
|
43
|
+
);
|
|
40
44
|
});
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { slugify } from "../../shared/index.js";
|
|
5
|
-
import { useMediaStorage } from "../utils/media.js";
|
|
5
|
+
import { assertUploadContentType, useMediaStorage } from "../utils/media.js";
|
|
6
6
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
7
7
|
const MAX_BASE_LENGTH = 80;
|
|
8
8
|
const MAX_EXT_LENGTH = 10;
|
|
@@ -22,6 +22,7 @@ export default defineEventHandler(async (event) => {
|
|
|
22
22
|
await requireAdmin(event);
|
|
23
23
|
const { media, client, bucketUrl, publicUrl } = useMediaStorage(event);
|
|
24
24
|
const { filename, contentType, size } = await readValidatedBody(event, bodySchema.parse);
|
|
25
|
+
assertUploadContentType(contentType);
|
|
25
26
|
if (size > MAX_FILE_SIZE) {
|
|
26
27
|
throw createError({
|
|
27
28
|
statusCode: 413,
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { useDb } from "#cms-db";
|
|
4
4
|
import { cms_media } from "#cms-tables";
|
|
5
5
|
import { objectKeySchema } from "../../shared/validation.js";
|
|
6
|
-
import { toMediaItem, useMediaConfig } from "../utils/media.js";
|
|
6
|
+
import { assertUploadContentType, toMediaItem, useMediaConfig } from "../utils/media.js";
|
|
7
7
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
8
8
|
const bodySchema = z.object({
|
|
9
9
|
key: objectKeySchema,
|
|
@@ -18,6 +18,7 @@ export default defineEventHandler(async (event) => {
|
|
|
18
18
|
await requireAdmin(event);
|
|
19
19
|
const { publicUrl } = useMediaConfig(event);
|
|
20
20
|
const body = await readValidatedBody(event, bodySchema.parse);
|
|
21
|
+
if (body.mime) assertUploadContentType(body.mime);
|
|
21
22
|
const values = {
|
|
22
23
|
key: body.key,
|
|
23
24
|
mime: body.mime ?? null,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
3
|
+
import cmsConfig from "#cms-config";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { useDb } from "../utils/db-libsql.js";
|
|
6
|
+
export default async () => {
|
|
7
|
+
const { migrationsDir } = useRuntimeConfig().cms;
|
|
8
|
+
if (!existsSync(migrationsDir)) {
|
|
9
|
+
if (Object.keys(cmsConfig).length) {
|
|
10
|
+
console.error(
|
|
11
|
+
`[nuxt-cms] Migrations folder not found: ${migrationsDir}. CMS tables may be missing \u2014 run the dev server once and deploy the generated server/db/migrations folder.`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await migrate(useDb(), { migrationsFolder: migrationsDir });
|
|
17
|
+
};
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
3
|
+
import cmsConfig from "#cms-config";
|
|
3
4
|
import { useRuntimeConfig } from "#imports";
|
|
4
5
|
import { useDb } from "../utils/db-postgres.js";
|
|
5
6
|
export default async () => {
|
|
6
7
|
const { migrationsDir } = useRuntimeConfig().cms;
|
|
7
|
-
if (!existsSync(migrationsDir))
|
|
8
|
+
if (!existsSync(migrationsDir)) {
|
|
9
|
+
if (Object.keys(cmsConfig).length) {
|
|
10
|
+
console.error(
|
|
11
|
+
`[nuxt-cms] Migrations folder not found: ${migrationsDir}. CMS tables may be missing \u2014 run the dev server once and deploy the generated server/db/migrations folder.`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
8
16
|
await migrate(useDb(), { migrationsFolder: migrationsDir });
|
|
9
17
|
};
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
3
|
+
import cmsConfig from "#cms-config";
|
|
3
4
|
import { useRuntimeConfig } from "#imports";
|
|
4
5
|
import { useDb } from "../utils/db-sqlite.js";
|
|
5
6
|
export default () => {
|
|
6
7
|
const { migrationsDir } = useRuntimeConfig().cms;
|
|
7
|
-
if (!existsSync(migrationsDir))
|
|
8
|
+
if (!existsSync(migrationsDir)) {
|
|
9
|
+
if (Object.keys(cmsConfig).length) {
|
|
10
|
+
console.error(
|
|
11
|
+
`[nuxt-cms] Migrations folder not found: ${migrationsDir}. CMS tables may be missing \u2014 run the dev server once and deploy the generated server/db/migrations folder.`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
8
16
|
migrate(useDb(), { migrationsFolder: migrationsDir });
|
|
9
17
|
};
|
|
@@ -24,7 +24,7 @@ function safeEqual(a, b) {
|
|
|
24
24
|
return timingSafeEqual(hashA, hashB);
|
|
25
25
|
}
|
|
26
26
|
export default defineEventHandler(async (event) => {
|
|
27
|
-
const ip = getRequestIP(event) ?? "unknown";
|
|
27
|
+
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
pruneFailures(now);
|
|
30
30
|
if (globalFailures.resetAt > now && globalFailures.count >= RATE_GLOBAL_MAX_FAILURES) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function useDb(): import("drizzle-orm/libsql").LibSQLDatabase<Record<string, unknown>> & {
|
|
2
|
+
$client: import("@libsql/client").Client;
|
|
3
|
+
};
|
|
4
|
+
type Db = ReturnType<typeof useDb>;
|
|
5
|
+
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createClient } from "@libsql/client";
|
|
4
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
5
|
+
import { useRuntimeConfig } from "#imports";
|
|
6
|
+
let _db = null;
|
|
7
|
+
export function useDb() {
|
|
8
|
+
if (!_db) {
|
|
9
|
+
const { databaseUrl, databaseAuthToken, dbPath } = useRuntimeConfig().cms;
|
|
10
|
+
const url = databaseUrl || `file:${dbPath}`;
|
|
11
|
+
if (url.startsWith("file:")) mkdirSync(dirname(dbPath), { recursive: true });
|
|
12
|
+
_db = drizzle(createClient({ url, authToken: databaseAuthToken || void 0 }));
|
|
13
|
+
}
|
|
14
|
+
return _db;
|
|
15
|
+
}
|
|
16
|
+
export function withTransaction(fn) {
|
|
17
|
+
return useDb().transaction((tx) => fn(tx));
|
|
18
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export declare function useDb(): import("drizzle-orm/node-postgres").NodePgDatabase<Record<string, unknown>> & {
|
|
2
2
|
$client: import("pg").Pool;
|
|
3
3
|
};
|
|
4
|
+
type Db = ReturnType<typeof useDb>;
|
|
5
|
+
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
7
|
+
export {};
|