@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.
Files changed (38) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +57 -5
  3. package/dist/runtime/app/components/cms/BlocksField.vue +19 -1
  4. package/dist/runtime/app/components/cms/MediaField.vue +14 -1
  5. package/dist/runtime/app/composables/cms-media-keys.d.ts +4 -0
  6. package/dist/runtime/app/composables/cms-media-keys.js +21 -0
  7. package/dist/runtime/app/composables/cms-query.js +57 -4
  8. package/dist/runtime/assets/main.css +1 -1
  9. package/dist/runtime/seed.js +9 -2
  10. package/dist/runtime/server/api/collection.get.js +2 -1
  11. package/dist/runtime/server/api/item.get.js +6 -4
  12. package/dist/runtime/server/api/item.put.js +5 -4
  13. package/dist/runtime/server/plugins/migrate-libsql.js +2 -2
  14. package/dist/runtime/server/routes/graphql.js +2 -0
  15. package/dist/runtime/server/utils/db-d1.d.ts +2 -0
  16. package/dist/runtime/server/utils/db-d1.js +5 -0
  17. package/dist/runtime/server/utils/db-libsql.d.ts +2 -0
  18. package/dist/runtime/server/utils/db-libsql.js +5 -0
  19. package/dist/runtime/server/utils/db-postgres.d.ts +2 -0
  20. package/dist/runtime/server/utils/db-postgres.js +8 -0
  21. package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
  22. package/dist/runtime/server/utils/db-sqlite.js +8 -0
  23. package/dist/runtime/server/utils/graphql.js +3 -4
  24. package/dist/runtime/server/utils/libsql-migrations.d.ts +16 -0
  25. package/dist/runtime/server/utils/libsql-migrations.js +35 -0
  26. package/dist/runtime/server/utils/media-check.d.ts +2 -0
  27. package/dist/runtime/server/utils/media-check.js +20 -0
  28. package/dist/runtime/server/utils/media-references.d.ts +10 -0
  29. package/dist/runtime/server/utils/media-references.js +50 -0
  30. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  31. package/dist/runtime/server/utils/migrate.js +7 -0
  32. package/dist/runtime/server/utils/page-rows.d.ts +51 -0
  33. package/dist/runtime/server/utils/page-rows.js +223 -0
  34. package/dist/runtime/server/utils/page-storage.d.ts +11 -0
  35. package/dist/runtime/server/utils/page-storage.js +42 -0
  36. package/dist/runtime/shared/index.d.ts +7 -0
  37. package/dist/runtime/shared/index.js +21 -0
  38. 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 table = tableFor(name);
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 table = tableFor(name);
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,2 @@
1
+ import type { FieldConfig } from '../../shared/index.js';
2
+ export declare function assertMediaExists(fields: Record<string, FieldConfig>, values: Record<string, unknown>): Promise<void>;
@@ -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>;