@xleddyl/nuxt-cms 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +17 -6
  3. package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
  4. package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
  5. package/dist/runtime/app/components/cms/Drawer.vue +5 -9
  6. package/dist/runtime/app/components/cms/EntryDrawer.vue +11 -3
  7. package/dist/runtime/app/components/cms/EntryForm.vue +0 -1
  8. package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
  9. package/dist/runtime/app/components/cms/Input.vue +7 -4
  10. package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
  11. package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
  12. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
  13. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
  14. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
  15. package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
  16. package/dist/runtime/app/components/cms/Modal.vue +5 -11
  17. package/dist/runtime/app/components/cms/PasswordInput.vue +9 -5
  18. package/dist/runtime/app/components/cms/RelationField.vue +7 -2
  19. package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
  20. package/dist/runtime/app/components/cms/SelectMenu.vue +1 -6
  21. package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
  22. package/dist/runtime/app/components/cms/SettingsEntry.vue +1 -8
  23. package/dist/runtime/app/components/cms/SettingsModal.vue +5 -10
  24. package/dist/runtime/app/components/cms/SettingsUsers.vue +8 -4
  25. package/dist/runtime/app/components/cms/Table.d.vue.ts +0 -7
  26. package/dist/runtime/app/components/cms/Table.vue +7 -49
  27. package/dist/runtime/app/components/cms/Table.vue.d.ts +0 -7
  28. package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
  29. package/dist/runtime/app/components/cms/Textarea.vue +1 -2
  30. package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
  31. package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
  32. package/dist/runtime/app/composables/cms-confirm.js +1 -0
  33. package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
  34. package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
  35. package/dist/runtime/app/composables/cms-media-keys.js +5 -0
  36. package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
  37. package/dist/runtime/app/composables/cms-media-library.js +15 -19
  38. package/dist/runtime/app/composables/cms-media-uploader.d.ts +0 -1
  39. package/dist/runtime/app/composables/cms-media-uploader.js +1 -1
  40. package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
  41. package/dist/runtime/app/composables/cms-overlay.js +23 -0
  42. package/dist/runtime/app/composables/cms-settings.d.ts +0 -1
  43. package/dist/runtime/app/composables/cms-settings.js +1 -1
  44. package/dist/runtime/app/pages/admin-entry.vue +1 -0
  45. package/dist/runtime/app/utils/ui.d.ts +0 -2
  46. package/dist/runtime/assets/main.css +1 -1
  47. package/dist/runtime/generate.js +1 -1
  48. package/dist/runtime/seed.js +11 -1
  49. package/dist/runtime/server/api/collection.get.js +1 -1
  50. package/dist/runtime/server/api/collection.post.js +20 -14
  51. package/dist/runtime/server/api/collection.put.js +12 -12
  52. package/dist/runtime/server/api/item.put.js +14 -13
  53. package/dist/runtime/server/api/media-folder.patch.js +10 -16
  54. package/dist/runtime/server/api/media-move.post.js +12 -8
  55. package/dist/runtime/server/api/media.delete.js +3 -2
  56. package/dist/runtime/server/api/media.post.js +4 -2
  57. package/dist/runtime/server/api/media.put.js +4 -2
  58. package/dist/runtime/server/api/users.post.js +13 -9
  59. package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
  60. package/dist/runtime/server/routes/auth/login.post.js +30 -24
  61. package/dist/runtime/server/utils/db-d1.d.ts +0 -1
  62. package/dist/runtime/server/utils/db-d1.js +3 -4
  63. package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
  64. package/dist/runtime/server/utils/db-libsql.js +3 -4
  65. package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
  66. package/dist/runtime/server/utils/db-postgres.js +1 -4
  67. package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
  68. package/dist/runtime/server/utils/db-sqlite.js +4 -10
  69. package/dist/runtime/server/utils/graphql-depth.js +12 -5
  70. package/dist/runtime/server/utils/graphql.js +4 -3
  71. package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
  72. package/dist/runtime/server/utils/libsql-migrations.js +1 -1
  73. package/dist/runtime/server/utils/media-folders.js +23 -24
  74. package/dist/runtime/server/utils/media.d.ts +1 -0
  75. package/dist/runtime/server/utils/media.js +6 -1
  76. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  77. package/dist/runtime/server/utils/migrate.js +33 -1
  78. package/dist/runtime/server/utils/registry.d.ts +3 -0
  79. package/dist/runtime/server/utils/registry.js +10 -1
  80. package/dist/runtime/server/utils/relations.d.ts +1 -1
  81. package/dist/runtime/server/utils/relations.js +6 -9
  82. package/dist/runtime/server/utils/require-admin.d.ts +0 -2
  83. package/dist/runtime/server/utils/require-admin.js +0 -1
  84. package/dist/runtime/server/utils/timestamps.d.ts +12 -0
  85. package/dist/runtime/server/utils/timestamps.js +48 -0
  86. package/dist/runtime/server/utils/users.js +3 -2
  87. package/dist/runtime/server/utils/write-statements.d.ts +33 -0
  88. package/dist/runtime/server/utils/write-statements.js +75 -0
  89. package/dist/runtime/shared/index.d.ts +14 -0
  90. package/dist/runtime/shared/index.js +13 -0
  91. package/dist/runtime/shared/layout.d.ts +0 -1
  92. package/dist/runtime/shared/layout.js +0 -3
  93. package/dist/runtime/shared/timestamps.d.ts +4 -0
  94. package/dist/runtime/shared/timestamps.js +24 -0
  95. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { createError, defineEventHandler, getValidatedQuery } from "h3";
