@xleddyl/nuxt-cms 0.1.56 → 0.1.58
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 +57 -5
- package/dist/runtime/app/components/cms/BlocksField.vue +19 -1
- package/dist/runtime/app/components/cms/MediaField.vue +14 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +4 -0
- package/dist/runtime/app/composables/cms-media-keys.js +21 -0
- package/dist/runtime/app/composables/cms-query.js +57 -4
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/seed.js +9 -2
- package/dist/runtime/server/api/collection.get.js +2 -1
- package/dist/runtime/server/api/item.get.js +6 -4
- package/dist/runtime/server/api/item.put.js +5 -4
- package/dist/runtime/server/plugins/migrate-libsql.js +2 -2
- package/dist/runtime/server/routes/graphql.js +2 -0
- package/dist/runtime/server/utils/db-d1.d.ts +2 -0
- package/dist/runtime/server/utils/db-d1.js +5 -0
- package/dist/runtime/server/utils/db-libsql.d.ts +2 -0
- package/dist/runtime/server/utils/db-libsql.js +5 -0
- package/dist/runtime/server/utils/db-postgres.d.ts +2 -0
- package/dist/runtime/server/utils/db-postgres.js +8 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
- package/dist/runtime/server/utils/db-sqlite.js +8 -0
- package/dist/runtime/server/utils/graphql.js +3 -4
- package/dist/runtime/server/utils/libsql-migrations.d.ts +16 -0
- package/dist/runtime/server/utils/libsql-migrations.js +35 -0
- package/dist/runtime/server/utils/media-check.d.ts +2 -0
- package/dist/runtime/server/utils/media-check.js +20 -0
- package/dist/runtime/server/utils/media-references.d.ts +10 -0
- package/dist/runtime/server/utils/media-references.js +50 -0
- package/dist/runtime/server/utils/migrate.d.ts +1 -0
- package/dist/runtime/server/utils/migrate.js +7 -0
- package/dist/runtime/server/utils/page-rows.d.ts +51 -0
- package/dist/runtime/server/utils/page-rows.js +223 -0
- package/dist/runtime/server/utils/page-storage.d.ts +11 -0
- package/dist/runtime/server/utils/page-storage.js +42 -0
- package/dist/runtime/shared/index.d.ts +7 -0
- package/dist/runtime/shared/index.js +21 -0
- package/package.json +1 -1
|
@@ -21,6 +21,11 @@ export function useDb() {
|
|
|
21
21
|
if (!_db) _db = drizzle(useD1Binding());
|
|
22
22
|
return _db;
|
|
23
23
|
}
|
|
24
|
+
export const cmsDialect = "sqlite";
|
|
24
25
|
export function withTransaction(fn) {
|
|
25
26
|
return fn(useDb());
|
|
26
27
|
}
|
|
28
|
+
export function runBatch(build) {
|
|
29
|
+
const db = useDb();
|
|
30
|
+
return db.batch(build(db));
|
|
31
|
+
}
|
|
@@ -3,5 +3,7 @@ export declare function useDb(): import("drizzle-orm/libsql").LibSQLDatabase<Rec
|
|
|
3
3
|
};
|
|
4
4
|
type Db = ReturnType<typeof useDb>;
|
|
5
5
|
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
7
9
|
export {};
|
|
@@ -20,6 +20,11 @@ export function useDb() {
|
|
|
20
20
|
}
|
|
21
21
|
return _db;
|
|
22
22
|
}
|
|
23
|
+
export const cmsDialect = "sqlite";
|
|
23
24
|
export function withTransaction(fn) {
|
|
24
25
|
return useDb().transaction((tx) => fn(tx));
|
|
25
26
|
}
|
|
27
|
+
export function runBatch(build) {
|
|
28
|
+
const db = useDb();
|
|
29
|
+
return db.batch(build(db));
|
|
30
|
+
}
|
|
@@ -3,5 +3,7 @@ export declare function useDb(): import("drizzle-orm/node-postgres").NodePgDatab
|
|
|
3
3
|
};
|
|
4
4
|
type Db = ReturnType<typeof useDb>;
|
|
5
5
|
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
7
9
|
export {};
|
|
@@ -16,6 +16,14 @@ export function useDb() {
|
|
|
16
16
|
}
|
|
17
17
|
return _db;
|
|
18
18
|
}
|
|
19
|
+
export const cmsDialect = "postgres";
|
|
19
20
|
export function withTransaction(fn) {
|
|
20
21
|
return useDb().transaction((tx) => fn(tx));
|
|
21
22
|
}
|
|
23
|
+
export function runBatch(build) {
|
|
24
|
+
return withTransaction(async (db) => {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const statement of build(db)) results.push(await statement);
|
|
27
|
+
return results;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -3,4 +3,6 @@ export declare function useDb(): import("drizzle-orm/better-sqlite3").BetterSQLi
|
|
|
3
3
|
$client: Database.Database;
|
|
4
4
|
};
|
|
5
5
|
export type CmsDb = ReturnType<typeof useDb>;
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
@@ -16,6 +16,7 @@ export function useDb() {
|
|
|
16
16
|
}
|
|
17
17
|
return _db;
|
|
18
18
|
}
|
|
19
|
+
export const cmsDialect = "sqlite";
|
|
19
20
|
let txQueue = Promise.resolve();
|
|
20
21
|
export function withTransaction(fn) {
|
|
21
22
|
const db = useDb();
|
|
@@ -34,3 +35,10 @@ export function withTransaction(fn) {
|
|
|
34
35
|
});
|
|
35
36
|
return run;
|
|
36
37
|
}
|
|
38
|
+
export function runBatch(build) {
|
|
39
|
+
return withTransaction(async (db) => {
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const statement of build(db)) results.push(await statement);
|
|
42
|
+
return results;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
typeName
|
|
39
39
|
} from "../../shared/graphql-sdl.js";
|
|
40
40
|
import { useMediaIndex } from "./media-index.js";
|
|
41
|
+
import { readPages } from "./page-storage.js";
|
|
41
42
|
import { getContentI18n, resolveTable, tableColumns } from "./registry.js";
|
|
42
43
|
const MAX_LIMIT = 100;
|
|
43
44
|
const DEFAULT_LIMIT = 50;
|
|
@@ -322,14 +323,12 @@ export function buildCmsSchema() {
|
|
|
322
323
|
if (entry.kind === "page") {
|
|
323
324
|
queryResolvers[name] = async (_, args) => {
|
|
324
325
|
const locale = resolveLocaleArg(args.locale);
|
|
325
|
-
const
|
|
326
|
-
const rows = await useDb().select().from(table).orderBy(asc(tableColumns(table).path));
|
|
326
|
+
const rows = await readPages(name, entry, tableFor(name), { orderByPath: true });
|
|
327
327
|
return rows.map((row) => localizeRow(entry, row, locale));
|
|
328
328
|
};
|
|
329
329
|
queryResolvers[`${name}ByPath`] = async (_, args) => {
|
|
330
330
|
const locale = resolveLocaleArg(args.locale);
|
|
331
|
-
const
|
|
332
|
-
const [row] = await useDb().select().from(table).where(eq(tableColumns(table).path, args.path)).limit(1);
|
|
331
|
+
const [row] = await readPages(name, entry, tableFor(name), { path: args.path });
|
|
333
332
|
return row ? localizeRow(entry, row, locale) : null;
|
|
334
333
|
};
|
|
335
334
|
continue;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LibsqlMigration {
|
|
2
|
+
sql: string[];
|
|
3
|
+
folderMillis: number;
|
|
4
|
+
hash: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LibsqlStatement {
|
|
7
|
+
sql: string;
|
|
8
|
+
args: (string | number)[];
|
|
9
|
+
}
|
|
10
|
+
export interface LibsqlMigrationClient {
|
|
11
|
+
execute: (statement: string) => Promise<{
|
|
12
|
+
rows: ArrayLike<unknown>[];
|
|
13
|
+
}>;
|
|
14
|
+
batch: (statements: LibsqlStatement[], mode: 'write') => Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare function applyLibsqlMigrations(client: LibsqlMigrationClient, migrations: LibsqlMigration[]): Promise<number>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
2
|
+
async function lastAppliedMillis(client) {
|
|
3
|
+
const { rows } = await client.execute(
|
|
4
|
+
`SELECT id, hash, created_at FROM "${MIGRATIONS_TABLE}" ORDER BY created_at DESC LIMIT 1`
|
|
5
|
+
);
|
|
6
|
+
const createdAt = rows[0]?.[2];
|
|
7
|
+
return createdAt == null ? null : Number(createdAt);
|
|
8
|
+
}
|
|
9
|
+
export async function applyLibsqlMigrations(client, migrations) {
|
|
10
|
+
if (!migrations.length) return 0;
|
|
11
|
+
await client.execute(
|
|
12
|
+
`CREATE TABLE IF NOT EXISTS "${MIGRATIONS_TABLE}" (id SERIAL PRIMARY KEY, hash text NOT NULL, created_at numeric)`
|
|
13
|
+
);
|
|
14
|
+
const lastApplied = await lastAppliedMillis(client);
|
|
15
|
+
const pending = migrations.filter(
|
|
16
|
+
(migration) => lastApplied == null || lastApplied < migration.folderMillis
|
|
17
|
+
);
|
|
18
|
+
if (!pending.length) return 0;
|
|
19
|
+
const statements = pending.flatMap((migration) => [
|
|
20
|
+
...migration.sql.filter((statement) => statement.trim()).map((sql) => ({ sql, args: [] })),
|
|
21
|
+
{
|
|
22
|
+
sql: `INSERT INTO "${MIGRATIONS_TABLE}" ("hash", "created_at") VALUES (?, ?)`,
|
|
23
|
+
args: [migration.hash, migration.folderMillis]
|
|
24
|
+
}
|
|
25
|
+
]);
|
|
26
|
+
try {
|
|
27
|
+
await client.batch(statements, "write");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const latest = Math.max(...migrations.map((migration) => migration.folderMillis));
|
|
30
|
+
const appliedNow = await lastAppliedMillis(client).catch(() => null);
|
|
31
|
+
if (appliedNow != null && appliedNow >= latest) return 0;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return pending.length;
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useDb } from "#cms-db";
|
|
2
|
+
import { cms_media } from "#cms-tables";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { useMediaIndex } from "./media-index.js";
|
|
5
|
+
import { assertKnownMedia, mediaKeysInTable, mediaReferences } from "./media-references.js";
|
|
6
|
+
async function mediaLookup() {
|
|
7
|
+
const { media } = useRuntimeConfig().cms;
|
|
8
|
+
if (media.storage !== "local") {
|
|
9
|
+
return (keys) => mediaKeysInTable(useDb(), cms_media, keys);
|
|
10
|
+
}
|
|
11
|
+
const index = await useMediaIndex();
|
|
12
|
+
if (index.source.kind === "none") return null;
|
|
13
|
+
return async (keys) => new Set(keys.filter((key) => index.get(key)));
|
|
14
|
+
}
|
|
15
|
+
export async function assertMediaExists(fields, values) {
|
|
16
|
+
const references = mediaReferences(fields, values);
|
|
17
|
+
if (!references.length) return;
|
|
18
|
+
const lookup = await mediaLookup();
|
|
19
|
+
if (lookup) await assertKnownMedia(references, lookup);
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { FieldConfig } from '../../shared/index.js';
|
|
3
|
+
export interface MediaReference {
|
|
4
|
+
field: string;
|
|
5
|
+
key: string;
|
|
6
|
+
}
|
|
7
|
+
export type MediaLookup = (keys: string[]) => Promise<Set<string>>;
|
|
8
|
+
export declare function mediaReferences(fields: Record<string, FieldConfig>, values: Record<string, unknown>): MediaReference[];
|
|
9
|
+
export declare function assertKnownMedia(references: MediaReference[], lookup: MediaLookup): Promise<void>;
|
|
10
|
+
export declare function mediaKeysInTable(db: Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'select'>, table: SQLiteTable, keys: string[]): Promise<Set<string>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { inArray } from "drizzle-orm";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
const LOOKUP_CHUNK = 90;
|
|
4
|
+
function mediaKeysOf(value) {
|
|
5
|
+
if (typeof value === "string") return value ? [value] : [];
|
|
6
|
+
if (!value || typeof value !== "object") return [];
|
|
7
|
+
return Object.values(value).filter(
|
|
8
|
+
(key) => typeof key === "string" && key !== ""
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
export function mediaReferences(fields, values) {
|
|
12
|
+
const references = [];
|
|
13
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
14
|
+
const value = values[key];
|
|
15
|
+
if (field.type === "media") {
|
|
16
|
+
for (const mediaKey of mediaKeysOf(value)) references.push({ field: key, key: mediaKey });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (field.type !== "blocks" || !Array.isArray(value)) continue;
|
|
20
|
+
value.forEach((item, index) => {
|
|
21
|
+
const block = item ? field.blocks?.[String(item.type)] : void 0;
|
|
22
|
+
for (const [blockFieldKey, blockField] of Object.entries(block?.fields ?? {})) {
|
|
23
|
+
if (blockField.type !== "media") continue;
|
|
24
|
+
for (const mediaKey of mediaKeysOf(item?.[blockFieldKey])) {
|
|
25
|
+
references.push({ field: `${key}[${index}].${blockFieldKey}`, key: mediaKey });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return references;
|
|
31
|
+
}
|
|
32
|
+
export async function assertKnownMedia(references, lookup) {
|
|
33
|
+
if (!references.length) return;
|
|
34
|
+
const known = await lookup([...new Set(references.map((reference) => reference.key))]);
|
|
35
|
+
const missing = references.filter((reference) => !known.has(reference.key));
|
|
36
|
+
if (!missing.length) return;
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 400,
|
|
39
|
+
statusMessage: `Media not found in the library: ${missing.map((reference) => `'${reference.key}' (${reference.field})`).join(", ")}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export async function mediaKeysInTable(db, table, keys) {
|
|
43
|
+
const column = table.key;
|
|
44
|
+
const found = /* @__PURE__ */ new Set();
|
|
45
|
+
for (let index = 0; index < keys.length; index += LOOKUP_CHUNK) {
|
|
46
|
+
const rows = await db.select({ key: column }).from(table).where(inArray(column, keys.slice(index, index + LOOKUP_CHUNK)));
|
|
47
|
+
for (const row of rows) found.add(row.key);
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
@@ -5,4 +5,5 @@ export interface CmsMigration {
|
|
|
5
5
|
hash: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function runCmsMigrations(db: unknown): Promise<void>;
|
|
8
|
+
export declare function runLibsqlMigrations(db: unknown): Promise<void>;
|
|
8
9
|
export declare function runD1Migrations(binding: unknown): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import cmsConfig from "#cms-config";
|
|
2
2
|
import { migrations as bundledMigrations } from "#cms-migrations";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { applyLibsqlMigrations } from "./libsql-migrations.js";
|
|
4
5
|
const MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
5
6
|
async function readMigrations(migrationsDir) {
|
|
6
7
|
if (import.meta.dev) {
|
|
@@ -28,6 +29,12 @@ export async function runCmsMigrations(db) {
|
|
|
28
29
|
const { dialect, session } = db;
|
|
29
30
|
await dialect.migrate(migrations, session, {});
|
|
30
31
|
}
|
|
32
|
+
export async function runLibsqlMigrations(db) {
|
|
33
|
+
const migrations = await pendingMigrations();
|
|
34
|
+
if (!migrations.length) return;
|
|
35
|
+
const { $client } = db;
|
|
36
|
+
await applyLibsqlMigrations($client, migrations);
|
|
37
|
+
}
|
|
31
38
|
export async function runD1Migrations(binding) {
|
|
32
39
|
const migrations = await pendingMigrations();
|
|
33
40
|
if (!migrations.length) return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm';
|
|
2
|
+
import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import type { CmsEntry, CmsPageRoute } from '../../shared/index.js';
|
|
4
|
+
export type PageDialect = 'sqlite' | 'postgres';
|
|
5
|
+
export type PageDb = Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'select' | 'insert' | 'delete'>;
|
|
6
|
+
export interface PageTables {
|
|
7
|
+
page: SQLiteTable;
|
|
8
|
+
fields: SQLiteTable;
|
|
9
|
+
media: SQLiteTable;
|
|
10
|
+
}
|
|
11
|
+
export interface PageFieldRow {
|
|
12
|
+
pageId: string;
|
|
13
|
+
key: string;
|
|
14
|
+
position: number;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PageMediaRow {
|
|
18
|
+
pageId: string;
|
|
19
|
+
key: string;
|
|
20
|
+
position: number;
|
|
21
|
+
mediaKey: string;
|
|
22
|
+
}
|
|
23
|
+
export interface EncodedPage {
|
|
24
|
+
columns: Record<string, unknown>;
|
|
25
|
+
fields: PageFieldRow[];
|
|
26
|
+
media: PageMediaRow[];
|
|
27
|
+
fieldKeys: string[];
|
|
28
|
+
mediaKeys: string[];
|
|
29
|
+
}
|
|
30
|
+
type Row = Record<string, unknown>;
|
|
31
|
+
export declare const PAGE_BLOCK_TYPE_KEY = "_type";
|
|
32
|
+
export declare function pageBlockKey(fieldKey: string, blockFieldKey: string): string;
|
|
33
|
+
export declare function encodePageRows(entry: CmsEntry, pageId: string, values: Row): EncodedPage;
|
|
34
|
+
export declare function decodePageRow(entry: CmsEntry, row: Row): Row;
|
|
35
|
+
export declare function decodePageRows(entry: CmsEntry, rows: Row[]): Row[];
|
|
36
|
+
export declare function pageSelection(dialect: PageDialect, tables: PageTables): {
|
|
37
|
+
$fields: SQL<unknown>;
|
|
38
|
+
$media: SQL<unknown>;
|
|
39
|
+
};
|
|
40
|
+
export declare function selectPages(db: PageDb, dialect: PageDialect, tables: PageTables): import("drizzle-orm/sqlite-core").SQLiteSelectBase<string, "async" | "sync", unknown, {
|
|
41
|
+
$fields: SQL<unknown>;
|
|
42
|
+
$media: SQL<unknown>;
|
|
43
|
+
}, "partial", Record<string, "not-null">, true, never, {
|
|
44
|
+
$fields: unknown;
|
|
45
|
+
$media: unknown;
|
|
46
|
+
}[], {
|
|
47
|
+
$fields: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
|
|
48
|
+
$media: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function pageWriteStatements(db: PageDb, dialect: PageDialect, tables: PageTables, entry: CmsEntry, route: Pick<CmsPageRoute, 'key' | 'path'>, values: Row): PromiseLike<unknown>[];
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { and, eq, getTableColumns, getTableName, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
PAGE_PATH_FIELD,
|
|
4
|
+
isMultiSelect,
|
|
5
|
+
isTranslatableField,
|
|
6
|
+
isTranslatableMediaField,
|
|
7
|
+
pageAllFields,
|
|
8
|
+
pageRowFields
|
|
9
|
+
} from "../../shared/index.js";
|
|
10
|
+
export const PAGE_BLOCK_TYPE_KEY = "_type";
|
|
11
|
+
const FIELDS_SELECTION = "$fields";
|
|
12
|
+
const MEDIA_SELECTION = "$media";
|
|
13
|
+
const INSERT_CHUNK = 20;
|
|
14
|
+
const KEY_CHUNK = 90;
|
|
15
|
+
const PLAIN_STRING_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"text",
|
|
17
|
+
"richtext",
|
|
18
|
+
"email",
|
|
19
|
+
"slug",
|
|
20
|
+
"date",
|
|
21
|
+
"select",
|
|
22
|
+
"relation"
|
|
23
|
+
]);
|
|
24
|
+
function columnsOf(table) {
|
|
25
|
+
return table;
|
|
26
|
+
}
|
|
27
|
+
function isPlainString(field) {
|
|
28
|
+
return !!field && PLAIN_STRING_TYPES.has(field.type) && !isTranslatableField(field) && !isMultiSelect(field);
|
|
29
|
+
}
|
|
30
|
+
function parseJson(raw) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function encodeValue(field, value) {
|
|
38
|
+
return isPlainString(field) ? String(value) : JSON.stringify(value);
|
|
39
|
+
}
|
|
40
|
+
function decodeValue(field, raw) {
|
|
41
|
+
return isPlainString(field) ? raw : parseJson(raw);
|
|
42
|
+
}
|
|
43
|
+
function encodeMedia(value) {
|
|
44
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
function decodeBlockMedia(field, raw) {
|
|
47
|
+
return isTranslatableMediaField(field) && raw.startsWith("{") ? parseJson(raw) : raw;
|
|
48
|
+
}
|
|
49
|
+
export function pageBlockKey(fieldKey, blockFieldKey) {
|
|
50
|
+
return `${fieldKey}.${blockFieldKey}`;
|
|
51
|
+
}
|
|
52
|
+
function blockOwnedKeys(key, field) {
|
|
53
|
+
const fields = /* @__PURE__ */ new Set([pageBlockKey(key, PAGE_BLOCK_TYPE_KEY)]);
|
|
54
|
+
const media = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const block of Object.values(field.blocks ?? {})) {
|
|
56
|
+
for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
|
|
57
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
58
|
+
if (blockField.type === "media") media.add(blockKey);
|
|
59
|
+
else fields.add(blockKey);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { fields: [...fields], media: [...media] };
|
|
63
|
+
}
|
|
64
|
+
export function encodePageRows(entry, pageId, values) {
|
|
65
|
+
const rowFields = pageRowFields(entry);
|
|
66
|
+
const encoded = { columns: {}, fields: [], media: [], fieldKeys: [], mediaKeys: [] };
|
|
67
|
+
const pushField = (key, position, value) => encoded.fields.push({ pageId, key, position, value });
|
|
68
|
+
const pushMedia = (key, position, value) => encoded.media.push({ pageId, key, position, mediaKey: encodeMedia(value) });
|
|
69
|
+
for (const [key, value] of Object.entries(values)) {
|
|
70
|
+
const field = rowFields[key];
|
|
71
|
+
if (!field) {
|
|
72
|
+
encoded.columns[key] = value;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (field.type === "blocks") {
|
|
76
|
+
const owned = blockOwnedKeys(key, field);
|
|
77
|
+
encoded.fieldKeys.push(...owned.fields);
|
|
78
|
+
encoded.mediaKeys.push(...owned.media);
|
|
79
|
+
if (!Array.isArray(value)) continue;
|
|
80
|
+
value.forEach((item, position) => {
|
|
81
|
+
const type = String(item.type);
|
|
82
|
+
const block = field.blocks?.[type];
|
|
83
|
+
pushField(pageBlockKey(key, PAGE_BLOCK_TYPE_KEY), position, type);
|
|
84
|
+
for (const [blockFieldKey, blockValue] of Object.entries(item)) {
|
|
85
|
+
if (blockFieldKey === "type" || blockValue == null) continue;
|
|
86
|
+
const blockField = block?.fields[blockFieldKey];
|
|
87
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
88
|
+
if (blockField?.type === "media") pushMedia(blockKey, position, blockValue);
|
|
89
|
+
else pushField(blockKey, position, encodeValue(blockField, blockValue));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (field.type === "media") {
|
|
95
|
+
encoded.mediaKeys.push(key);
|
|
96
|
+
if (value != null) pushMedia(key, 0, value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
encoded.fieldKeys.push(key);
|
|
100
|
+
if (value != null) pushField(key, 0, encodeValue(field, value));
|
|
101
|
+
}
|
|
102
|
+
return encoded;
|
|
103
|
+
}
|
|
104
|
+
function parseChildren(value) {
|
|
105
|
+
if (value == null) return [];
|
|
106
|
+
const rows = typeof value === "string" ? parseJson(value) : value;
|
|
107
|
+
return Array.isArray(rows) ? rows : [];
|
|
108
|
+
}
|
|
109
|
+
function indexChildren(rows) {
|
|
110
|
+
const index = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const [key, position, value] of rows) {
|
|
112
|
+
if (key == null || value == null) continue;
|
|
113
|
+
let positions = index.get(key);
|
|
114
|
+
if (!positions) {
|
|
115
|
+
positions = /* @__PURE__ */ new Map();
|
|
116
|
+
index.set(key, positions);
|
|
117
|
+
}
|
|
118
|
+
positions.set(Number(position), String(value));
|
|
119
|
+
}
|
|
120
|
+
return index;
|
|
121
|
+
}
|
|
122
|
+
function decodeBlocks(key, field, fields, media) {
|
|
123
|
+
const types = fields.get(pageBlockKey(key, PAGE_BLOCK_TYPE_KEY));
|
|
124
|
+
if (!types?.size) return null;
|
|
125
|
+
return [...types.entries()].sort(([a], [b]) => a - b).map(([position, type]) => {
|
|
126
|
+
const item = { type };
|
|
127
|
+
for (const [blockFieldKey, blockField] of Object.entries(
|
|
128
|
+
field.blocks?.[type]?.fields ?? {}
|
|
129
|
+
)) {
|
|
130
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
131
|
+
if (blockField.type === "media") {
|
|
132
|
+
const raw2 = media.get(blockKey)?.get(position);
|
|
133
|
+
item[blockFieldKey] = raw2 == null ? null : decodeBlockMedia(blockField, raw2);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const raw = fields.get(blockKey)?.get(position);
|
|
137
|
+
item[blockFieldKey] = raw == null ? null : decodeValue(blockField, raw);
|
|
138
|
+
}
|
|
139
|
+
return item;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function decodeRowField(key, field, fields, media) {
|
|
143
|
+
if (field.type === "blocks") return decodeBlocks(key, field, fields, media);
|
|
144
|
+
if (field.type === "media") return media.get(key)?.get(0) ?? null;
|
|
145
|
+
const raw = fields.get(key)?.get(0);
|
|
146
|
+
return raw == null ? null : decodeValue(field, raw);
|
|
147
|
+
}
|
|
148
|
+
export function decodePageRow(entry, row) {
|
|
149
|
+
const rowFields = pageRowFields(entry);
|
|
150
|
+
const fields = indexChildren(parseChildren(row[FIELDS_SELECTION]));
|
|
151
|
+
const media = indexChildren(parseChildren(row[MEDIA_SELECTION]));
|
|
152
|
+
const result = { id: row.id, [PAGE_PATH_FIELD]: row[PAGE_PATH_FIELD] };
|
|
153
|
+
for (const [key, field] of Object.entries(pageAllFields(entry))) {
|
|
154
|
+
result[key] = Object.hasOwn(rowFields, key) ? decodeRowField(key, field, fields, media) : row[key] ?? null;
|
|
155
|
+
}
|
|
156
|
+
for (const [key, value] of Object.entries(row)) {
|
|
157
|
+
if (key === FIELDS_SELECTION || key === MEDIA_SELECTION || Object.hasOwn(result, key))
|
|
158
|
+
continue;
|
|
159
|
+
result[key] = value;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
export function decodePageRows(entry, rows) {
|
|
164
|
+
return rows.map((row) => decodePageRow(entry, row));
|
|
165
|
+
}
|
|
166
|
+
function qualified(table, column) {
|
|
167
|
+
return sql`${sql.identifier(getTableName(table))}.${sql.identifier(column.name)}`;
|
|
168
|
+
}
|
|
169
|
+
function childAggregate(dialect, page, table, valueKey) {
|
|
170
|
+
const columns = columnsOf(table);
|
|
171
|
+
const owner = qualified(table, columns.pageId);
|
|
172
|
+
const key = qualified(table, columns.key);
|
|
173
|
+
const position = qualified(table, columns.position);
|
|
174
|
+
const value = qualified(table, columns[valueKey]);
|
|
175
|
+
const pageId = qualified(page, columnsOf(page).id);
|
|
176
|
+
const from = sql.identifier(getTableName(table));
|
|
177
|
+
return dialect === "postgres" ? sql`(select coalesce(json_agg(json_build_array(${key}, ${position}, ${value})), '[]'::json) from ${from} where ${owner} = ${pageId})` : sql`(select json_group_array(json_array(${key}, ${position}, ${value})) from ${from} where ${owner} = ${pageId})`;
|
|
178
|
+
}
|
|
179
|
+
export function pageSelection(dialect, tables) {
|
|
180
|
+
return {
|
|
181
|
+
...getTableColumns(tables.page),
|
|
182
|
+
[FIELDS_SELECTION]: childAggregate(dialect, tables.page, tables.fields, "value"),
|
|
183
|
+
[MEDIA_SELECTION]: childAggregate(dialect, tables.page, tables.media, "mediaKey")
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function selectPages(db, dialect, tables) {
|
|
187
|
+
return db.select(pageSelection(dialect, tables)).from(tables.page).$dynamic();
|
|
188
|
+
}
|
|
189
|
+
function chunks(items, size) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (let index = 0; index < items.length; index += size) {
|
|
192
|
+
out.push(items.slice(index, index + size));
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export function pageWriteStatements(db, dialect, tables, entry, route, values) {
|
|
197
|
+
const encoded = encodePageRows(entry, route.key, values);
|
|
198
|
+
const pageId = columnsOf(tables.page).id;
|
|
199
|
+
const fieldColumns = columnsOf(tables.fields);
|
|
200
|
+
const mediaColumns = columnsOf(tables.media);
|
|
201
|
+
const set = encoded.columns;
|
|
202
|
+
const statements = [
|
|
203
|
+
db.insert(tables.page).values({ ...set, id: route.key, [PAGE_PATH_FIELD]: route.path }).onConflictDoUpdate({ target: pageId, set })
|
|
204
|
+
];
|
|
205
|
+
for (const keys of chunks(encoded.fieldKeys, KEY_CHUNK)) {
|
|
206
|
+
statements.push(
|
|
207
|
+
db.delete(tables.fields).where(and(eq(fieldColumns.pageId, route.key), inArray(fieldColumns.key, keys)))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
for (const keys of chunks(encoded.mediaKeys, KEY_CHUNK)) {
|
|
211
|
+
statements.push(
|
|
212
|
+
db.delete(tables.media).where(and(eq(mediaColumns.pageId, route.key), inArray(mediaColumns.key, keys)))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
for (const rows of chunks(encoded.fields, INSERT_CHUNK)) {
|
|
216
|
+
statements.push(db.insert(tables.fields).values(rows));
|
|
217
|
+
}
|
|
218
|
+
for (const rows of chunks(encoded.media, INSERT_CHUNK)) {
|
|
219
|
+
statements.push(db.insert(tables.media).values(rows));
|
|
220
|
+
}
|
|
221
|
+
statements.push(selectPages(db, dialect, tables).where(eq(pageId, route.key)).limit(1));
|
|
222
|
+
return statements;
|
|
223
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { CmsEntry, CmsPageRoute } from '../../shared/index.js';
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export interface PageReadOptions {
|
|
5
|
+
id?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
orderByPath?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function readPages(name: string, entry: CmsEntry, table: SQLiteTable, options?: PageReadOptions): Promise<Row[]>;
|
|
10
|
+
export declare function writePage(name: string, entry: CmsEntry, table: SQLiteTable, route: Pick<CmsPageRoute, 'key' | 'path'>, set: Row): Promise<Row>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { asc, eq } from "drizzle-orm";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
import { cmsDialect, runBatch, useDb } from "#cms-db";
|
|
4
|
+
import * as cmsTables from "#cms-tables";
|
|
5
|
+
import { PAGE_PATH_FIELD, pageFieldsTableName, pageMediaTableName } from "../../shared/index.js";
|
|
6
|
+
import { decodePageRows, pageWriteStatements, selectPages } from "./page-rows.js";
|
|
7
|
+
import { idColumn, tableColumns } from "./registry.js";
|
|
8
|
+
function childTable(name) {
|
|
9
|
+
const table = cmsTables[name];
|
|
10
|
+
if (!table) throw createError({ statusCode: 500, statusMessage: `No table: ${name}` });
|
|
11
|
+
return table;
|
|
12
|
+
}
|
|
13
|
+
function pageTables(name, table) {
|
|
14
|
+
return {
|
|
15
|
+
page: table,
|
|
16
|
+
fields: childTable(pageFieldsTableName(name)),
|
|
17
|
+
media: childTable(pageMediaTableName(name))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function pageFilter(table, options) {
|
|
21
|
+
if (options.id !== void 0) return eq(idColumn(table), options.id);
|
|
22
|
+
if (options.path !== void 0) return eq(tableColumns(table)[PAGE_PATH_FIELD], options.path);
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
export async function readPages(name, entry, table, options = {}) {
|
|
26
|
+
const db = useDb();
|
|
27
|
+
const where = pageFilter(table, options);
|
|
28
|
+
const single = options.id !== void 0 || options.path !== void 0;
|
|
29
|
+
const rows = selectPages(db, cmsDialect, pageTables(name, table));
|
|
30
|
+
if (where) rows.where(where);
|
|
31
|
+
if (options.orderByPath) rows.orderBy(asc(tableColumns(table)[PAGE_PATH_FIELD]));
|
|
32
|
+
if (single) rows.limit(1);
|
|
33
|
+
const result = await rows;
|
|
34
|
+
return decodePageRows(entry, result);
|
|
35
|
+
}
|
|
36
|
+
export async function writePage(name, entry, table, route, set) {
|
|
37
|
+
const tables = pageTables(name, table);
|
|
38
|
+
const results = await runBatch(
|
|
39
|
+
(db) => pageWriteStatements(db, cmsDialect, tables, entry, route, set)
|
|
40
|
+
);
|
|
41
|
+
return decodePageRows(entry, results.at(-1))[0];
|
|
42
|
+
}
|
|
@@ -96,6 +96,7 @@ export declare function translatableBlockFieldKeys(block: BlockConfig): string[]
|
|
|
96
96
|
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
97
97
|
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
98
98
|
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
99
|
+
export declare const CMS_GRAPHQL_BATCH_LIMIT = 20;
|
|
99
100
|
export type CmsEntryKind = 'collection' | 'single' | 'page';
|
|
100
101
|
export interface CmsPageRoute {
|
|
101
102
|
path: string;
|
|
@@ -116,6 +117,7 @@ export interface CmsEntry {
|
|
|
116
117
|
labels?: Record<string, string>;
|
|
117
118
|
overrides?: Record<string, Record<string, FieldConfig | null>>;
|
|
118
119
|
pages?: CmsPageRoute[];
|
|
120
|
+
columns?: string[];
|
|
119
121
|
tabs?: CmsTab[];
|
|
120
122
|
table?: CmsTable;
|
|
121
123
|
}
|
|
@@ -134,6 +136,10 @@ export declare function pageRouteOf(entry: CmsEntry, key: string): CmsPageRoute
|
|
|
134
136
|
export declare function pageOverrideFields(entry: CmsEntry, path: string): Record<string, FieldConfig | null>;
|
|
135
137
|
export declare function pageFields(entry: CmsEntry, path: string): Record<string, FieldConfig>;
|
|
136
138
|
export declare function pageAllFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
139
|
+
export declare function pageColumnFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
140
|
+
export declare function pageRowFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
141
|
+
export declare function pageFieldsTableName(name: string): string;
|
|
142
|
+
export declare function pageMediaTableName(name: string): string;
|
|
137
143
|
type EntryLike = Pick<CmsEntry, 'fields'> & Partial<Pick<CmsEntry, 'kind' | 'overrides'>>;
|
|
138
144
|
export declare function entryFieldsFor(entry: EntryLike, path?: string): Record<string, FieldConfig>;
|
|
139
145
|
export declare function typeName(name: string): string;
|
|
@@ -228,6 +234,7 @@ export interface CmsPageInput extends CmsEntryInputBase {
|
|
|
228
234
|
order?: string[];
|
|
229
235
|
labels?: Record<string, string>;
|
|
230
236
|
overrides?: Record<string, Record<string, CmsFieldInput | null>>;
|
|
237
|
+
columns?: string[];
|
|
231
238
|
}
|
|
232
239
|
export type CmsEntryInput = CmsCollectionInput | CmsSingleInput | CmsPageInput;
|
|
233
240
|
export type CmsConfigInput = Record<string, CmsEntryInput>;
|