mktcms 0.3.6 → 0.3.8

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": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.3.6",
4
+ "version": "0.3.8",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -193,6 +193,10 @@ const module$1 = defineNuxtModule({
193
193
  route: "/api/content/:path",
194
194
  handler: resolver.resolve("./runtime/server/api/content/[path]")
195
195
  });
196
+ addServerHandler({
197
+ route: "/api/health",
198
+ handler: resolver.resolve("./runtime/server/api/health")
199
+ });
196
200
  extendPages((pages) => {
197
201
  pages.push({
198
202
  name: "Admin Dashboard",
@@ -29,6 +29,30 @@ function isObjectSchema(schema) {
29
29
  function isPrimitiveSchema(schema) {
30
30
  return isRecord(schema) && ["string", "number", "date", "datetime"].includes(schema.type ?? "");
31
31
  }
32
+ function isSelectSingleSchema(schema) {
33
+ if (!isRecord(schema)) {
34
+ return false;
35
+ }
36
+ return schema["x-ui"] === "select-single" || schema.type === "string" && Array.isArray(schema.enum);
37
+ }
38
+ function isSelectMultipleSchema(schema) {
39
+ if (!isRecord(schema)) {
40
+ return false;
41
+ }
42
+ if (schema["x-ui"] === "select-multiple") {
43
+ return true;
44
+ }
45
+ if (schema.type !== "array") {
46
+ return false;
47
+ }
48
+ if (Array.isArray(schema.enum)) {
49
+ return true;
50
+ }
51
+ if (!isRecord(schema.items)) {
52
+ return false;
53
+ }
54
+ return schema.items.type === "string" && Array.isArray(schema.items.enum);
55
+ }
32
56
  function isSchemaMap(schema) {
33
57
  return isRecord(schema) && !("type" in schema);
34
58
  }
@@ -39,7 +63,18 @@ function createDefaultFromSchema(schema) {
39
63
  if (!schema || !schema.type) {
40
64
  return "";
41
65
  }
66
+ if (isSelectSingleSchema(schema)) {
67
+ const options = getSelectSingleOptionsFromSchema(schema);
68
+ const firstOption = options[0];
69
+ if (firstOption) {
70
+ return firstOption.value;
71
+ }
72
+ return "";
73
+ }
42
74
  if (schema.type === "array") {
75
+ if (isSelectMultipleSchema(schema)) {
76
+ return [];
77
+ }
43
78
  return [];
44
79
  }
45
80
  if (schema.type === "object") {
@@ -58,6 +93,43 @@ function createDefaultFromSchema(schema) {
58
93
  }
59
94
  return "";
60
95
  }
96
+ function getSelectOptionsFromSchema(schema) {
97
+ if (!schema) {
98
+ return [];
99
+ }
100
+ const rawOptions = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.enum) ? schema.enum.map((value) => ({ value })) : [];
101
+ return rawOptions.filter((option) => option !== null && option !== void 0).map((option) => {
102
+ if (typeof option === "string" || typeof option === "number") {
103
+ return {
104
+ label: String(option),
105
+ value: String(option)
106
+ };
107
+ }
108
+ if (isRecord(option) && option.value !== void 0 && option.value !== null) {
109
+ const optionLabel = "label" in option ? option.label : void 0;
110
+ return {
111
+ label: String(optionLabel ?? option.value),
112
+ value: String(option.value)
113
+ };
114
+ }
115
+ return null;
116
+ }).filter((option) => option !== null);
117
+ }
118
+ function getSelectSingleOptionsFromSchema(schema) {
119
+ return getSelectOptionsFromSchema(schema);
120
+ }
121
+ function getSelectMultipleOptionsFromSchema(schema) {
122
+ if (!schema || !isArraySchema(schema)) {
123
+ return [];
124
+ }
125
+ if (Array.isArray(schema.enum) || Array.isArray(schema.oneOf)) {
126
+ return getSelectOptionsFromSchema(schema);
127
+ }
128
+ if (schema.items && isRecord(schema.items)) {
129
+ return getSelectOptionsFromSchema(schema.items);
130
+ }
131
+ return [];
132
+ }
61
133
  function ensureInitializedFromSchema() {
62
134
  const schema = props.schema;
63
135
  if (!schema) {
@@ -192,6 +264,14 @@ function removeArrayItem(arrayRef, index) {
192
264
  </label>
193
265
  </div>
194
266
 
267
+ <FrontmatterInput
268
+ v-else-if="isSelectSingleSchema(arrayItemSchema)"
269
+ v-model:value="frontmatter[index]"
270
+ label=""
271
+ :select-options="getSelectSingleOptionsFromSchema(arrayItemSchema)"
272
+ select-mode="single"
273
+ />
274
+
195
275
  <FrontmatterInput
196
276
  v-else-if="isPrimitiveSchema(arrayItemSchema)"
197
277
  v-model:value="frontmatter[index]"
@@ -247,7 +327,21 @@ function removeArrayItem(arrayRef, index) {
247
327
  </p>
248
328
 
249
329
  <FrontmatterInput
250
- v-if="isPrimitiveSchema(entry[1])"
330
+ v-if="isSelectSingleSchema(entry[1])"
331
+ v-model:value="frontmatter[entry[0]]"
332
+ :select-options="getSelectSingleOptionsFromSchema(entry[1])"
333
+ select-mode="single"
334
+ />
335
+
336
+ <FrontmatterInput
337
+ v-else-if="isSelectMultipleSchema(entry[1])"
338
+ v-model:value="frontmatter[entry[0]]"
339
+ :select-options="getSelectMultipleOptionsFromSchema(entry[1])"
340
+ select-mode="multiple"
341
+ />
342
+
343
+ <FrontmatterInput
344
+ v-else-if="isPrimitiveSchema(entry[1])"
251
345
  v-model:value="frontmatter[entry[0]]"
252
346
  :input-type="getInputTypeFromSchema(entry[1])"
253
347
  :ui-hint="getUiHintFromSchema(entry[1])"
@@ -2,19 +2,29 @@ type __VLS_Props = {
2
2
  label?: string;
3
3
  inputType?: 'text' | 'number' | 'date' | 'datetime-local';
4
4
  uiHint?: 'image' | 'pdf' | 'file';
5
+ selectMode?: 'single' | 'multiple';
6
+ selectOptions?: Array<{
7
+ label: string;
8
+ value: string | number;
9
+ }>;
5
10
  };
6
11
  type __VLS_ModelProps = {
7
- 'value': string | number;
12
+ 'value': string | number | Array<string | number>;
8
13
  };
9
14
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- "update:value": (value: string | number) => any;
16
+ "update:value": (value: string | number | (string | number)[]) => any;
12
17
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
- "onUpdate:value"?: ((value: string | number) => any) | undefined;
18
+ "onUpdate:value"?: ((value: string | number | (string | number)[]) => any) | undefined;
14
19
  }>, {
15
20
  label: string;
16
21
  uiHint: "image" | "pdf" | "file";
17
22
  inputType: "text" | "number" | "date" | "datetime-local";
23
+ selectMode: "single" | "multiple";
24
+ selectOptions: Array<{
25
+ label: string;
26
+ value: string | number;
27
+ }>;
18
28
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
29
  declare const _default: typeof __VLS_export;
20
30
  export default _default;
@@ -4,12 +4,15 @@ import FilePickerModal from "./filePicker/modal.vue";
4
4
  const props = defineProps({
5
5
  label: { type: String, required: false, default: "" },
6
6
  inputType: { type: String, required: false, default: "text" },
7
- uiHint: { type: String, required: false, default: void 0 }
7
+ uiHint: { type: String, required: false, default: void 0 },
8
+ selectMode: { type: String, required: false, default: void 0 },
9
+ selectOptions: { type: Array, required: false, default: () => [] }
8
10
  });
9
- const value = defineModel("value", { type: [String, Number], ...{
11
+ const value = defineModel("value", { type: [String, Number, Array], ...{
10
12
  required: true
11
13
  } });
12
14
  const isPickerOpen = ref(false);
15
+ const hasSelectInput = computed(() => props.selectMode === "single" || props.selectMode === "multiple");
13
16
  const resolvedInputType = computed(() => {
14
17
  if (props.inputType !== "text") {
15
18
  return props.inputType;
@@ -17,6 +20,14 @@ const resolvedInputType = computed(() => {
17
20
  return typeof value.value === "number" ? "number" : "text";
18
21
  });
19
22
  const isNumberInput = computed(() => resolvedInputType.value === "number");
23
+ function normalizeOptionValue(optionValue) {
24
+ for (const option of props.selectOptions) {
25
+ if (String(option.value) === optionValue) {
26
+ return option.value;
27
+ }
28
+ }
29
+ return optionValue;
30
+ }
20
31
  const inputValue = computed({
21
32
  get() {
22
33
  return `${value.value ?? ""}`;
@@ -30,6 +41,37 @@ const inputValue = computed({
30
41
  value.value = newValue;
31
42
  }
32
43
  });
44
+ const singleSelectValue = computed({
45
+ get() {
46
+ if (Array.isArray(value.value)) {
47
+ return value.value.length > 0 ? String(value.value[0]) : "";
48
+ }
49
+ return `${value.value ?? ""}`;
50
+ },
51
+ set(newValue) {
52
+ value.value = normalizeOptionValue(newValue);
53
+ }
54
+ });
55
+ function isMultipleOptionSelected(optionValue) {
56
+ if (!Array.isArray(value.value)) {
57
+ return false;
58
+ }
59
+ return value.value.some((selected) => String(selected) === String(optionValue));
60
+ }
61
+ function toggleMultipleOption(optionValue, isChecked) {
62
+ const normalizedValue = normalizeOptionValue(String(optionValue));
63
+ const currentValues = Array.isArray(value.value) ? [...value.value] : [];
64
+ if (isChecked) {
65
+ if (!currentValues.some((selected) => String(selected) === String(normalizedValue))) {
66
+ currentValues.push(normalizedValue);
67
+ }
68
+ } else {
69
+ const nextValues = currentValues.filter((selected) => String(selected) !== String(normalizedValue));
70
+ value.value = nextValues;
71
+ return;
72
+ }
73
+ value.value = currentValues;
74
+ }
33
75
  const pickerButtonLabel = computed(() => {
34
76
  if (props.uiHint === "image") {
35
77
  return "Bild ausw\xE4hlen";
@@ -55,7 +97,43 @@ function onPickerSelect(path) {
55
97
  >
56
98
  {{ props.label }}
57
99
  </label>
58
- <div class="flex items-center gap-2">
100
+
101
+ <select
102
+ v-if="hasSelectInput && props.selectMode === 'single'"
103
+ v-model="singleSelectValue"
104
+ class="w-full"
105
+ >
106
+ <option
107
+ v-for="option in props.selectOptions"
108
+ :key="`${option.value}`"
109
+ :value="`${option.value}`"
110
+ >
111
+ {{ option.label }}
112
+ </option>
113
+ </select>
114
+
115
+ <div
116
+ v-else-if="hasSelectInput && props.selectMode === 'multiple'"
117
+ class="flex flex-col gap-2 w-full"
118
+ >
119
+ <label
120
+ v-for="option in props.selectOptions"
121
+ :key="`${option.value}`"
122
+ class="inline-flex items-center gap-2"
123
+ >
124
+ <input
125
+ type="checkbox"
126
+ :checked="isMultipleOptionSelected(option.value)"
127
+ @change="toggleMultipleOption(option.value, $event.target.checked)"
128
+ >
129
+ {{ option.label }}
130
+ </label>
131
+ </div>
132
+
133
+ <div
134
+ v-else
135
+ class="flex items-center gap-2"
136
+ >
59
137
  <input
60
138
  v-model="inputValue"
61
139
  :type="resolvedInputType"
@@ -2,19 +2,29 @@ type __VLS_Props = {
2
2
  label?: string;
3
3
  inputType?: 'text' | 'number' | 'date' | 'datetime-local';
4
4
  uiHint?: 'image' | 'pdf' | 'file';
5
+ selectMode?: 'single' | 'multiple';
6
+ selectOptions?: Array<{
7
+ label: string;
8
+ value: string | number;
9
+ }>;
5
10
  };
6
11
  type __VLS_ModelProps = {
7
- 'value': string | number;
12
+ 'value': string | number | Array<string | number>;
8
13
  };
9
14
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- "update:value": (value: string | number) => any;
16
+ "update:value": (value: string | number | (string | number)[]) => any;
12
17
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
- "onUpdate:value"?: ((value: string | number) => any) | undefined;
18
+ "onUpdate:value"?: ((value: string | number | (string | number)[]) => any) | undefined;
14
19
  }>, {
15
20
  label: string;
16
21
  uiHint: "image" | "pdf" | "file";
17
22
  inputType: "text" | "number" | "date" | "datetime-local";
23
+ selectMode: "single" | "multiple";
24
+ selectOptions: Array<{
25
+ label: string;
26
+ value: string | number;
27
+ }>;
18
28
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
29
  declare const _default: typeof __VLS_export;
20
30
  export default _default;
@@ -1,87 +1,9 @@
1
- import { access, readdir, lstat } from "node:fs/promises";
2
- import { resolve } from "node:path";
3
- import { execFile } from "node:child_process";
4
- import { promisify } from "node:util";
5
1
  import { createError, defineEventHandler } from "h3";
6
- const execFileAsync = promisify(execFile);
7
- const CACHE_TTL_MS = 6e4;
8
- let cachedBytes = null;
9
- let inFlight = null;
10
- async function getDirectorySizeBytes(dirPath) {
11
- const dirsToVisit = [dirPath];
12
- let totalBytes = 0;
13
- while (dirsToVisit.length > 0) {
14
- const currentDir = dirsToVisit.pop();
15
- let entries;
16
- try {
17
- entries = await readdir(currentDir, { withFileTypes: true });
18
- } catch (error) {
19
- if (error?.code === "ENOENT") {
20
- continue;
21
- }
22
- throw error;
23
- }
24
- for (const entry of entries) {
25
- const entryPath = resolve(currentDir, entry.name);
26
- let stats;
27
- try {
28
- stats = await lstat(entryPath);
29
- } catch (error) {
30
- if (error?.code === "ENOENT") {
31
- continue;
32
- }
33
- throw error;
34
- }
35
- if (stats.isDirectory()) {
36
- dirsToVisit.push(entryPath);
37
- continue;
38
- }
39
- totalBytes += stats.size;
40
- }
41
- }
42
- return totalBytes;
43
- }
2
+ import { getStorageUsageBytes } from "../../utils/storageUsage.js";
44
3
  export default defineEventHandler(async () => {
45
4
  try {
46
- const now = Date.now();
47
- if (cachedBytes && now - cachedBytes.at < CACHE_TTL_MS) {
48
- return { bytes: cachedBytes.value };
49
- }
50
- const storageDir = resolve(process.cwd(), ".storage");
51
- try {
52
- await access(storageDir);
53
- } catch {
54
- cachedBytes = { value: 0, at: now };
55
- return { bytes: 0 };
56
- }
57
- try {
58
- if (!inFlight) {
59
- inFlight = (async () => {
60
- try {
61
- const { stdout } = await execFileAsync("du", ["-sb", storageDir], {
62
- timeout: 15e3,
63
- maxBuffer: 1024 * 1024
64
- });
65
- const bytesString = stdout.trim().split(/\s+/)[0];
66
- const bytes2 = Number.parseInt(bytesString || "", 10);
67
- if (!Number.isFinite(bytes2) || bytes2 < 0) {
68
- throw new Error("Unexpected du output");
69
- }
70
- return bytes2;
71
- } catch {
72
- return await getDirectorySizeBytes(storageDir);
73
- }
74
- })();
75
- }
76
- const bytes = await inFlight;
77
- cachedBytes = { value: bytes, at: Date.now() };
78
- return { bytes };
79
- } catch {
80
- inFlight = null;
81
- throw new Error("Failed to calculate storage usage");
82
- } finally {
83
- inFlight = null;
84
- }
5
+ const bytes = await getStorageUsageBytes();
6
+ return { bytes };
85
7
  } catch (error) {
86
8
  throw createError({
87
9
  statusCode: 500,
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { createError, defineEventHandler, setHeader } from "h3";
2
+ import { getStorageUsageBytes } from "../utils/storageUsage.js";
3
+ export default defineEventHandler(async (event) => {
4
+ try {
5
+ const storageUsageBytes = await getStorageUsageBytes();
6
+ setHeader(event, "Content-Type", "text/plain; version=0.0.4; charset=utf-8");
7
+ return [
8
+ "# HELP mktcms_up Whether the mktcms API is healthy (1 = healthy).",
9
+ "# TYPE mktcms_up gauge",
10
+ "mktcms_up 1",
11
+ "# HELP mktcms_storage_usage_bytes Current .storage directory size in bytes.",
12
+ "# TYPE mktcms_storage_usage_bytes gauge",
13
+ `mktcms_storage_usage_bytes ${storageUsageBytes}`
14
+ ].join("\n") + "\n";
15
+ } catch (error) {
16
+ throw createError({
17
+ statusCode: 500,
18
+ statusMessage: error?.message || "Failed to build health metrics"
19
+ });
20
+ }
21
+ });
@@ -0,0 +1 @@
1
+ export declare function getStorageUsageBytes(): Promise<number>;
@@ -0,0 +1,44 @@
1
+ import { access } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execFileAsync = promisify(execFile);
6
+ const CACHE_TTL_MS = 9e5;
7
+ let cachedBytes = null;
8
+ let inFlight = null;
9
+ export async function getStorageUsageBytes() {
10
+ const now = Date.now();
11
+ if (cachedBytes && now - cachedBytes.at < CACHE_TTL_MS) {
12
+ return cachedBytes.value;
13
+ }
14
+ const storageDir = resolve(process.cwd(), ".storage");
15
+ try {
16
+ await access(storageDir);
17
+ } catch {
18
+ cachedBytes = { value: 0, at: now };
19
+ return 0;
20
+ }
21
+ try {
22
+ if (!inFlight) {
23
+ inFlight = (async () => {
24
+ const { stdout } = await execFileAsync("du", ["-sb", storageDir], {
25
+ timeout: 15e3,
26
+ maxBuffer: 1024 * 1024
27
+ });
28
+ const bytesString = stdout.trim().split(/\s+/)[0];
29
+ const bytes2 = Number.parseInt(bytesString || "", 10);
30
+ if (!Number.isFinite(bytes2) || bytes2 < 0) {
31
+ throw new Error("Unexpected du output");
32
+ }
33
+ return bytes2;
34
+ })();
35
+ }
36
+ const bytes = await inFlight;
37
+ cachedBytes = { value: bytes, at: Date.now() };
38
+ return bytes;
39
+ } catch {
40
+ throw new Error("Failed to calculate storage usage");
41
+ } finally {
42
+ inFlight = null;
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",