3
3
  import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
+ import { isMediaFolderMarker } from "../../shared/index.js";
6
7
  import { objectKeySchema } from "../../shared/validation.js";
7
8
  import { useMediaStorage } from "../utils/media.js";
8
9
  import { requireAdmin } from "../utils/require-admin.js";
@@ -13,10 +14,10 @@ export default defineEventHandler(async (event) => {
13
14
  const { key } = await getValidatedQuery(event, querySchema.parse);
14
15
  const db = useDb();
15
16
  const [row] = await db.select().from(cms_media).where(eq(cms_media.key, key)).limit(1);
16
- if (!row) {
17
+ if (!row || isMediaFolderMarker(key)) {
17
18
  throw createError({ statusCode: 404, statusMessage: "Media not found" });
18
19
  }
19
- await store.remove(key);
20
20
  await db.delete(cms_media).where(eq(cms_media.key, key));
21
+ await store.remove(key);
21
22
  return { ok: true };
22
23
  });
@@ -4,11 +4,13 @@ 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 {
10
11
  assertMediaWritable,
11
12
  assertUploadContentType,
13
+ baseContentType,
12
14
  toMediaItem,
13
15
  useMediaConfig
14
16
  } from "../utils/media.js";
@@ -33,13 +35,13 @@ export default defineEventHandler(async (event) => {
33
35
  }
34
36
  const values = {
35
37
  key: body.key,
36
- mime: body.mime ?? null,
38
+ mime: body.mime ? baseContentType(body.mime) : null,
37
39
  size: body.size ?? null,
38
40
  width: body.width ?? null,
39
41
  height: body.height ?? null,
40
42
  alt: body.alt ?? null,
41
43
  folder: normalizeMediaFolder(body.folder)
42
44
  };
43
- 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();
44
46
  return toMediaItem(row, publicUrl);
45
47
  });
@@ -4,11 +4,13 @@ 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";
8
+ import { objectKeySchema } from "../../shared/validation.js";
7
9
  import { useMediaIndex } from "../utils/media-index.js";
8
10
  import { assertMediaWritable, toMediaItem, useMediaConfig } from "../utils/media.js";
9
11
  import { requireAdmin } from "../utils/require-admin.js";
10
12
  const bodySchema = z.object({
11
- key: z.string().min(1).max(1024),
13
+ key: objectKeySchema,
12
14
  alt: z.string().max(1024).nullish(),
13
15
  folder: z.string().max(255).nullish()
14
16
  });
@@ -28,7 +30,7 @@ export default defineEventHandler(async (event) => {
28
30
  throw createError({ statusCode: 404, statusMessage: "Media not found" });
29
31
  }
30
32
  const alt = body.alt ?? null;
31
- 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 } });
32
34
  return toMediaItem({ ...file, alt, createdAt: null }, publicUrl);
