@xleddyl/nuxt-cms 0.1.31 → 0.1.32
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
CHANGED
|
@@ -5,7 +5,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
|
|
|
5
5
|
- **Zero extra infrastructure**: the CMS runs inside your app's Nitro server; you deploy one thing.
|
|
6
6
|
- **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
|
|
7
7
|
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, or a local mode backed directly by your `public/` folder), single-admin auth from env credentials.
|
|
8
|
-
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination.
|
|
8
|
+
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination; fields marked `private` stay out of it.
|
|
9
9
|
- **SQLite, Postgres or libSQL/Turso**: a local file database by default, one config line to switch (including remote SQLite over the network).
|
|
10
10
|
|
|
11
11
|
## Installation
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { introspectionFromSchema, buildSchema } from 'graphql';
|
|
|
10
10
|
import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
|
-
import { isMultiSelect, isTranslatableField, isTranslatableMediaField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
13
|
+
import { isMultiSelect, isTranslatableField, isTranslatableMediaField, isPrivateField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
14
14
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
15
15
|
|
|
16
16
|
async function collectMediaManifest(root) {
|
|
@@ -169,6 +169,8 @@ function validateConfig(config, i18n) {
|
|
|
169
169
|
}
|
|
170
170
|
if (blockField.translatable)
|
|
171
171
|
errors.push(`${bfat}: translatable fields are not supported inside blocks`);
|
|
172
|
+
if (blockField.private)
|
|
173
|
+
errors.push(`${bfat}: private fields are not supported inside blocks`);
|
|
172
174
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
173
175
|
errors.push(`${bfat}: select requires a non-empty options array`);
|
|
174
176
|
}
|
|
@@ -416,6 +418,7 @@ ${lines.join("\n")}
|
|
|
416
418
|
function entryTs(config, name, entry) {
|
|
417
419
|
const lines = [" id: string"];
|
|
418
420
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
421
|
+
if (isPrivateField(field)) continue;
|
|
419
422
|
lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
|
|
420
423
|
}
|
|
421
424
|
if (entry.kind === "collection") lines.push(" createdAt: string");
|
|
@@ -440,7 +443,8 @@ function renderTypesFile(config) {
|
|
|
440
443
|
];
|
|
441
444
|
for (const [name, entry] of Object.entries(config)) {
|
|
442
445
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
443
|
-
if (field.type === "blocks"
|
|
446
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
447
|
+
parts.push(...blockTypesTs(name, key, field));
|
|
444
448
|
}
|
|
445
449
|
parts.push(entryTs(config, name, entry));
|
|
446
450
|
}
|
|
@@ -21,6 +21,7 @@ import * as cmsTables from "#cms-tables";
|
|
|
21
21
|
import { useRuntimeConfig } from "#imports";
|
|
22
22
|
import {
|
|
23
23
|
decodeTranslatableMedia,
|
|
24
|
+
isPrivateField,
|
|
24
25
|
isTranslatableMediaField,
|
|
25
26
|
mediaPublicUrl,
|
|
26
27
|
mediaTypeFor,
|
|
@@ -257,6 +258,7 @@ function mediaObject(key, row) {
|
|
|
257
258
|
function entryResolvers(config, name, entry) {
|
|
258
259
|
const resolvers = {};
|
|
259
260
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
261
|
+
if (isPrivateField(field)) continue;
|
|
260
262
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
261
263
|
resolvers[key] = async (parent, _args, ctx) => {
|
|
262
264
|
const rows = await loadManyToMany(ctx, name, key, field).load(parent.id);
|
|
@@ -301,7 +303,8 @@ export function buildCmsSchema() {
|
|
|
301
303
|
const fieldLevel = entryResolvers(config, name, entry);
|
|
302
304
|
if (Object.keys(fieldLevel).length) typeResolvers[gqlType] = fieldLevel;
|
|
303
305
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
304
|
-
if (field.type === "blocks"
|
|
306
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
307
|
+
Object.assign(typeResolvers, blockResolvers(name, key, field));
|
|
305
308
|
}
|
|
306
309
|
if (entry.kind === "single") {
|
|
307
310
|
queryResolvers[name] = async (_, args) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTranslatableField } from "./index.js";
|
|
1
|
+
import { isPrivateField, isTranslatableField } from "./index.js";
|
|
2
2
|
export function typeName(name) {
|
|
3
3
|
return name.replace(/(?:^|_)([a-z0-9])/gi, (_, c) => c.toUpperCase());
|
|
4
4
|
}
|
|
@@ -21,7 +21,7 @@ function scalarFor(field) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function filterScalarFor(field) {
|
|
24
|
-
if (isTranslatableField(field)) return null;
|
|
24
|
+
if (isPrivateField(field) || isTranslatableField(field)) return null;
|
|
25
25
|
switch (field.type) {
|
|
26
26
|
case "json":
|
|
27
27
|
return null;
|
|
@@ -64,6 +64,7 @@ function fieldSdl(config, entryName, key, field) {
|
|
|
64
64
|
function entrySdl(config, name, entry) {
|
|
65
65
|
const lines = [" id: ID!"];
|
|
66
66
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
67
|
+
if (isPrivateField(field)) continue;
|
|
67
68
|
lines.push(fieldSdl(config, name, key, field));
|
|
68
69
|
}
|
|
69
70
|
if (entry.kind === "collection") lines.push(" createdAt: String!");
|
|
@@ -127,7 +128,8 @@ export function renderGraphqlSdl(config) {
|
|
|
127
128
|
const gqlType = typeName(name);
|
|
128
129
|
types.push(entrySdl(config, name, entry));
|
|
129
130
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
130
|
-
if (field.type === "blocks"
|
|
131
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
132
|
+
types.push(...blocksSdl(name, key, field));
|
|
131
133
|
}
|
|
132
134
|
if (entry.kind === "single") {
|
|
133
135
|
queryLines.push(` ${name}(locale: String): ${gqlType}`);
|
|
@@ -46,6 +46,7 @@ export interface FieldConfig {
|
|
|
46
46
|
label: string;
|
|
47
47
|
type: FieldType;
|
|
48
48
|
required?: boolean;
|
|
49
|
+
private?: boolean;
|
|
49
50
|
textarea?: boolean;
|
|
50
51
|
integer?: boolean;
|
|
51
52
|
translatable?: boolean;
|
|
@@ -59,6 +60,7 @@ export interface FieldConfig {
|
|
|
59
60
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
60
61
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
61
62
|
}
|
|
63
|
+
export declare function isPrivateField(field: FieldConfig): boolean;
|
|
62
64
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
63
65
|
export declare function isTranslatableMediaField(field: FieldConfig): boolean;
|
|
64
66
|
export declare function decodeTranslatableMedia(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
@@ -82,6 +84,7 @@ export type CmsConfig = Record<string, CmsEntry>;
|
|
|
82
84
|
interface FieldInputBase {
|
|
83
85
|
label: string;
|
|
84
86
|
required?: boolean;
|
|
87
|
+
private?: boolean;
|
|
85
88
|
}
|
|
86
89
|
export interface TextFieldInput extends FieldInputBase {
|
|
87
90
|
type: 'text';
|
|
@@ -129,7 +132,7 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
129
132
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
130
133
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
131
134
|
}
|
|
132
|
-
export type BlockFieldInput = Omit<TextFieldInput, 'translatable'> | Omit<RichtextFieldInput, 'translatable'> | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SelectFieldInput | JsonFieldInput | Omit<MediaFieldInput, 'translatable'>;
|
|
135
|
+
export type BlockFieldInput = Omit<TextFieldInput, 'translatable' | 'private'> | Omit<RichtextFieldInput, 'translatable' | 'private'> | Omit<NumberFieldInput, 'private'> | Omit<BooleanFieldInput, 'private'> | Omit<DateFieldInput, 'private'> | Omit<EmailFieldInput, 'private'> | Omit<SelectFieldInput, 'private'> | Omit<JsonFieldInput, 'private'> | Omit<MediaFieldInput, 'translatable' | 'private'>;
|
|
133
136
|
export interface BlockInput {
|
|
134
137
|
label: string;
|
|
135
138
|
fields: Record<string, BlockFieldInput>;
|
|
@@ -61,6 +61,9 @@ export function normalizeMediaFolder(value) {
|
|
|
61
61
|
const segments = value.split("/").map(slugify).filter(Boolean).slice(0, MEDIA_FOLDER_MAX_DEPTH);
|
|
62
62
|
return segments.length ? segments.join("/") : null;
|
|
63
63
|
}
|
|
64
|
+
export function isPrivateField(field) {
|
|
65
|
+
return !!field.private;
|
|
66
|
+
}
|
|
64
67
|
export function isTranslatableField(field) {
|
|
65
68
|
return !!field.translatable && (field.type === "text" || field.type === "richtext" || field.type === "media");
|
|
66
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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)",
|