@xleddyl/nuxt-cms 0.2.1 → 0.2.3

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 (110) hide show
  1. package/README.md +3 -1
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +320 -33
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
  8. package/dist/runtime/app/blocks/resolve.d.ts +21 -0
  9. package/dist/runtime/app/blocks/resolve.js +37 -0
  10. package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
  11. package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
  12. package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
  13. package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
  14. package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
  15. package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
  16. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
  17. package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
  18. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
  19. package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
  20. package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
  21. package/dist/runtime/app/components/cms/EntryForm.vue +9 -4
  22. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  23. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  24. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  25. package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
  26. package/dist/runtime/app/components/cms/Modal.vue +0 -2
  27. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  28. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  29. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  30. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  31. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  32. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
  33. package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
  35. package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
  36. package/dist/runtime/app/composables/cms-confirm.js +1 -0
  37. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  38. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  39. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  40. package/dist/runtime/app/composables/cms-entry.js +51 -1
  41. package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
  42. package/dist/runtime/app/composables/cms-media-library.js +12 -19
  43. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  44. package/dist/runtime/app/composables/cms-settings.js +8 -2
  45. package/dist/runtime/app/layouts/cms-admin.vue +5 -0
  46. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  47. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  48. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  49. package/dist/runtime/app/pages/admin-collection.vue +33 -4
  50. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  51. package/dist/runtime/app/pages/admin-content.vue +284 -0
  52. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  53. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  54. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  55. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  56. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  57. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  58. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  59. package/dist/runtime/app/utils/ui.d.ts +0 -2
  60. package/dist/runtime/app/utils/ui.js +1 -0
  61. package/dist/runtime/assets/main.css +1 -1
  62. package/dist/runtime/seed.js +11 -1
  63. package/dist/runtime/server/api/collection.get.js +3 -2
  64. package/dist/runtime/server/api/collection.post.js +24 -15
  65. package/dist/runtime/server/api/collection.put.js +12 -12
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +16 -14
  68. package/dist/runtime/server/api/media-folder.patch.js +10 -16
  69. package/dist/runtime/server/api/media-move.post.js +12 -8
  70. package/dist/runtime/server/api/media.delete.js +1 -1
  71. package/dist/runtime/server/api/media.post.js +2 -1
  72. package/dist/runtime/server/api/media.put.js +2 -1
  73. package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
  74. package/dist/runtime/server/routes/auth/login.post.js +28 -22
  75. package/dist/runtime/server/utils/db-d1.d.ts +0 -1
  76. package/dist/runtime/server/utils/db-d1.js +3 -4
  77. package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
  78. package/dist/runtime/server/utils/db-libsql.js +3 -4
  79. package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
  80. package/dist/runtime/server/utils/db-postgres.js +1 -4
  81. package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
  82. package/dist/runtime/server/utils/db-sqlite.js +4 -10
  83. package/dist/runtime/server/utils/graphql.js +25 -9
  84. package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
  85. package/dist/runtime/server/utils/libsql-migrations.js +1 -1
  86. package/dist/runtime/server/utils/media-folders.js +14 -23
  87. package/dist/runtime/server/utils/media.js +2 -0
  88. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  89. package/dist/runtime/server/utils/migrate.js +32 -0
  90. package/dist/runtime/server/utils/registry.d.ts +4 -0
  91. package/dist/runtime/server/utils/registry.js +16 -1
  92. package/dist/runtime/server/utils/relations.d.ts +1 -1
  93. package/dist/runtime/server/utils/relations.js +6 -9
  94. package/dist/runtime/server/utils/timestamps.d.ts +12 -0
  95. package/dist/runtime/server/utils/timestamps.js +48 -0
  96. package/dist/runtime/server/utils/users.js +3 -2
  97. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  98. package/dist/runtime/server/utils/visibility.js +12 -0
  99. package/dist/runtime/server/utils/write-statements.d.ts +33 -0
  100. package/dist/runtime/server/utils/write-statements.js +75 -0
  101. package/dist/runtime/shared/graphql-sdl.js +4 -1
  102. package/dist/runtime/shared/index.d.ts +73 -5
  103. package/dist/runtime/shared/index.js +146 -0
  104. package/dist/runtime/shared/preview.d.ts +85 -0
  105. package/dist/runtime/shared/preview.js +219 -0
  106. package/dist/runtime/shared/timestamps.d.ts +4 -0
  107. package/dist/runtime/shared/timestamps.js +24 -0
  108. package/dist/runtime/shared/validation.d.ts +2 -0
  109. package/dist/runtime/shared/validation.js +15 -2
  110. package/package.json +1 -1