33
35
  }
34
36
  assertMediaWritable(media);
@@ -4,6 +4,7 @@ import { useDb } from "#cms-db";
4
4
  import { cms_users } from "#cms-tables";
5
5
  import { MIN_PASSWORD_LENGTH } from "../../shared/index.js";
6
6
  import { customId } from "../utils/custom-id.js";
7
+ import { mapConstraintErrors } from "../utils/db-errors.js";
7
8
  import { hashPassword } from "../utils/password.js";
8
9
  import { isSuperAdminEmail, requireSuperAdmin } from "../utils/require-admin.js";
9
10
  import { findUserByEmail, toAccount } from "../utils/users.js";
@@ -19,14 +20,17 @@ export default defineEventHandler(async (event) => {
19
20
  throw createError({ statusCode: 409, statusMessage: "A user with this email already exists" });
20
21
  }
21
22
  const now = (/* @__PURE__ */ new Date()).toISOString();
22
- const [row] = await useDb().insert(cms_users).values({
23
- id: customId("usr"),
24
- email: body.email,
25
- name: body.name || null,
26
- role: "admin",
27
- passwordHash: await hashPassword(body.password),
28
- createdAt: now,
29
- updatedAt: now
30
- }).returning();
23
+ const passwordHash = await hashPassword(body.password);
24
+ const [row] = await mapConstraintErrors(
25
+ () => useDb().insert(cms_users).values({
26
+ id: customId("usr"),
27
+ email: body.email,
28
+ name: body.name || null,
29
+ role: "admin",
30
+ passwordHash,
31
+ createdAt: now,
32
+ updatedAt: now
33
+ }).returning()
34
+ );
31
35
  return toAccount(row);
32
36
  });
@@ -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
  };
@@ -9,28 +9,30 @@ import { verifyNothing, verifyPassword } from "../../utils/password.js";
9
9
  import { assertSameOrigin } from "../../utils/same-origin.js";
10
10
  import { findUserByEmail, sessionUserFor } from "../../utils/users.js";
