@xleddyl/nuxt-cms 0.8.1 → 0.9.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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.8.1",
4
+ "version": "0.9.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -9,7 +9,7 @@ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal'
9
9
  import { createJiti } from 'jiti';
10
10
  import { typeName, blockTypeName as blockTypeName$1, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
11
11
  import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
12
- import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, fieldConditions, selectOptionValues, CONTENT_SLUG_FIELD, pageFieldsTableName, pageMediaTableName, CONTENT_SYSTEM_FIELDS, isRequiredField, pageFields, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
12
+ import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, fieldConditions, selectOptionValues, pageFieldsTableName, pageMediaTableName, CONTENT_SYSTEM_FIELDS, isRequiredField, pageFields, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
13
13
  import { previewPathErrors, DEFAULT_CMS_PREVIEW_PATH } from '../dist/runtime/shared/preview.js';
14
14
  import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
15
15
  import { createHash } from 'node:crypto';
@@ -233,6 +233,7 @@ const RESERVED_ENTRY_KEYS = [
233
233
  "admin",
234
234
  "auth",
235
235
  "login",
236
+ "logout",
236
237
  "media",
237
238
  "graphql",
238
239
  "settings",
@@ -482,8 +483,6 @@ function validateConfig(config, i18n) {
482
483
  if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
483
484
  else if (source.type !== "text")
484
485
  errors.push(`${fat}: slug source '${field.from}' must be a text field`);
485
- else if (isTranslatableField(source) && !(entry.kind === "content" && key === CONTENT_SLUG_FIELD))
486
- errors.push(`${fat}: slug source '${field.from}' cannot be translatable`);
487
486
  }
488
487
  if (field.type === "tags") {
489
488
  errors.push(...tagsOptionErrors(fat, field));
@@ -761,9 +760,8 @@ function columnExpr(key, field, dialect, table) {
761
760
  expr = `text('${col}').unique()`;
762
761
  break;
763
762
  case "relation": {
764
- const onDelete = field.onDelete ?? (field.required ? "restrict" : "set null");
765
- const target = field.to === table ? `(): ${anyColumnType(dialect)}` : "()";
766
- expr = `text('${col}').references(${target} => ${field.to}.id, { onDelete: '${onDelete}' })`;
763
+ const onDelete = relationOnDelete(field);
764
+ expr = field.to === table ? `text('${col}')` : `text('${col}').references(() => ${field.to}.id, { onDelete: '${onDelete}' })`;
767
765
  if (field.cardinality === "one-to-one") expr += ".unique()";
768
766
  break;
769
767
  }
@@ -773,8 +771,8 @@ function columnExpr(key, field, dialect, table) {
773
771
  if (isRequiredField(field)) expr += ".notNull()";
774
772
  return ` ${key}: ${expr},`;
775
773
  }
776
- function anyColumnType(dialect) {
777
- return dialect === "postgres" ? "AnyPgColumn" : "AnySQLiteColumn";
774
+ function relationOnDelete(field) {
775
+ return field.onDelete ?? (field.required ? "restrict" : "set null");
778
776
  }
779
777
  function isSelfRelation(table, field) {
780
778
  return field.type === "relation" && !isManyToMany(field) && field.to === table;
@@ -792,6 +790,7 @@ function tableExpr(name, entry, dialect) {
792
790
  const pg = dialect === "postgres";
793
791
  const tableFn = pg ? "pgTable" : "sqliteTable";
794
792
  const lines = [];
793
+ const selfReferences = [];
795
794
  lines.push(` id: text('id').primaryKey(),`);
796
795
  if (entry.kind === "page") lines.push(` path: text('path').notNull().unique(),`);
797
796
  for (const [key, field] of Object.entries(
@@ -799,13 +798,22 @@ function tableExpr(name, entry, dialect) {
799
798
  )) {
800
799
  if (isManyToMany(field)) continue;
801
800
  lines.push(columnExpr(key, field, dialect, name));
801
+ if (isSelfRelation(name, field))
802
+ selfReferences.push(
803
+ `foreignKey({ columns: [table.${key}], foreignColumns: [table.id] }).onDelete('${relationOnDelete(
804
+ field
805
+ )}')`
806
+ );
802
807
  }
803
808
  if (entry.drafts) lines.push(` status: text('status').notNull().default('draft'),`);
804
809
  if (isCollectionKind(entry)) lines.push(` createdAt: ${nowExpr(pg, "created_at")},`);
805
810
  lines.push(` updatedAt: ${nowExpr(pg, "updated_at")},`);
811
+ const extra = selfReferences.length ? `, table => [
812
+ ${selfReferences.join(",\n ")},
813
+ ]` : "";
806
814
  return `export const ${name} = ${tableFn}('${name}', {
807
815
  ${lines.join("\n")}
808
- })`;
816
+ }${extra})`;
809
817
  }
810
818
  function joinTableExpr(name, key, field, dialect) {
811
819
  const pg = dialect === "postgres";
@@ -888,18 +896,14 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
888
896
  core.add("jsonb");
889
897
  if (pg) core.add("timestamp");
890
898
  if (fields.some(isManyToMany) || rowsEntries.length) core.add("primaryKey");
891
- const hasSelfRelation = derived.some(
899
+ if (derived.some(
892
900
  ([name, entry]) => Object.values(entry.kind === "page" ? pageColumnFields(entry) : entry.fields).some(
893
901
  (field) => isSelfRelation(name, field)
894
902
  )
895
- );
903
+ ))
904
+ core.add("foreignKey");
896
905
  const imports = [
897
906
  ...pg ? [] : [`import { sql } from '${resolveImport("drizzle-orm")}'`],
898
- ...hasSelfRelation ? [
899
- `import type { ${anyColumnType(dialect)} } from '${resolveImport(
900
- `drizzle-orm/${pg ? "pg-core" : "sqlite-core"}`
901
- )}'`
902
- ] : [],
903
907
  `import { ${[...core].sort().join(", ")} } from '${resolveImport(
904
908
  `drizzle-orm/${pg ? "pg-core" : "sqlite-core"}`
905
909
  )}'`
@@ -1985,6 +1989,11 @@ const module$1 = defineNuxtModule({
1985
1989
  path: "/cms/login",
1986
1990
  file: resolver.resolve("./runtime/app/pages/admin-login.vue")
1987
1991
  },
1992
+ {
1993
+ name: "cms-admin-logout",
1994
+ path: "/cms/logout",
1995
+ file: resolver.resolve("./runtime/app/pages/admin-logout.vue")
1996
+ },
1988
1997
  {
1989
1998
  name: "cms-admin-media",
1990
1999
  path: "/cms/media",
@@ -86,7 +86,7 @@ import {
86
86
  Root as KRoot,
87
87
  provideTheme
88
88
  } from "@xleddyl/443-components-vue";
89
- import { computed, navigateTo, ref, useRoute, useUserSession, watch } from "#imports";
89
+ import { computed, navigateTo, ref, useRoute, watch } from "#imports";
90
90
  import cmsConfig from "#cms-config";
91
91
  import { cmsBrand } from "#cms-brand";
92
92
  import { CMS_SEARCH_MIN_LENGTH, normalizeSearchQuery } from "../../shared/search";
@@ -95,7 +95,6 @@ import { useCmsAccount } from "../composables/cms-account";
95
95
  import { loadCmsSettings } from "../composables/cms-settings";
96
96
  import { cmsIcon } from "../utils/ui";
97
97
  const route = useRoute();
98
- const { clear } = useUserSession();
99
98
  const { email, displayName } = useCmsAccount();
100
99
  const theme = useCmsTheme();
101
100
  provideTheme(theme);
@@ -219,8 +218,7 @@ function openSettings() {
219
218
  }
220
219
  async function logout() {
221
220
  settingsOpen.value = false;
222
- await clear();
223
- await navigateTo("/cms/login");
221
+ await navigateTo("/cms/logout");
224
222
  }
225
223
  </script>
226
224
 
@@ -1,52 +1,29 @@
1
1
  <template>
2
2
  <KRoot class="cms-scope cms-canvas" :theme="theme" preset="default">
3
- <KAuthLayout
3
+ <KAuthLogin
4
+ :submit="login"
5
+ :submit-error="loginError"
4
6
  title="Welcome back"
5
7
  description="Sign in to manage your content."
6
8
  footer="Powered by nuxt-cms"
9
+ aside-title="Your content, next to your code."
10
+ aside-description="Edit entries, pages and media for this site in one place."
7
11
  >
8
12
  <template #brand>
9
- <CmsBrandMark size="lg" hide-text class="cms-auth-brand" />
13
+ <CmsBrandMark size="lg" hide-text />
10
14
  </template>
11
-
12
- <KForm :form="form">
13
- <KFormField label="Email" name="email" required>
14
- <KInput
15
- v-model="form.values.email"
16
- type="email"
17
- :icon="cmsIcon('envelope')"
18
- autocomplete="username"
19
- placeholder="you@example.com"
20
- />
21
- </KFormField>
22
- <KFormField label="Password" name="password" required>
23
- <KInput
24
- v-model="form.values.password"
25
- type="password"
26
- icon="key"
27
- autocomplete="current-password"
28
- placeholder="••••••••"
29
- />
30
- </KFormField>
31
- <KButton type="submit" label="Sign in" trailing-icon="arrow-right" size="lg" block />
32
- </KForm>
33
- </KAuthLayout>
15
+ </KAuthLogin>
34
16
  </KRoot>
35
17
  </template>
36
18
 
37
19
  <script setup>
38
20
  import {
39
- AuthLayout as KAuthLayout,
40
- Button as KButton,
41
- Form as KForm,
42
- FormField as KFormField,
43
- Input as KInput,
21
+ AuthLogin as KAuthLogin,
44
22
  Root as KRoot,
45
- provideTheme,
46
- useForm
23
+ provideTheme
47
24
  } from "@xleddyl/443-components-vue";
48
25
  import { definePageMeta, navigateTo, useUserSession } from "#imports";
49
- import { cmsIcon, errorMessage } from "../utils/ui";
26
+ import { errorMessage } from "../utils/ui";
50
27
  import { useCmsTheme } from "../composables/cms-theme";
51
28
  definePageMeta({ layout: false });
52
29
  const theme = useCmsTheme();
@@ -55,25 +32,16 @@ const { loggedIn } = useUserSession();
55
32
  if (loggedIn.value) {
56
33
  await navigateTo("/cms", { replace: true });
57
34
  }
58
- const form = useForm({
59
- schema: {
60
- email: { initial: () => "" },
61
- password: { initial: () => "" }
62
- },
63
- onSubmit: login
64
- });
65
- async function login(values) {
66
- try {
67
- await $fetch("/api/cms/auth/login", { method: "POST", body: values });
68
- window.location.replace("/cms");
69
- } catch (e) {
70
- const status = e.statusCode;
71
- form.setError(
72
- "",
73
- status === 401 ? "Invalid email or password." : errorMessage(e) ?? "Login failed. Please try again."
74
- );
75
- }
35
+ async function login(credentials) {
36
+ await $fetch("/api/cms/auth/login", { method: "POST", body: credentials });
37
+ window.location.replace("/cms");
76
38
  }
39
+ const loginError = (error) => {
40
+ const status = error.statusCode;
41
+ return {
42
+ message: status === 401 ? "Invalid email or password." : errorMessage(error) ?? "Login failed. Please try again."
43
+ };
44
+ };
77
45
  </script>
78
46
 
79
47
  <style src="../../assets/admin.css">
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <KRoot class="cms-scope cms-canvas" :theme="theme" preset="default">
3
+ <KAuthLogout
4
+ :logout="clear"
5
+ :account="account"
6
+ login-to="/cms/login"
7
+ footer="Powered by nuxt-cms"
8
+ aside-title="Your content, next to your code."
9
+ aside-description="Edit entries, pages and media for this site in one place."
10
+ >
11
+ <template #brand>
12
+ <CmsBrandMark size="lg" hide-text />
13
+ </template>
14
+ </KAuthLogout>
15
+ </KRoot>
16
+ </template>
17
+
18
+ <script setup>
19
+ import { AuthLogout as KAuthLogout, Root as KRoot, provideTheme } from "@xleddyl/443-components-vue";
20
+ import { computed, definePageMeta, useUserSession } from "#imports";
21
+ import { useCmsAccount } from "../composables/cms-account";
22
+ import { useCmsTheme } from "../composables/cms-theme";
23
+ definePageMeta({ layout: false, middleware: "cms-auth" });
24
+ const theme = useCmsTheme();
25
+ provideTheme(theme);
26
+ const { clear } = useUserSession();
27
+ const { user, email } = useCmsAccount();
28
+ const account = computed(() => ({
29
+ email: email.value,
30
+ name: user.value?.name?.trim() || void 0
31
+ }));
32
+ </script>
33
+
34
+ <style src="../../assets/admin.css">
35
+
36
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -14,7 +14,7 @@ import {
14
14
  } from "./index.js";
15
15
  export const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
16
16
  export const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
17
- const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "media"]);
17
+ const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "logout", "media"]);
18
18
  export function previewPathErrors(path, entryNames) {
19
19
  const match = /^\/cms\/([a-z0-9][a-z0-9-]*)$/.exec(path);
20
20
  if (!match)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
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)",
@@ -69,7 +69,7 @@
69
69
  "@tiptap/pm": "^3.32.0",
70
70
  "@tiptap/starter-kit": "^3.32.0",
71
71
  "@tiptap/vue-3": "^3.32.0",
72
- "@xleddyl/443-components-vue": "^0.6.2",
72
+ "@xleddyl/443-components-vue": "^0.16.49",
73
73
  "aws4fetch": "^1.0.20",
74
74
  "drizzle-kit": "^0.31.0",
75
75
  "drizzle-orm": "^0.44.0",