@xleddyl/nuxt-cms 0.2.0 → 0.2.2
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 +1 -1
- package/dist/module.mjs +17 -6
- package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
- package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
- package/dist/runtime/app/components/cms/Drawer.vue +5 -9
- package/dist/runtime/app/components/cms/EntryDrawer.vue +11 -3
- package/dist/runtime/app/components/cms/EntryForm.vue +0 -1
- package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Input.vue +7 -4
- package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
- package/dist/runtime/app/components/cms/Modal.vue +5 -11
- package/dist/runtime/app/components/cms/PasswordInput.vue +9 -5
- package/dist/runtime/app/components/cms/RelationField.vue +7 -2
- package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/SelectMenu.vue +1 -6
- package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/SettingsEntry.vue +1 -8
- package/dist/runtime/app/components/cms/SettingsModal.vue +5 -10
- package/dist/runtime/app/components/cms/SettingsUsers.vue +8 -4
- package/dist/runtime/app/components/cms/Table.d.vue.ts +0 -7
- package/dist/runtime/app/components/cms/Table.vue +7 -49
- package/dist/runtime/app/components/cms/Table.vue.d.ts +0 -7
- package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Textarea.vue +1 -2
- package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
- package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
- package/dist/runtime/app/composables/cms-confirm.js +1 -0
- package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
- package/dist/runtime/app/composables/cms-media-keys.js +5 -0
- package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
- package/dist/runtime/app/composables/cms-media-library.js +15 -19
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +0 -1
- package/dist/runtime/app/composables/cms-media-uploader.js +1 -1
- package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
- package/dist/runtime/app/composables/cms-overlay.js +23 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +0 -1
- package/dist/runtime/app/composables/cms-settings.js +1 -1
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/utils/ui.d.ts +0 -2
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/generate.js +1 -1
- package/dist/runtime/seed.js +11 -1
- package/dist/runtime/server/api/collection.get.js +1 -1
- package/dist/runtime/server/api/collection.post.js +20 -14
- package/dist/runtime/server/api/collection.put.js +12 -12
- package/dist/runtime/server/api/item.put.js +14 -13
- package/dist/runtime/server/api/media-folder.patch.js +10 -16
- package/dist/runtime/server/api/media-move.post.js +12 -8
- package/dist/runtime/server/api/media.delete.js +3 -2
- package/dist/runtime/server/api/media.post.js +4 -2
- package/dist/runtime/server/api/media.put.js +4 -2
- package/dist/runtime/server/api/users.post.js +13 -9
- package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
- package/dist/runtime/server/routes/auth/login.post.js +30 -24
- package/dist/runtime/server/utils/db-d1.d.ts +0 -1
- package/dist/runtime/server/utils/db-d1.js +3 -4
- package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
- package/dist/runtime/server/utils/db-libsql.js +3 -4
- package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
- package/dist/runtime/server/utils/db-postgres.js +1 -4
- package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
- package/dist/runtime/server/utils/db-sqlite.js +4 -10
- package/dist/runtime/server/utils/graphql-depth.js +12 -5
- package/dist/runtime/server/utils/graphql.js +4 -3
- package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
- package/dist/runtime/server/utils/libsql-migrations.js +1 -1
- package/dist/runtime/server/utils/media-folders.js +23 -24
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +6 -1
- package/dist/runtime/server/utils/migrate.d.ts +1 -0
- package/dist/runtime/server/utils/migrate.js +33 -1
- package/dist/runtime/server/utils/registry.d.ts +3 -0
- package/dist/runtime/server/utils/registry.js +10 -1
- package/dist/runtime/server/utils/relations.d.ts +1 -1
- package/dist/runtime/server/utils/relations.js +6 -9
- package/dist/runtime/server/utils/require-admin.d.ts +0 -2
- package/dist/runtime/server/utils/require-admin.js +0 -1
- package/dist/runtime/server/utils/timestamps.d.ts +12 -0
- package/dist/runtime/server/utils/timestamps.js +48 -0
- package/dist/runtime/server/utils/users.js +3 -2
- package/dist/runtime/server/utils/write-statements.d.ts +33 -0
- package/dist/runtime/server/utils/write-statements.js +75 -0
- package/dist/runtime/shared/index.d.ts +14 -0
- package/dist/runtime/shared/index.js +13 -0
- package/dist/runtime/shared/layout.d.ts +0 -1
- package/dist/runtime/shared/layout.js +0 -3
- package/dist/runtime/shared/timestamps.d.ts +4 -0
- package/dist/runtime/shared/timestamps.js +24 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ export declare function splitRelationValues(entry: CmsEntry, body: Row): {
|
|
|
8
8
|
lists: Record<string, string[]>;
|
|
9
9
|
};
|
|
10
10
|
export declare function assertRelationTargets(entry: CmsEntry, lists: Record<string, string[]>): Promise<void>;
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function manyToManyStatements(db: CmsDb, name: string, sourceId: string, lists: Record<string, string[]>): PromiseLike<unknown>[];
|
|
12
12
|
export declare function relationTitles(db: CmsDb, entry: CmsEntry, rows: Row[]): Promise<Record<string, Record<string, unknown>>>;
|
|
13
13
|
export declare function attachManyToMany<T extends Row>(db: CmsDb, name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
|
|
14
14
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { asc,
|
|
1
|
+
import { asc, inArray } from "drizzle-orm";
|
|
2
2
|
import { createError } from "h3";
|
|
3
3
|
import cmsConfig from "#cms-config";
|
|
4
4
|
import { useDb } from "#cms-db";
|
|
5
5
|
import * as cmsTables from "#cms-tables";
|
|
6
6
|
import { entryFieldsFor } from "../../shared/index.js";
|
|
7
7
|
import { resolveTable, tableColumns } from "./registry.js";
|
|
8
|
+
import { joinWriteStatements } from "./write-statements.js";
|
|
8
9
|
function manyToManyKeys(entry) {
|
|
9
10
|
return Object.entries(entryFieldsFor(entry)).filter(([, field]) => field.type === "relation" && field.cardinality === "many-to-many").map(([key]) => key);
|
|
10
11
|
}
|
|
@@ -50,14 +51,10 @@ export async function assertRelationTargets(entry, lists) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (ids.length) {
|
|
58
|
-
await db.insert(join.table).values(ids.map((targetId, position) => ({ sourceId, targetId, position })));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
54
|
+
export function manyToManyStatements(db, name, sourceId, lists) {
|
|
55
|
+
return Object.entries(lists).flatMap(
|
|
56
|
+
([key, ids]) => joinWriteStatements(db, joinTable(name, key), sourceId, ids)
|
|
57
|
+
);
|
|
61
58
|
}
|
|
62
59
|
export async function relationTitles(db, entry, rows) {
|
|
63
60
|
const config = cmsConfig;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { CmsSessionUser } from '../../shared/index.js';
|
|
3
|
-
import { assertSameOrigin } from './same-origin.js';
|
|
4
|
-
export { assertSameOrigin };
|
|
5
3
|
export declare function isSuperAdminEmail(event: H3Event, email: string): boolean;
|
|
6
4
|
export declare function requireAdmin(event: H3Event): Promise<CmsSessionUser>;
|
|
7
5
|
export declare function requireSuperAdmin(event: H3Event): Promise<CmsSessionUser>;
|
|
@@ -2,7 +2,6 @@ import { createError } from "h3";
|
|
|
2
2
|
import { getUserSession, useRuntimeConfig } from "#imports";
|
|
3
3
|
import { assertSameOrigin } from "./same-origin.js";
|
|
4
4
|
import { findUserById, passwordStamp } from "./users.js";
|
|
5
|
-
export { assertSameOrigin };
|
|
6
5
|
export function isSuperAdminEmail(event, email) {
|
|
7
6
|
const { adminEmail } = useRuntimeConfig(event).cms;
|
|
8
7
|
return !!adminEmail && email.toLowerCase() === adminEmail.toLowerCase();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TimestampColumn {
|
|
2
|
+
table: string;
|
|
3
|
+
column: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TimestampBackfillDriver {
|
|
6
|
+
select: (query: string) => Promise<unknown[]>;
|
|
7
|
+
write: (statements: string[]) => Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function timestampColumns(tables: Record<string, unknown>): TimestampColumn[];
|
|
10
|
+
export declare function timestampProbeQuery(columns: TimestampColumn[]): string;
|
|
11
|
+
export declare function timestampBackfillStatement({ table, column }: TimestampColumn): string;
|
|
12
|
+
export declare function backfillTimestamps(tables: Record<string, unknown>, driver: TimestampBackfillDriver): Promise<number>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { is } from "drizzle-orm";
|
|
2
|
+
import { SQLiteTable, getTableConfig } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { SQLITE_ISO_FORMAT } from "../../shared/timestamps.js";
|
|
4
|
+
const TIMESTAMP_COLUMNS = /* @__PURE__ */ new Set(["created_at", "updated_at"]);
|
|
5
|
+
function quoteIdentifier(name) {
|
|
6
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
function legacyCondition(column) {
|
|
9
|
+
const target = quoteIdentifier(column);
|
|
10
|
+
return `${target} NOT LIKE '%T%' AND strftime('${SQLITE_ISO_FORMAT}', ${target}) IS NOT NULL`;
|
|
11
|
+
}
|
|
12
|
+
export function timestampColumns(tables) {
|
|
13
|
+
const found = [];
|
|
14
|
+
for (const table of Object.values(tables)) {
|
|
15
|
+
if (!is(table, SQLiteTable)) continue;
|
|
16
|
+
const { name, columns } = getTableConfig(table);
|
|
17
|
+
for (const column of columns) {
|
|
18
|
+
if (TIMESTAMP_COLUMNS.has(column.name)) found.push({ table: name, column: column.name });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
export function timestampProbeQuery(columns) {
|
|
24
|
+
return columns.map(
|
|
25
|
+
({ table, column }, index) => `SELECT ${index} AS target WHERE EXISTS (SELECT 1 FROM ${quoteIdentifier(
|
|
26
|
+
table
|
|
27
|
+
)} WHERE ${legacyCondition(column)})`
|
|
28
|
+
).join(" UNION ALL ");
|
|
29
|
+
}
|
|
30
|
+
export function timestampBackfillStatement({ table, column }) {
|
|
31
|
+
const target = quoteIdentifier(column);
|
|
32
|
+
return `UPDATE ${quoteIdentifier(
|
|
33
|
+
table
|
|
34
|
+
)} SET ${target} = strftime('${SQLITE_ISO_FORMAT}', ${target}) WHERE ${legacyCondition(column)}`;
|
|
35
|
+
}
|
|
36
|
+
function targetIndex(row) {
|
|
37
|
+
if (Array.isArray(row)) return Number(row[0]);
|
|
38
|
+
return Number(row.target);
|
|
39
|
+
}
|
|
40
|
+
export async function backfillTimestamps(tables, driver) {
|
|
41
|
+
const columns = timestampColumns(tables);
|
|
42
|
+
if (!columns.length) return 0;
|
|
43
|
+
const rows = await driver.select(timestampProbeQuery(columns));
|
|
44
|
+
const stale = rows.map((row) => columns[targetIndex(row)]).filter((column) => !!column);
|
|
45
|
+
if (!stale.length) return 0;
|
|
46
|
+
await driver.write(stale.map(timestampBackfillStatement));
|
|
47
|
+
return stale.length;
|
|
48
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { useDb } from "#cms-db";
|
|
3
3
|
import { cms_users } from "#cms-tables";
|
|
4
|
+
import { normalizeTimestamp } from "../../shared/timestamps.js";
|
|
4
5
|
export async function findUserByEmail(email) {
|
|
5
6
|
const [row] = await useDb().select().from(cms_users).where(eq(cms_users.email, email.toLowerCase()));
|
|
6
7
|
return row;
|
|
@@ -28,7 +29,7 @@ export function toAccount(row) {
|
|
|
28
29
|
email: row.email,
|
|
29
30
|
name: row.name ?? null,
|
|
30
31
|
role: row.role,
|
|
31
|
-
lastLoginAt: row.lastLoginAt ?? null,
|
|
32
|
-
createdAt: String(row.createdAt)
|
|
32
|
+
lastLoginAt: normalizeTimestamp(row.lastLoginAt ?? null),
|
|
33
|
+
createdAt: String(normalizeTimestamp(row.createdAt))
|
|
33
34
|
};
|
|
34
35
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AnySQLiteColumn, BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
export type WriteDb = Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'insert' | 'update' | 'delete'>;
|
|
3
|
+
export interface JoinTable {
|
|
4
|
+
table: SQLiteTable;
|
|
5
|
+
sourceId: AnySQLiteColumn;
|
|
6
|
+
targetId: AnySQLiteColumn;
|
|
7
|
+
position: AnySQLiteColumn;
|
|
8
|
+
}
|
|
9
|
+
export interface RebasedMediaRow {
|
|
10
|
+
key: string;
|
|
11
|
+
folder: string | null;
|
|
12
|
+
target: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface MediaFolderRename {
|
|
15
|
+
folders: [string, string | null][];
|
|
16
|
+
markers: string[];
|
|
17
|
+
staleMarkers: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const KEY_CHUNK = 90;
|
|
20
|
+
export declare function chunks<T>(items: T[], size: number): T[][];
|
|
21
|
+
export declare function joinWriteStatements(db: WriteDb, join: JoinTable, sourceId: string, ids: string[]): PromiseLike<unknown>[];
|
|
22
|
+
export declare function mediaFolderMarkerValues(folder: string): {
|
|
23
|
+
key: string;
|
|
24
|
+
mime: string;
|
|
25
|
+
size: number;
|
|
26
|
+
width: null;
|
|
27
|
+
height: null;
|
|
28
|
+
alt: null;
|
|
29
|
+
folder: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function planMediaFolderRename(rows: RebasedMediaRow[]): MediaFolderRename;
|
|
32
|
+
export declare function mediaFolderRenameStatements(db: WriteDb, table: SQLiteTable, plan: MediaFolderRename): PromiseLike<unknown>[];
|
|
33
|
+
export declare function mediaDeleteStatements(db: WriteDb, table: SQLiteTable, keys: string[]): PromiseLike<unknown>[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eq, getTableColumns, inArray } from "drizzle-orm";
|
|
2
|
+
import { isMediaFolderMarker, mediaFolderDepth, mediaFolderMarkerKey } from "../../shared/index.js";
|
|
3
|
+
import { nowTimestamp } from "../../shared/timestamps.js";
|
|
4
|
+
export const KEY_CHUNK = 90;
|
|
5
|
+
const JOIN_INSERT_CHUNK = 30;
|
|
6
|
+
export function chunks(items, size) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let index = 0; index < items.length; index += size) {
|
|
9
|
+
out.push(items.slice(index, index + size));
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
export function joinWriteStatements(db, join, sourceId, ids) {
|
|
14
|
+
const statements = [
|
|
15
|
+
db.delete(join.table).where(eq(join.sourceId, sourceId))
|
|
16
|
+
];
|
|
17
|
+
const rows = ids.map((targetId, position) => ({ sourceId, targetId, position }));
|
|
18
|
+
for (const chunk of chunks(rows, JOIN_INSERT_CHUNK)) {
|
|
19
|
+
statements.push(db.insert(join.table).values(chunk));
|
|
20
|
+
}
|
|
21
|
+
return statements;
|
|
22
|
+
}
|
|
23
|
+
export function mediaFolderMarkerValues(folder) {
|
|
24
|
+
return {
|
|
25
|
+
key: mediaFolderMarkerKey(folder),
|
|
26
|
+
mime: "application/x-empty",
|
|
27
|
+
size: 0,
|
|
28
|
+
width: null,
|
|
29
|
+
height: null,
|
|
30
|
+
alt: null,
|
|
31
|
+
folder
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function planMediaFolderRename(rows) {
|
|
35
|
+
const folders = /* @__PURE__ */ new Map();
|
|
36
|
+
const markers = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
if (!isMediaFolderMarker(row.key)) {
|
|
39
|
+
if (row.folder) folders.set(row.folder, row.target);
|
|
40
|
+
} else if (row.target) {
|
|
41
|
+
markers.add(row.target);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const markerKeys = new Set([...markers].map(mediaFolderMarkerKey));
|
|
45
|
+
return {
|
|
46
|
+
folders: [...folders].sort(([a], [b]) => mediaFolderDepth(a) - mediaFolderDepth(b)),
|
|
47
|
+
markers: [...markers],
|
|
48
|
+
staleMarkers: rows.filter((row) => isMediaFolderMarker(row.key) && !markerKeys.has(row.key)).map((row) => row.key)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function mediaColumns(table) {
|
|
52
|
+
const columns = getTableColumns(table);
|
|
53
|
+
return { key: columns.key, folder: columns.folder };
|
|
54
|
+
}
|
|
55
|
+
export function mediaFolderRenameStatements(db, table, plan) {
|
|
56
|
+
const columns = mediaColumns(table);
|
|
57
|
+
const statements = [];
|
|
58
|
+
for (const [folder, target] of plan.folders) {
|
|
59
|
+
statements.push(db.update(table).set({ folder: target }).where(eq(columns.folder, folder)));
|
|
60
|
+
}
|
|
61
|
+
for (const folder of plan.markers) {
|
|
62
|
+
const values = mediaFolderMarkerValues(folder);
|
|
63
|
+
statements.push(
|
|
64
|
+
db.insert(table).values({ ...values, createdAt: nowTimestamp() }).onConflictDoUpdate({ target: columns.key, set: values })
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
for (const keys of chunks(plan.staleMarkers, KEY_CHUNK)) {
|
|
68
|
+
statements.push(db.delete(table).where(inArray(columns.key, keys)));
|
|
69
|
+
}
|
|
70
|
+
return statements;
|
|
71
|
+
}
|
|
72
|
+
export function mediaDeleteStatements(db, table, keys) {
|
|
73
|
+
const { key } = mediaColumns(table);
|
|
74
|
+
return chunks(keys, KEY_CHUNK).map((chunk) => db.delete(table).where(inArray(key, chunk)));
|
|
75
|
+
}
|
|
@@ -60,6 +60,20 @@ export interface MediaUsage {
|
|
|
60
60
|
title: string | null;
|
|
61
61
|
field: string;
|
|
62
62
|
}
|
|
63
|
+
export interface MediaUsageEntry {
|
|
64
|
+
collection: string;
|
|
65
|
+
label: string;
|
|
66
|
+
kind: CmsEntryKind;
|
|
67
|
+
id: string | null;
|
|
68
|
+
title: string | null;
|
|
69
|
+
fields: string[];
|
|
70
|
+
}
|
|
71
|
+
export declare function groupMediaUsage(usage: MediaUsage[]): MediaUsageEntry[];
|
|
72
|
+
export interface MediaUsageGroup {
|
|
73
|
+
key: string;
|
|
74
|
+
entries: MediaUsageEntry[];
|
|
75
|
+
}
|
|
76
|
+
export declare function mediaUsageGroups(usage: Record<string, MediaUsage[]>, keys: string[]): MediaUsageGroup[];
|
|
63
77
|
export interface BlockConfig {
|
|
64
78
|
label: string;
|
|
65
79
|
fields: Record<string, FieldConfig>;
|
|
@@ -110,6 +110,19 @@ export function expandMediaFolders(folders) {
|
|
|
110
110
|
}
|
|
111
111
|
return [...expanded].sort();
|
|
112
112
|
}
|
|
113
|
+
export function groupMediaUsage(usage) {
|
|
114
|
+
const entries = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const { field, ...rest } of usage) {
|
|
116
|
+
const id = `${rest.collection}\0${rest.id ?? ""}`;
|
|
117
|
+
const entry = entries.get(id);
|
|
118
|
+
if (!entry) entries.set(id, { ...rest, fields: [field] });
|
|
119
|
+
else if (!entry.fields.includes(field)) entry.fields.push(field);
|
|
120
|
+
}
|
|
121
|
+
return [...entries.values()];
|
|
122
|
+
}
|
|
123
|
+
export function mediaUsageGroups(usage, keys) {
|
|
124
|
+
return [...new Set(keys)].map((key) => ({ key, entries: groupMediaUsage(usage[key] ?? []) })).filter((group) => group.entries.length > 0);
|
|
125
|
+
}
|
|
113
126
|
export function isPrivateField(field) {
|
|
114
127
|
return !!field.private;
|
|
115
128
|
}
|
|
@@ -26,7 +26,6 @@ export interface CmsEntrySettings {
|
|
|
26
26
|
}
|
|
27
27
|
export type CmsSettingsMap = Record<string, CmsEntrySettings>;
|
|
28
28
|
export declare function layoutFromConfig(input: CmsLayoutBlock[] | undefined): CmsFormLayout;
|
|
29
|
-
export declare function layoutKeys(input: CmsLayoutBlock[] | undefined): string[];
|
|
30
29
|
export declare function resolveFormLayout(fields: Record<string, FieldConfig>, layout: CmsFormLayout | undefined): CmsFormLayout;
|
|
31
30
|
export declare function defaultListColumns(fields: Record<string, FieldConfig>, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
32
31
|
export declare function resolveListColumns(fields: Record<string, FieldConfig>, saved: string[] | undefined, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
@@ -30,9 +30,6 @@ function sectionsFromBlocks(blocks) {
|
|
|
30
30
|
export function layoutFromConfig(input) {
|
|
31
31
|
return { main: sectionsFromBlocks(input) };
|
|
32
32
|
}
|
|
33
|
-
export function layoutKeys(input) {
|
|
34
|
-
return layoutFromConfig(input).main.flatMap((section) => section.rows.flat());
|
|
35
|
-
}
|
|
36
33
|
function cloneSection(section) {
|
|
37
34
|
return { ...section, rows: section.rows.map((row) => [...row]) };
|
|
38
35
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const SQLITE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
2
|
+
export declare function nowTimestamp(): string;
|
|
3
|
+
export declare function normalizeTimestamp<T>(value: T): T | string;
|
|
4
|
+
export declare function normalizeTimestampFields<T extends Record<string, unknown>>(row: T, keys?: readonly string[]): T;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const SQLITE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
2
|
+
export function nowTimestamp() {
|
|
3
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4
|
+
}
|
|
5
|
+
const TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/;
|
|
6
|
+
export function normalizeTimestamp(value) {
|
|
7
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
8
|
+
if (typeof value !== "string") return value;
|
|
9
|
+
const match = TIMESTAMP_PATTERN.exec(value.trim());
|
|
10
|
+
if (!match) return value;
|
|
11
|
+
const [, date, time, fraction = "", zone] = match;
|
|
12
|
+
const millis = fraction.padEnd(3, "0").slice(0, 3);
|
|
13
|
+
const utc = `${date}T${time}.${millis}Z`;
|
|
14
|
+
if (!zone || zone === "Z") return utc;
|
|
15
|
+
const offset = zone.length === 3 ? `${zone}:00` : zone.includes(":") ? zone : `${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
16
|
+
const parsed = /* @__PURE__ */ new Date(`${date}T${time}.${millis}${offset}`);
|
|
17
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
|
|
18
|
+
}
|
|
19
|
+
export function normalizeTimestampFields(row, keys = ["createdAt", "updatedAt"]) {
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (Object.hasOwn(row, key)) row[key] = normalizeTimestamp(row[key]);
|
|
22
|
+
}
|
|
23
|
+
return row;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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)",
|