11
11
  const credentialsSchema = z.object({
12
- email: z.string().trim().min(1),
13
- password: z.string().min(1)
12
+ email: z.string().trim().min(1).max(254),
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
- }
@@ -1,24 +1,31 @@
1
1
  import { GraphQLError, Kind } from "graphql";
2
2
  const DEFAULT_MAX_DEPTH = 8;
3
3
  const DEFAULT_MAX_FIELDS = 500;
4
- function measure(selectionSet, fragments, visited) {
4
+ function measure(selectionSet, fragments, visited, fragmentDepths) {
5
5
  let max = 0;
6
6
  for (const selection of selectionSet.selections) {
7
7
  if (selection.kind === Kind.FIELD) {
8
8
  if (selection.name.value.startsWith("__")) continue;
9
- const depth = selection.selectionSet ? 1 + measure(selection.selectionSet, fragments, visited) : 1;
9
+ const depth = selection.selectionSet ? 1 + measure(selection.selectionSet, fragments, visited, fragmentDepths) : 1;
10
10
  max = Math.max(max, depth);
11
11
  } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
12
12
  const name = selection.name.value;
13
13
  if (visited.has(name)) continue;
14
+ const known = fragmentDepths.get(name);
15
+ if (known !== void 0) {
16
+ max = Math.max(max, known);
17
+ continue;
18
+ }
14
19
  const fragment = fragments.get(name);
15
20
  if (fragment) {
16
21
  visited.add(name);
17
- max = Math.max(max, measure(fragment.selectionSet, fragments, visited));
22
+ const depth = measure(fragment.selectionSet, fragments, visited, fragmentDepths);
18
23
  visited.delete(name);
24
+ fragmentDepths.set(name, depth);
25
+ max = Math.max(max, depth);
19
26
  }
20
27
  } else if (selection.kind === Kind.INLINE_FRAGMENT) {
21
- max = Math.max(max, measure(selection.selectionSet, fragments, visited));
28
+ max = Math.max(max, measure(selection.selectionSet, fragments, visited, fragmentDepths));
22
29
  }
23
30
  }
24
31
  return max;
@@ -32,7 +39,7 @@ export function createDepthRule(maxDepth) {
32
39
  if (definition.kind === Kind.FRAGMENT_DEFINITION)
33
40
  fragments.set(definition.name.value, definition);
34
41
  }
35
- const depth = measure(node.selectionSet, fragments, /* @__PURE__ */ new Set());
42
+ const depth = measure(node.selectionSet, fragments, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map());
36
43
  if (depth > limit) {
37
44
  context.reportError(
38
45
  new GraphQLError(`Query is too deep: depth ${depth} exceeds the maximum of ${limit}`)
@@ -16,7 +16,7 @@ import {
16
16
  import { GraphQLError, GraphQLScalarType, valueFromASTUntyped } from "graphql";
17
17
  import { createSchema } from "graphql-yoga";
18
18
  import cmsConfig from "#cms-config";
19
- import { useDb } from "#cms-db";
19
+ import { cmsDialect, useDb } from "#cms-db";
20
20
  import * as cmsTables from "#cms-tables";
21
21
  import { useRuntimeConfig } from "#imports";
22
22
  import {
@@ -37,6 +37,7 @@ import {
37
37
  renderGraphqlSdl,
38
38
  typeName
39
39
  } from "../../shared/graphql-sdl.js";
40
+ import { normalizeTimestampFields } from "../../shared/timestamps.js";
40
41
  import { useMediaIndex } from "./media-index.js";
41
42
  import { readPages } from "./page-storage.js";
42
43
  import { getContentI18n, resolveTable, tableColumns } from "./registry.js";
@@ -103,7 +104,7 @@ function resolveLocaleArg(locale) {
103
104
  }
104
105
  function localizeRow(entry, row, locale) {
105
106
  const { defaultLocale } = getContentI18n();
106
- const result = { ...row, [LOCALE]: locale };
107
+ const result = normalizeTimestampFields({ ...row, [LOCALE]: locale });
107
108
  const fields = entryFieldsFor(entry);
108
109
  for (const key of translatableFieldKeys(entry)) {
109
110
  if (isTranslatableMediaField(fields[key])) {
@@ -129,7 +130,7 @@ const OPERATORS = {
129
130
  lte: (column, value) => lte(column, value),
130
131
  like: (column, value) => {
131
132
  const pattern = String(value).replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_").replaceAll("*", "%");
132
- return sql`${column} like ${pattern} escape '\\'`;
133
+ return cmsDialect === "postgres" ? sql`cast(${column} as text) ilike ${pattern} escape '\\'` : sql`${column} like ${pattern} escape '\\'`;
133
134
  },
134
135
  in: (column, value) => inArray(column, value)
135
136
  };
@@ -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,37 +1,36 @@
1
- import { inArray, like, or } 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";
12
+ function escapeLike(value) {
13
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
14
+ }
6
15
  export async function writeMediaFolderMarker(store, folder) {
7
- const key = mediaFolderMarkerKey(folder);
8
- await store.write(key, new Uint8Array(), "application/x-empty");
9
- const values = {
10
- key,
11
- mime: "application/x-empty",
12
- size: 0,
13
- width: null,
14
- height: null,
15
- alt: null,
16
- folder
17
- };
18
- 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 });
19
19
  }
20
20
  export function mediaRowsInFolder(folder) {
21
- return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(or(like(cms_media.folder, `${folder}/%`), like(cms_media.folder, folder)));
21
+ return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(
22
+ or(
23
+ sql`${cms_media.folder} like ${`${escapeLike(folder)}/%`} escape '\\'`,
24
+ eq(cms_media.folder, folder)
25
+ )
26
+ );
22
27
  }
23
28
  export async function deleteMediaRows(store, keys) {
29
+ await runBatch((db) => mediaDeleteStatements(db, cms_media, keys));
24
30
  for (const key of keys) {
25
31
  await store.remove(key);
26
32
  }
27
- for (const chunk of keyChunks(keys)) {
28
- await useDb().delete(cms_media).where(inArray(cms_media.key, chunk));
29
- }
30
33
  }
31
34
  export function keyChunks(keys) {
32
- const chunks = [];
33
- for (let index = 0; index < keys.length; index += KEY_CHUNK) {
34
- chunks.push(keys.slice(index, index + KEY_CHUNK));
35
- }
36
- return chunks;
35
+ return chunks(keys, KEY_CHUNK);
37
36
  }
@@ -20,6 +20,7 @@ export interface MediaStore {
20
20
  write: (key: string, body: Uint8Array<ArrayBuffer>, contentType: string) => Promise<void>;
21
21
  remove: (key: string) => Promise<void>;
22
22
  }
23
+ export declare function baseContentType(contentType: string): string;
23
24
  export declare function isAllowedUploadType(contentType: string): boolean;
24
25
  export declare function assertUploadContentType(contentType: string): void;
25
26
  export declare function assertUploadSize(size: number, maxFileSize: number): void;
@@ -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"]);
@@ -20,8 +21,11 @@ const UPLOAD_TYPES = /* @__PURE__ */ new Set([
20
21
  "application/vnd.ms-powerpoint",
21
22
  "application/vnd.openxmlformats-officedocument.presentationml.presentation"
22
23
  ]);
24
+ export function baseContentType(contentType) {
25
+ return contentType.split(";")[0].trim().toLowerCase();
26
+ }
23
27
  export function isAllowedUploadType(contentType) {
24
- const type = contentType.toLowerCase();
28
+ const type = baseContentType(contentType);
25
29
  return !UPLOAD_TYPE_BLOCKLIST.has(type) && (UPLOAD_TYPES.has(type) || UPLOAD_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix)));
26
30
  }
27
31
  export function assertUploadContentType(contentType) {
@@ -144,6 +148,7 @@ export function useMediaStorage(event) {
144
148
  export function toMediaItem(row, publicUrl) {
145
149
  return {
146
150
  ...row,
151
+ createdAt: normalizeTimestamp(row.createdAt),
147
152
  type: mediaTypeFor(row.mime, row.key),
148
153
  url: publicUrl(row.key)
149
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) {
@@ -18,7 +21,7 @@ async function pendingMigrations() {
18
21
  const migrations = await readMigrations(migrationsDir);
19
22
  if (!migrations.length && Object.keys(cmsConfig).length) {
20
23
  console.error(
21
- `[nuxt-cms] No migrations were found for this build (expected in ${migrationsDir}). CMS tables may be missing \u2014 run the dev server once to generate them, commit server/db/migrations, and rebuild.`
24
+ `[nuxt-cms] No migrations were found for this build (expected in ${migrationsDir}). CMS tables may be missing. Run the dev server once to generate them, commit server/db/migrations, and rebuild.`
22
25
  );
23
26
  }
24
27
  return migrations;
@@ -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;
@@ -9,6 +9,7 @@ import {
9
9
  pageFields,
10
10
  pageRouteOf
11
11
  } from "../../shared/index.js";
12
+ import { normalizeTimestampFields, nowTimestamp } from "../../shared/timestamps.js";
12
13
  import { buildEntrySchema } from "../../shared/validation.js";
13
14
  let contentI18n;
14
15
  export function getContentI18n() {
@@ -30,9 +31,16 @@ export function idColumn(table) {
30
31
  }
31
32
  export function withUpdatedAt(table, values) {
32
33
  const set = { ...values };
33
- if ("updatedAt" in table) set.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
34
+ if ("updatedAt" in table) set.updatedAt = nowTimestamp();
34
35
  return set;
35
36
  }
37
+ export function withTimestamps(table, values) {
38
+ const now = nowTimestamp();
39
+ const stamped = { ...values };
40
+ if ("createdAt" in table) stamped.createdAt ??= now;
41
+ if ("updatedAt" in table) stamped.updatedAt ??= now;
42
+ return stamped;
43
+ }
36
44
  export function getRegistryEntry(event) {
37
45
  const name = getRouterParam(event, "collection");
38
46
  const config = cmsConfig;
@@ -58,6 +66,7 @@ export function encodeColumnValues(entry, values) {
58
66
  return encodeEntryTranslatableMedia(entry, values);
59
67
  }
60
68
  export function decodeRows(entry, rows) {
69
+ for (const row of rows) normalizeTimestampFields(row);
61
70
  return decodeEntryTranslatableMedia(entry, rows, getContentI18n().defaultLocale);
62
71
  }
63
72
  export function parseId(event) {