@@ -1,20 +1,20 @@
1
- import { eq } from "drizzle-orm";
2
1
  import { createError, defineEventHandler, readValidatedBody } from "h3";
3
2
  import { z } from "zod";
4
- import { useDb } from "#cms-db";
3
+ import { runBatch } from "#cms-db";
5
4
  import { cms_media } from "#cms-tables";
6
5
  import {
7
- isMediaFolderMarker,
8
6
  isWithinMediaFolder,
9
7
  MEDIA_FOLDER_MAX_DEPTH,
10
8
  mediaFolderDepth,
9
+ mediaFolderMarkerKey,
11
10
  mediaFolderSegments,
12
11
  normalizeMediaFolder,
13
12
  rebaseMediaFolder
14
13
  } from "../../shared/index.js";
15
14
  import { useMediaStorage } from "../utils/media.js";
16
- import { mediaRowsInFolder, writeMediaFolderMarker } from "../utils/media-folders.js";
15
+ import { mediaRowsInFolder } from "../utils/media-folders.js";
17
16
  import { requireAdmin } from "../utils/require-admin.js";
17
+ import { mediaFolderRenameStatements, planMediaFolderRename } from "../utils/write-statements.js";
18
18
  const bodySchema = z.object({
19
19
  from: z.string().min(1).max(255),
20
20
  to: z.string().min(1).max(255)
@@ -50,19 +50,13 @@ export default defineEventHandler(async (event) => {
50
50
  if (rebased.some((row) => mediaFolderDepth(row.target) > MEDIA_FOLDER_MAX_DEPTH)) {
51
51
  throw tooDeep();
52
52
  }
53
- const db = useDb();
54
- const fileFolders = /* @__PURE__ */ new Map();
55
- for (const row of rebased) {
56
- if (!isMediaFolderMarker(row.key)) fileFolders.set(row.folder, row.target);
53
+ const plan = planMediaFolderRename(rebased);
54
+ for (const folder of plan.markers) {
55
+ await store.write(mediaFolderMarkerKey(folder), new Uint8Array(), "application/x-empty");
57
56
  }
58
- for (const [folder, target] of fileFolders) {
59
- await db.update(cms_media).set({ folder: target }).where(eq(cms_media.folder, folder));
60
- }
61
- for (const row of rebased) {
62
- if (!isMediaFolderMarker(row.key)) continue;
63
- await writeMediaFolderMarker(store, row.target);
64
- await store.remove(row.key);
65
- await db.delete(cms_media).where(eq(cms_media.key, row.key));
57
+ await runBatch((db) => mediaFolderRenameStatements(db, cms_media, plan));
58
+ for (const key of plan.staleMarkers) {
59
+ await store.remove(key);
66
60
  }
67
61
  return { folder: to, moved: rows.length };
68
62
  });
@@ -1,7 +1,7 @@
1
1
  import { and, inArray, not, like } from "drizzle-orm";
2
2
  import { defineEventHandler, readValidatedBody } from "h3";
3
3
  import { z } from "zod";
4
- import { useDb } from "#cms-db";
4
+ import { runBatch } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
6
  import { MEDIA_FOLDER_MARKER, normalizeMediaFolder } from "../../shared/index.js";
7
7
  import { objectKeySchema } from "../../shared/validation.js";
@@ -17,12 +17,16 @@ export default defineEventHandler(async (event) => {
17
17
  useMediaStorage(event);
18
18
  const { keys, folder } = await readValidatedBody(event, bodySchema.parse);
19
19
  const target = normalizeMediaFolder(folder);
20
- let moved = 0;
21
- for (const chunk of keyChunks([...new Set(keys)])) {
22
- const rows = await useDb().update(cms_media).set({ folder: target }).where(
23
- and(inArray(cms_media.key, chunk), not(like(cms_media.key, `%/${MEDIA_FOLDER_MARKER}`)))
24
- ).returning({ key: cms_media.key });
25
- moved += rows.length;
26
- }
20
+ const results = await runBatch(
21
+ (db) => keyChunks([...new Set(keys)]).map(
22
+ (chunk) => db.update(cms_media).set({ folder: target }).where(
23
+ and(
24
+ inArray(cms_media.key, chunk),
25
+ not(like(cms_media.key, `%/${MEDIA_FOLDER_MARKER}`))
26
+ )
27
+ ).returning({ key: cms_media.key })
28
+ )
29
+ );
30
+ const moved = results.reduce((total, rows) => total + rows.length, 0);
27
31
  return { folder: target, moved };
28
32
  });
@@ -17,7 +17,7 @@ export default defineEventHandler(async (event) => {
17
17
  if (!row || isMediaFolderMarker(key)) {
18
18
  throw createError({ statusCode: 404, statusMessage: "Media not found" });
19
19
  }
20
- await store.remove(key);
21
20
  await db.delete(cms_media).where(eq(cms_media.key, key));
21
+ await store.remove(key);
22
22
  return { ok: true };
23
23
  });
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
6
  import { normalizeMediaFolder } from "../../shared/index.js";
7
+ import { nowTimestamp } from "../../shared/timestamps.js";
7
8
  import { objectKeySchema } from "../../shared/validation.js";
8
9
  import { requireMediaFilePath } from "../utils/media-fs.js";
9
10
  import {
@@ -41,6 +42,6 @@ export default defineEventHandler(async (event) => {
41
42
  alt: body.alt ?? null,
42
43
  folder: normalizeMediaFolder(body.folder)
43
44
  };
44
- const [row] = await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values }).returning();
45
+ const [row] = await useDb().insert(cms_media).values({ ...values, createdAt: nowTimestamp() }).onConflictDoUpdate({ target: cms_media.key, set: values }).returning();
45
46
  return toMediaItem(row, publicUrl);
46
47
  });
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
6
  import { normalizeMediaFolder } from "../../shared/index.js";
7
+ import { nowTimestamp } from "../../shared/timestamps.js";
7
8
  import { objectKeySchema } from "../../shared/validation.js";
8
9
  import { useMediaIndex } from "../utils/media-index.js";
9
10
  import { assertMediaWritable, toMediaItem, useMediaConfig } from "../utils/media.js";
@@ -29,7 +30,7 @@ export default defineEventHandler(async (event) => {
29
30
  throw createError({ statusCode: 404, statusMessage: "Media not found" });
30
31
  }
31
32
  const alt = body.alt ?? null;
32
- await useDb().insert(cms_media).values({ key: body.key, alt }).onConflictDoUpdate({ target: cms_media.key, set: { alt } });
33
+ await useDb().insert(cms_media).values({ key: body.key, alt, createdAt: nowTimestamp() }).onConflictDoUpdate({ target: cms_media.key, set: { alt } });
33
34
  return toMediaItem({ ...file, alt, createdAt: null }, publicUrl);
34
35
  }
35
36
  assertMediaWritable(media);
@@ -1,5 +1,5 @@
1
1
  import { useDb } from "../utils/db-sqlite.js";
2
- import { runCmsMigrations } from "../utils/migrate.js";
2
+ import { runSqliteMigrations } from "../utils/migrate.js";
3
3
  export default async () => {
4
- await runCmsMigrations(useDb());
4
+ await runSqliteMigrations(useDb());
5
5
  };
@@ -12,25 +12,27 @@ const credentialsSchema = z.object({
12
12
  email: z.string().trim().min(1).max(254),
13
13
  password: z.string().min(1).max(256)
14
14
  });
15
- const RATE_WINDOW_MS = 15 * 6e4;
16
- const RATE_MAX_FAILURES = 10;
17
- const RATE_GLOBAL_MAX_FAILURES = 100;
15
+ const RATE_LOCK_MS = 5 * 6e4;
16
+ const RATE_MAX_FAILURES = 5;
18
17
  const RATE_PRUNE_EVERY = 200;
19
- const GLOBAL_KEY = "global";
20
18
  let writesSincePrune = 0;
21
19
  function rateStorage() {
22
20
  return useStorage("cms:login-rate");
23
21
  }
24
- function rateKey(ip) {
25
- return ip.replace(/[^a-z0-9]/gi, "-");
22
+ function rateKey(kind, value) {
23
+ return `${kind}:${value.replace(/[^a-z0-9]/gi, "-")}`;
26
24
  }
27
25
  async function readEntry(key, now) {
28
26
  const entry = await rateStorage().getItem(key);
29
27
  return entry && entry.resetAt > now ? entry : null;
30
28
  }
31
- async function recordFailure(key, now) {
32
- const current = await readEntry(key, now) ?? { count: 0, resetAt: now + RATE_WINDOW_MS };
29
+ function isLocked(entry) {
30
+ return !!entry && entry.count >= RATE_MAX_FAILURES;
31
+ }
32
+ async function recordAttempt(key, now) {
33
+ const current = await readEntry(key, now) ?? { count: 0, resetAt: now + RATE_LOCK_MS };
33
34
  current.count++;
35
+ if (current.count >= RATE_MAX_FAILURES) current.resetAt = now + RATE_LOCK_MS;
34
36
  await rateStorage().setItem(key, current, {
35
37
  ttl: Math.ceil((current.resetAt - now) / 1e3)
36
38
  });
@@ -47,6 +49,15 @@ async function prune(now) {
47
49
  })
48
50
  );
49
51
  }
52
+ async function clearAttempts(keys) {
53
+ await Promise.all(keys.map((key) => rateStorage().removeItem(key)));
54
+ }
55
+ function tooManyAttempts() {
56
+ return createError({
57
+ statusCode: 429,
58
+ statusMessage: "Too many failed attempts, try again in 5 minutes"
59
+ });
60
+ }
50
61
  function safeEqual(a, b) {
51
62
  const hashA = createHash("sha256").update(a).digest();
52
63
  const hashB = createHash("sha256").update(b).digest();
@@ -54,34 +65,29 @@ function safeEqual(a, b) {
54
65
  }
55
66
  export default defineEventHandler(async (event) => {
56
67
  assertSameOrigin(event);
57
- const ip = rateKey(getRequestIP(event, { xForwardedFor: true }) ?? "unknown");
58
68
  const now = Date.now();
59
- const [attempts, globalFailures] = await Promise.all([
60
- readEntry(ip, now),
61
- readEntry(GLOBAL_KEY, now)
62
- ]);
63
- if (globalFailures && globalFailures.count >= RATE_GLOBAL_MAX_FAILURES || attempts && attempts.count >= RATE_MAX_FAILURES) {
64
- throw createError({
65
- statusCode: 429,
66
- statusMessage: "Too many failed attempts, try again later"
67
- });
68
- }
69
69
  const body = await readValidatedBody(event, credentialsSchema.parse);
70
70
  const email = body.email.toLowerCase();
71
+ const keys = [
72
+ rateKey("ip", getRequestIP(event, { xForwardedFor: true }) ?? "unknown"),
73
+ rateKey("email", email)
74
+ ];
75
+ const entries = await Promise.all(keys.map((key) => readEntry(key, now)));
76
+ if (entries.some(isLocked)) throw tooManyAttempts();
77
+ await Promise.all([...keys.map((key) => recordAttempt(key, now)), prune(now)]);
71
78
  const { adminEmail, adminPassword } = useRuntimeConfig(event).cms;
72
79
  const superAdmin = !!adminEmail && !!adminPassword && safeEqual(email, adminEmail.toLowerCase()) && safeEqual(body.password, adminPassword);
73
80
  if (superAdmin) {
74
- await rateStorage().removeItem(ip);
81
+ await clearAttempts(keys);
75
82
  await replaceUserSession(event, { user: { email, role: "superadmin" } });
76
83
  return { loggedIn: true };
77
84
  }
78
85
  const row = await findUserByEmail(email);
79
86
  const passwordOk = row ? await verifyPassword(row.passwordHash, body.password) : await verifyNothing(body.password);
80
87
  if (!row || !passwordOk) {
81
- await Promise.all([recordFailure(ip, now), recordFailure(GLOBAL_KEY, now), prune(now)]);
82
88
  throw createError({ statusCode: 401, statusMessage: "Invalid credentials" });
83
89
  }
84
- await rateStorage().removeItem(ip);
90
+ await clearAttempts(keys);
85
91
  await useDb().update(cms_users).set({ lastLoginAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(cms_users.id, row.id));
86
92
  await replaceUserSession(event, { user: sessionUserFor(row) });
87
93
  return { loggedIn: true };
@@ -8,6 +8,5 @@ export declare function useDb(): import("drizzle-orm/d1").DrizzleD1Database<Reco
8
8
  type Db = ReturnType<typeof useDb>;
9
9
  export type CmsDb = Db;
10
10
  export declare const cmsDialect: 'sqlite' | 'postgres';
11
- export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
12
11
  export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
13
12
  export {};
@@ -22,10 +22,9 @@ export function useDb() {
22
22
  return _db;
23
23
  }
24
24
  export const cmsDialect = "sqlite";
25
- export function withTransaction(fn) {
26
- return fn(useDb());
27
- }
28
25
  export function runBatch(build) {
29
26
  const db = useDb();
30
- return db.batch(build(db));
27
+ const statements = build(db);
28
+ if (!statements.length) return Promise.resolve([]);
29
+ return db.batch(statements);
31
30
  }
@@ -4,6 +4,5 @@ export declare function useDb(): import("drizzle-orm/libsql").LibSQLDatabase<Rec
4
4
  type Db = ReturnType<typeof useDb>;
5
5
  export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
6
6
  export declare const cmsDialect: 'sqlite' | 'postgres';
7
- export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
8
7
  export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
9
8
  export {};
@@ -21,10 +21,9 @@ export function useDb() {
21
21
  return _db;
22
22
  }
23
23
  export const cmsDialect = "sqlite";
24
- export function withTransaction(fn) {
25
- return useDb().transaction((tx) => fn(tx));
26
- }
27
24
  export function runBatch(build) {
28
25
  const db = useDb();
29
- return db.batch(build(db));
26
+ const statements = build(db);
27
+ if (!statements.length) return Promise.resolve([]);
28
+ return db.batch(statements);
30
29
  }
@@ -4,6 +4,5 @@ export declare function useDb(): import("drizzle-orm/node-postgres").NodePgDatab
4
4
  type Db = ReturnType<typeof useDb>;
5
5
  export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
6
6
  export declare const cmsDialect: 'sqlite' | 'postgres';
7
- export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
8
7
  export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
9
8
  export {};
@@ -17,11 +17,8 @@ export function useDb() {
17
17
  return _db;
18
18
  }
19
19
  export const cmsDialect = "postgres";
20
- export function withTransaction(fn) {
21
- return useDb().transaction((tx) => fn(tx));
22
- }
23
20
  export function runBatch(build) {
24
- return withTransaction(async (db) => {
21
+ return useDb().transaction(async (db) => {
25
22
  const results = [];
26
23
  for (const statement of build(db)) results.push(await statement);
27
24
  return results;
@@ -4,5 +4,4 @@ export declare function useDb(): import("drizzle-orm/better-sqlite3").BetterSQLi
4
4
  };
5
5
  export type CmsDb = ReturnType<typeof useDb>;
6
6
  export declare const cmsDialect: 'sqlite' | 'postgres';
7
- export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
8
7
  export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
@@ -18,14 +18,15 @@ export function useDb() {
18
18
  }
19
19
  export const cmsDialect = "sqlite";
20
20
  let txQueue = Promise.resolve();
21
- export function withTransaction(fn) {
21
+ export function runBatch(build) {
22
22
  const db = useDb();
23
23
  const run = txQueue.then(async () => {
24
24
  await db.run(sql`begin immediate`);
25
25
  try {
26
- const result = await fn(db);
26
+ const results = [];
27
+ for (const statement of build(db)) results.push(await statement);
27
28
  await db.run(sql`commit`);
28
- return result;
29
+ return results;
29
30
  } catch (error) {
30
31
  await db.run(sql`rollback`);
31
32
  throw error;
@@ -35,10 +36,3 @@ export function withTransaction(fn) {
35
36
  });
36
37
  return run;
37
38
  }
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
- }
@@ -20,6 +20,8 @@ import { cmsDialect, useDb } from "#cms-db";
20
20
  import * as cmsTables from "#cms-tables";
21
21
  import { useRuntimeConfig } from "#imports";
22
22
  import {
23
+ CONTENT_SLUG_FIELD,
24
+ CONTENT_PUBLISHED_AT_FIELD,
23
25
  decodeTranslatableMedia,
24
26
  entryFieldsFor,
25
27
  hasTranslatableBlockFields,
@@ -29,6 +31,7 @@ import {
29
31
  mediaPublicUrl,
30
32
  mediaTypeFor,
31
33
  pickTranslatedMedia,
34
+ publicBlocks,
32
35
  translatableFieldKeys
33
36
  } from "../../shared/index.js";
34
37
  import {
@@ -37,9 +40,11 @@ import {
37
40
  renderGraphqlSdl,
38
41
  typeName
39
42
  } from "../../shared/graphql-sdl.js";
43
+ import { normalizeTimestampFields } from "../../shared/timestamps.js";
40
44
  import { useMediaIndex } from "./media-index.js";
41
45
  import { readPages } from "./page-storage.js";
42
46
  import { getContentI18n, resolveTable, tableColumns } from "./registry.js";
47
+ import { visibilityConditions } from "./visibility.js";
43
48
  const MAX_LIMIT = 100;
44
49
  const DEFAULT_LIMIT = 50;
45
50
  const LOCALE = Symbol("locale");
@@ -84,9 +89,7 @@ function tableFor(name) {
84
89
  return table;
85
90
  }
86
91
  function publishedFilter(entry, table) {
87
- if (!entry.drafts) return [];
88
- const status = tableColumns(table).status;
89
- return status ? [eq(status, "published")] : [];
92
+ return visibilityConditions(entry, tableColumns(table));
90
93
  }
91
94
  function localeOf(parent) {
92
95
  return parent[LOCALE] ?? getContentI18n().defaultLocale;
@@ -103,7 +106,7 @@ function resolveLocaleArg(locale) {
103
106
  }
104
107
  function localizeRow(entry, row, locale) {
105
108
  const { defaultLocale } = getContentI18n();
106
- const result = { ...row, [LOCALE]: locale };
109
+ const result = normalizeTimestampFields({ ...row, [LOCALE]: locale });
107
110
  const fields = entryFieldsFor(entry);
108
111
  for (const key of translatableFieldKeys(entry)) {
109
112
  if (isTranslatableMediaField(fields[key])) {
@@ -115,8 +118,9 @@ function localizeRow(entry, row, locale) {
115
118
  result[key] = value?.[locale] ?? value?.[defaultLocale] ?? null;
116
119
  }
117
120
  for (const [key, field] of Object.entries(fields)) {
118
- if (isPrivateField(field) || !hasTranslatableBlockFields(field)) continue;
119
- result[key] = localizeBlocks(field, result[key], locale, defaultLocale);
121
+ if (isPrivateField(field) || field.type !== "blocks") continue;
122
+ const visible = publicBlocks(field, result[key]);
123
+ result[key] = hasTranslatableBlockFields(field) ? localizeBlocks(field, visible, locale, defaultLocale) : visible;
120
124
  }
121
125
  return result;
122
126
  }
@@ -161,7 +165,7 @@ function filterConditions(table, filters) {
161
165
  }
162
166
  return conditions;
163
167
  }
164
- function sortOrder(table, sort) {
168
+ function sortOrder(entry, table, sort) {
165
169
  const order = [];
166
170
  for (const part of sort ?? []) {
167
171
  const column = columnFor(table, part.field);
@@ -169,7 +173,10 @@ function sortOrder(table, sort) {
169
173
  order.push(part.direction === "desc" ? desc(column) : asc(column));
170
174
  }
171
175
  if (!order.length) {
172
- const fallback = tableColumns(table).createdAt ?? tableColumns(table).id;
176
+ const columns = tableColumns(table);
177
+ const published = entry.kind === "content" ? columns[CONTENT_PUBLISHED_AT_FIELD] : void 0;
178
+ if (published) order.push(desc(published));
179
+ const fallback = columns.createdAt ?? columns.id;
173
180
  if (fallback) order.push(desc(fallback));
174
181
  }
175
182
  return order;
@@ -349,7 +356,7 @@ export function buildCmsSchema() {
349
356
  const where = [...publishedFilter(entry, table), ...filterConditions(table, args.filters)];
350
357
  const limit = Math.min(MAX_LIMIT, Math.max(0, args.limit ?? DEFAULT_LIMIT));
351
358
  const offset = Math.max(0, args.offset ?? 0);
352
- const rows = await useDb().select().from(table).where(where.length ? and(...where) : void 0).orderBy(...sortOrder(table, args.sort)).limit(limit).offset(offset);
359
+ const rows = await useDb().select().from(table).where(where.length ? and(...where) : void 0).orderBy(...sortOrder(entry, table, args.sort)).limit(limit).offset(offset);
353
360
  return rows.map((row) => localizeRow(entry, row, locale));
354
361
  };
355
362
  queryResolvers[`${name}ById`] = async (_, args) => {
@@ -358,6 +365,15 @@ export function buildCmsSchema() {
358
365
  const [row] = await useDb().select().from(table).where(and(eq(tableColumns(table).id, args.id), ...publishedFilter(entry, table))).limit(1);
359
366
  return row ? localizeRow(entry, row, locale) : null;
360
367
  };
368
+ if (entry.kind === "content") {
369
+ queryResolvers[`${name}BySlug`] = async (_, args) => {
370
+ const locale = resolveLocaleArg(args.locale);
371
+ const table = tableFor(name);
372
+ const slug = tableColumns(table)[CONTENT_SLUG_FIELD];
373
+ const [row] = await useDb().select().from(table).where(and(eq(slug, args.slug), ...publishedFilter(entry, table))).limit(1);
374
+ return row ? localizeRow(entry, row, locale) : null;
375
+ };
376
+ }
361
377
  queryResolvers[`${name}Count`] = async (_, args) => {
362
378
  const table = tableFor(name);
363
379
  const where = [...publishedFilter(entry, table), ...filterConditions(table, args.filters)];
@@ -12,5 +12,6 @@ export interface LibsqlMigrationClient {
12
12
  rows: ArrayLike<unknown>[];
13
13
  }>;
14
14
  batch: (statements: LibsqlStatement[], mode: 'write') => Promise<unknown>;
15
+ migrate: (statements: LibsqlStatement[]) => Promise<unknown>;
15
16
  }
16
17
  export declare function applyLibsqlMigrations(client: LibsqlMigrationClient, migrations: LibsqlMigration[]): Promise<number>;
@@ -24,7 +24,7 @@ export async function applyLibsqlMigrations(client, migrations) {
24
24
  }
25
25
  ]);
26
26
  try {
27
- await client.batch(statements, "write");
27
+ await client.migrate(statements);
28
28
  } catch (error) {
29
29
  const latest = Math.max(...migrations.map((migration) => migration.folderMillis));
30
30
  const appliedNow = await lastAppliedMillis(client).catch(() => null);
@@ -1,24 +1,21 @@
1
- import { eq, inArray, or, sql } from "drizzle-orm";
2
- import { useDb } from "#cms-db";
1
+ import { eq, or, sql } from "drizzle-orm";
2
+ import { runBatch, useDb } from "#cms-db";
3
3
  import { cms_media } from "#cms-tables";
4
4
  import { mediaFolderMarkerKey } from "../../shared/index.js";
5
- const KEY_CHUNK = 90;
5
+ import { nowTimestamp } from "../../shared/timestamps.js";
6
+ import {
7
+ chunks,
8
+ KEY_CHUNK,
9
+ mediaDeleteStatements,
10
+ mediaFolderMarkerValues
11
+ } from "./write-statements.js";
6
12
  function escapeLike(value) {
7
13
  return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
8
14
  }
9
15
  export async function writeMediaFolderMarker(store, folder) {
10
- const key = mediaFolderMarkerKey(folder);
11
- await store.write(key, new Uint8Array(), "application/x-empty");
12
- const values = {
13
- key,
14
- mime: "application/x-empty",
15
- size: 0,
16
- width: null,
17
- height: null,
18
- alt: null,
19
- folder
20
- };
21
- await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values });
16
+ await store.write(mediaFolderMarkerKey(folder), new Uint8Array(), "application/x-empty");
17
+ const values = mediaFolderMarkerValues(folder);
18
+ await useDb().insert(cms_media).values({ ...values, createdAt: nowTimestamp() }).onConflictDoUpdate({ target: cms_media.key, set: values });
22
19
  }
23
20
  export function mediaRowsInFolder(folder) {
24
21
  return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(
@@ -29,17 +26,11 @@ export function mediaRowsInFolder(folder) {
29
26
  );
30
27
  }
31
28
  export async function deleteMediaRows(store, keys) {
29
+ await runBatch((db) => mediaDeleteStatements(db, cms_media, keys));
32
30
  for (const key of keys) {
33
31
  await store.remove(key);
34
32
  }
35
- for (const chunk of keyChunks(keys)) {
36
- await useDb().delete(cms_media).where(inArray(cms_media.key, chunk));
37
- }
38
33
  }
39
34
  export function keyChunks(keys) {
40
- const chunks = [];
41
- for (let index = 0; index < keys.length; index += KEY_CHUNK) {
42
- chunks.push(keys.slice(index, index + KEY_CHUNK));
43
- }
44
- return chunks;
35
+ return chunks(keys, KEY_CHUNK);
45
36
  }
@@ -2,6 +2,7 @@ import { AwsClient } from "aws4fetch";
2
2
  import { createError } from "h3";
3
3
  import { useRuntimeConfig } from "#imports";
4
4
  import { formatFileSize, mediaPublicUrl, mediaTypeFor } from "../../shared/index.js";
5
+ import { normalizeTimestamp } from "../../shared/timestamps.js";
5
6
  import { removeMediaFile, writeMediaFile } from "./media-fs.js";
6
7
  const UPLOAD_TYPE_PREFIXES = ["image/", "video/", "audio/", "font/"];
7
8
  const UPLOAD_TYPE_BLOCKLIST = /* @__PURE__ */ new Set(["image/svg+xml"]);
@@ -147,6 +148,7 @@ export function useMediaStorage(event) {
147
148
  export function toMediaItem(row, publicUrl) {
148
149
  return {
149
150
  ...row,
151
+ createdAt: normalizeTimestamp(row.createdAt),
150
152
  type: mediaTypeFor(row.mime, row.key),
151
153
  url: publicUrl(row.key)
152
154
  };
@@ -5,5 +5,6 @@ export interface CmsMigration {
5
5
  hash: string;
6
6
  }
7
7
  export declare function runCmsMigrations(db: unknown): Promise<void>;
8
+ export declare function runSqliteMigrations(db: unknown): Promise<void>;
8
9
  export declare function runLibsqlMigrations(db: unknown): Promise<void>;
9
10
  export declare function runD1Migrations(binding: unknown): Promise<void>;
@@ -1,7 +1,10 @@
1
+ import { sql } from "drizzle-orm";
1
2
  import cmsConfig from "#cms-config";
2
3
  import { migrations as bundledMigrations } from "#cms-migrations";
4
+ import * as cmsTables from "#cms-tables";
3
5
  import { useRuntimeConfig } from "#imports";
4
6
  import { applyLibsqlMigrations } from "./libsql-migrations.js";
7
+ import { backfillTimestamps } from "./timestamps.js";
5
8
  const MIGRATIONS_TABLE = "__drizzle_migrations";
6
9
  async function readMigrations(migrationsDir) {
7
10
  if (import.meta.dev) {
@@ -29,11 +32,36 @@ export async function runCmsMigrations(db) {
29
32
  const { dialect, session } = db;
30
33
  await dialect.migrate(migrations, session, {});
31
34
  }
35
+ export async function runSqliteMigrations(db) {
36
+ const migrations = await pendingMigrations();
37
+ if (!migrations.length) return;
38
+ const { dialect, session, $client } = db;
39
+ $client.pragma("foreign_keys = OFF");
40
+ try {
41
+ await dialect.migrate(migrations, session, {});
42
+ } finally {
43
+ $client.pragma("foreign_keys = ON");
44
+ }
45
+ const sqlite = db;
46
+ await backfillTimestamps(cmsTables, {
47
+ select: async (query) => sqlite.all(sql.raw(query)),
48
+ write: async (statements) => {
49
+ for (const statement of statements) sqlite.run(sql.raw(statement));
50
+ }
51
+ });
52
+ }
32
53
  export async function runLibsqlMigrations(db) {
33
54
  const migrations = await pendingMigrations();
34
55
  if (!migrations.length) return;
35
56
  const { $client } = db;
36
57
  await applyLibsqlMigrations($client, migrations);
58
+ await backfillTimestamps(cmsTables, {
59
+ select: async (query) => Array.from((await $client.execute(query)).rows),
60
+ write: (statements) => $client.batch(
61
+ statements.map((statement) => ({ sql: statement, args: [] })),
62
+ "write"
63
+ )
64
+ });
37
65
  }
38
66
  export async function runD1Migrations(binding) {
39
67
  const migrations = await pendingMigrations();
@@ -55,4 +83,8 @@ export async function runD1Migrations(binding) {
55
83
  );
56
84
  }
57
85
  if (batch.length) await d1.batch(batch);
86
+ await backfillTimestamps(cmsTables, {
87
+ select: async (query) => (await d1.prepare(query).all()).results,
88
+ write: (statements) => d1.batch(statements.map((statement) => d1.prepare(statement)))
89
+ });
58
90
  }
@@ -9,6 +9,9 @@ export declare function idColumn(table: SQLiteTable): AnySQLiteColumn;
9
9
  export declare function withUpdatedAt(table: SQLiteTable, values: Record<string, unknown>): {
10
10
  [x: string]: unknown;
11
11
  };
12
+ export declare function withTimestamps(table: SQLiteTable, values: Record<string, unknown>): {
13
+ [x: string]: unknown;
14
+ };
12
15
  export declare function getRegistryEntry(event: H3Event): {
13
16
  name: string;
14
17
  entry: CmsEntry;
@@ -20,4 +23,5 @@ export declare function buildValidator(entry: CmsEntry, path?: string): z.ZodObj
20
23
  export declare function requirePageRoute(entry: CmsEntry, key: string): CmsPageRoute;
21
24
  export declare function encodeColumnValues(entry: CmsEntry, values: Record<string, unknown>): Record<string, unknown>;
22
25
  export declare function decodeRows<T extends Record<string, unknown>>(entry: CmsEntry, rows: T[]): T[];
26
+ export declare function withContentDefaults(entry: CmsEntry, values: Record<string, unknown>): Record<string, unknown>;
23
27
  export declare function parseId(event: H3Event): string;