@plank-cms/plank 0.22.0 → 0.24.0

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.
@@ -12,7 +12,7 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap"
13
13
  rel="stylesheet"
14
14
  />
15
- <script type="module" crossorigin src="/admin/assets/index-dWzhX4Ev.js"></script>
15
+ <script type="module" crossorigin src="/admin/assets/index-BHOPEoNL.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/admin/assets/index-BSi0iXTe.css">
17
17
  </head>
18
18
  <body>
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { randomBytes } from "crypto";
7
7
  import { resolve, join } from "path";
8
8
  import fs from "fs-extra";
9
9
  import { execa } from "execa";
10
- var PACKAGE_VERSION = "0.22.0";
10
+ var PACKAGE_VERSION = "0.24.0";
11
11
  function generateSecret() {
12
12
  return randomBytes(32).toString("hex");
13
13
  }
@@ -101,7 +101,7 @@ import { dirname, join as join2, resolve as resolve2 } from "path";
101
101
  async function start() {
102
102
  config({ path: resolve2(process.cwd(), ".env") });
103
103
  process.env.PLANK_ADMIN_DIST = join2(dirname(fileURLToPath(import.meta.url)), "admin");
104
- const { start: startServer } = await import("./server-YVELAF42.js");
104
+ const { start: startServer } = await import("./server-FYUZINCY.js");
105
105
  await startServer();
106
106
  }
107
107
 
@@ -135,7 +135,7 @@ async function publishScheduled() {
135
135
  `UPDATE ${table_name} SET
136
136
  status = 'published',
137
137
  published_data = ${snapshotExpr(table_name)},
138
- published_at = NOW(),
138
+ published_at = COALESCE(published_at, NOW()),
139
139
  scheduled_for = NULL,
140
140
  updated_at = NOW()
141
141
  WHERE id = $1`,
@@ -0,0 +1,79 @@
1
+ ALTER TABLE plank_users
2
+ ADD COLUMN IF NOT EXISTS public_author_slug VARCHAR(255);
3
+
4
+ DO $$
5
+ DECLARE
6
+ usr RECORD;
7
+ base_slug TEXT;
8
+ next_slug TEXT;
9
+ suffix_num INTEGER;
10
+ BEGIN
11
+ FOR usr IN
12
+ SELECT id, email, first_name, last_name
13
+ FROM plank_users
14
+ WHERE public_author_slug IS NULL OR btrim(public_author_slug) = ''
15
+ ORDER BY created_at, id
16
+ LOOP
17
+ base_slug := lower(
18
+ trim(
19
+ both '-'
20
+ FROM regexp_replace(
21
+ regexp_replace(
22
+ regexp_replace(
23
+ coalesce(
24
+ nullif(trim(concat_ws(' ', usr.first_name, usr.last_name)), ''),
25
+ nullif(split_part(usr.email, '@', 1), ''),
26
+ 'author'
27
+ ),
28
+ '[áàäâãåÁÀÄÂÃÅ]',
29
+ 'a',
30
+ 'g'
31
+ ),
32
+ '[éèëêÉÈËÊ]',
33
+ 'e',
34
+ 'g'
35
+ ),
36
+ '\s+',
37
+ '-',
38
+ 'g'
39
+ )
40
+ )
41
+ );
42
+
43
+ base_slug := regexp_replace(base_slug, '[íìïîÍÌÏÎ]', 'i', 'g');
44
+ base_slug := regexp_replace(base_slug, '[óòöôõÓÒÖÔÕ]', 'o', 'g');
45
+ base_slug := regexp_replace(base_slug, '[úùüûÚÙÜÛ]', 'u', 'g');
46
+ base_slug := regexp_replace(base_slug, '[ñÑ]', 'n', 'g');
47
+ base_slug := regexp_replace(base_slug, '[çÇ]', 'c', 'g');
48
+ base_slug := regexp_replace(base_slug, '[^a-z0-9-]', '', 'g');
49
+ base_slug := trim(both '-' FROM base_slug);
50
+
51
+ IF base_slug IS NULL OR base_slug = '' THEN
52
+ base_slug := 'author';
53
+ END IF;
54
+
55
+ next_slug := base_slug;
56
+ suffix_num := 2;
57
+
58
+ WHILE EXISTS (
59
+ SELECT 1
60
+ FROM plank_users
61
+ WHERE public_author_slug = next_slug
62
+ AND id <> usr.id
63
+ ) LOOP
64
+ next_slug := base_slug || '-' || suffix_num;
65
+ suffix_num := suffix_num + 1;
66
+ END LOOP;
67
+
68
+ UPDATE plank_users
69
+ SET public_author_slug = next_slug
70
+ WHERE id = usr.id;
71
+ END LOOP;
72
+ END;
73
+ $$;
74
+
75
+ ALTER TABLE plank_users
76
+ ALTER COLUMN public_author_slug SET NOT NULL;
77
+
78
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_plank_users_public_author_slug
79
+ ON plank_users(public_author_slug);
@@ -0,0 +1,82 @@
1
+ DO $$
2
+ DECLARE
3
+ usr RECORD;
4
+ base_slug TEXT;
5
+ next_slug TEXT;
6
+ suffix_num INTEGER;
7
+ BEGIN
8
+ FOR usr IN
9
+ SELECT id, email, first_name, last_name
10
+ FROM plank_users
11
+ ORDER BY created_at, id
12
+ LOOP
13
+ base_slug := lower(
14
+ trim(
15
+ both '-'
16
+ FROM regexp_replace(
17
+ regexp_replace(
18
+ regexp_replace(
19
+ coalesce(
20
+ nullif(trim(concat_ws(' ', usr.first_name, usr.last_name)), ''),
21
+ nullif(split_part(usr.email, '@', 1), ''),
22
+ 'author'
23
+ ),
24
+ '[áàäâãåÁÀÄÂÃÅ]',
25
+ 'a',
26
+ 'g'
27
+ ),
28
+ '[éèëêÉÈËÊ]',
29
+ 'e',
30
+ 'g'
31
+ ),
32
+ '\s+',
33
+ '-',
34
+ 'g'
35
+ )
36
+ )
37
+ );
38
+
39
+ base_slug := regexp_replace(base_slug, '[íìïîÍÌÏÎ]', 'i', 'g');
40
+ base_slug := regexp_replace(base_slug, '[óòöôõÓÒÖÔÕ]', 'o', 'g');
41
+ base_slug := regexp_replace(base_slug, '[úùüûÚÙÜÛ]', 'u', 'g');
42
+ base_slug := regexp_replace(base_slug, '[ñÑ]', 'n', 'g');
43
+ base_slug := regexp_replace(base_slug, '[çÇ]', 'c', 'g');
44
+ base_slug := regexp_replace(base_slug, '[^a-z0-9-]', '', 'g');
45
+ base_slug := trim(both '-' FROM base_slug);
46
+
47
+ IF base_slug IS NULL OR base_slug = '' THEN
48
+ base_slug := 'author';
49
+ END IF;
50
+
51
+ next_slug := base_slug || '--' || usr.id;
52
+
53
+ UPDATE plank_users
54
+ SET public_author_slug = next_slug
55
+ WHERE id = usr.id;
56
+ END LOOP;
57
+
58
+ FOR usr IN
59
+ SELECT id, public_author_slug
60
+ FROM plank_users
61
+ ORDER BY created_at, id
62
+ LOOP
63
+ base_slug := split_part(usr.public_author_slug, '--', 1);
64
+ next_slug := base_slug;
65
+ suffix_num := 2;
66
+
67
+ WHILE EXISTS (
68
+ SELECT 1
69
+ FROM plank_users
70
+ WHERE public_author_slug = next_slug
71
+ AND id <> usr.id
72
+ ) LOOP
73
+ next_slug := base_slug || '-' || suffix_num;
74
+ suffix_num := suffix_num + 1;
75
+ END LOOP;
76
+
77
+ UPDATE plank_users
78
+ SET public_author_slug = next_slug
79
+ WHERE id = usr.id;
80
+ END LOOP;
81
+ END;
82
+ $$;
@@ -2948,7 +2948,20 @@ var ArraySubFieldSchema = z2.object({
2948
2948
  });
2949
2949
  var FieldSchema = z2.object({
2950
2950
  name: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Field name must be lowercase with underscores"),
2951
- type: z2.enum(["string", "text", "richtext", "number", "boolean", "datetime", "media", "media-gallery", "relation", "uid", "array", "navigation"]),
2951
+ type: z2.enum([
2952
+ "string",
2953
+ "text",
2954
+ "richtext",
2955
+ "number",
2956
+ "boolean",
2957
+ "datetime",
2958
+ "media",
2959
+ "media-gallery",
2960
+ "relation",
2961
+ "uid",
2962
+ "array",
2963
+ "navigation"
2964
+ ]),
2952
2965
  required: z2.boolean().optional(),
2953
2966
  subtype: z2.enum(["integer", "float"]).optional(),
2954
2967
  relationType: z2.enum(["many-to-one", "one-to-one", "one-to-many", "many-to-many"]).optional(),
@@ -2974,6 +2987,9 @@ function isReservedSqlIdentifier(name) {
2974
2987
  }
2975
2988
  function findReservedIdentifierErrors(ct) {
2976
2989
  const errors = [];
2990
+ if (ct.tableName === "authors") {
2991
+ errors.push('Content type slug "authors" is reserved by the public API.');
2992
+ }
2977
2993
  if (isReservedSqlIdentifier(ct.tableName)) {
2978
2994
  errors.push(`Table name "${ct.tableName}" is reserved by SQL. Choose a different content type slug.`);
2979
2995
  }
@@ -3714,7 +3730,7 @@ var patchEntryStatus = async (req, res) => {
3714
3730
  UPDATE ${quotedTableName} SET
3715
3731
  status = 'published',
3716
3732
  published_data = ${buildSnapshotExpr(ct.tableName)},
3717
- published_at = NOW(),
3733
+ published_at = COALESCE(published_at, NOW()),
3718
3734
  scheduled_for = NULL,
3719
3735
  review_rejected = FALSE,
3720
3736
  updated_at = NOW()
@@ -3867,10 +3883,40 @@ var deleteEntry = async (req, res) => {
3867
3883
  import bcrypt2 from "bcryptjs";
3868
3884
  import { randomBytes as randomBytes6 } from "crypto";
3869
3885
  import { z as z4, flattenError as flattenError4 } from "zod";
3886
+
3887
+ // ../core/dist/lib/publicAuthorSlug.js
3888
+ function slugifySegment(value) {
3889
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
3890
+ }
3891
+ function baseSlugFromUser(input) {
3892
+ const fullName = [input.firstName, input.lastName].map((value) => value?.trim() ?? "").filter(Boolean).join(" ");
3893
+ const base = fullName || input.email.split("@")[0] || "author";
3894
+ return slugifySegment(base) || "author";
3895
+ }
3896
+ async function resolveUniquePublicAuthorSlug(input, excludeUserId) {
3897
+ const baseSlug = baseSlugFromUser(input);
3898
+ let slug = baseSlug;
3899
+ let suffix = 2;
3900
+ while (true) {
3901
+ const { rows } = await pool_default.query(`SELECT id
3902
+ FROM plank_users
3903
+ WHERE public_author_slug = $1
3904
+ AND ($2::text IS NULL OR id != $2)
3905
+ LIMIT 1`, [slug, excludeUserId ?? null]);
3906
+ if (!rows[0])
3907
+ return slug;
3908
+ slug = `${baseSlug}-${suffix}`;
3909
+ suffix += 1;
3910
+ }
3911
+ }
3912
+
3913
+ // ../core/dist/controllers/users.js
3870
3914
  var CreateUserSchema = z4.object({
3871
3915
  email: z4.email(),
3872
3916
  password: z4.string().min(8),
3873
3917
  roleId: z4.string().min(1),
3918
+ firstName: z4.string().trim().min(1).max(100),
3919
+ lastName: z4.string().trim().min(1).max(100),
3874
3920
  enabled: z4.boolean().optional()
3875
3921
  });
3876
3922
  var UpdateUserSchema = z4.object({
@@ -3939,7 +3985,7 @@ async function listUsers(_req, res) {
3939
3985
  res.json(rows);
3940
3986
  }
3941
3987
  async function getMe(req, res) {
3942
- const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.avatar_url,
3988
+ const { rows } = await pool_default.query(`SELECT u.id, u.email, u.role_id, u.first_name, u.last_name, u.public_author_slug, u.avatar_url,
3943
3989
  u.job_title, u.organization, u.country, u.two_factor_enabled, u.enabled, u.created_at,
3944
3990
  r.name AS role_name, r.permissions
3945
3991
  FROM plank_users u
@@ -4018,7 +4064,9 @@ async function disableTwoFactor(req, res) {
4018
4064
  res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
4019
4065
  return;
4020
4066
  }
4021
- const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [req.user.id]);
4067
+ const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
4068
+ req.user.id
4069
+ ]);
4022
4070
  const user = rows[0];
4023
4071
  if (!user) {
4024
4072
  res.status(404).json({ error: "User not found" });
@@ -4056,7 +4104,9 @@ async function regenerateBackupCodes(req, res) {
4056
4104
  res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
4057
4105
  return;
4058
4106
  }
4059
- const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [req.user.id]);
4107
+ const { rows } = await pool_default.query("SELECT two_factor_enabled, two_factor_secret, password FROM plank_users WHERE id = $1", [
4108
+ req.user.id
4109
+ ]);
4060
4110
  const user = rows[0];
4061
4111
  if (!user || !user.two_factor_enabled || !user.two_factor_secret) {
4062
4112
  res.status(400).json({ error: "2FA is not enabled" });
@@ -4067,7 +4117,10 @@ async function regenerateBackupCodes(req, res) {
4067
4117
  res.status(401).json({ error: "Current password is incorrect" });
4068
4118
  return;
4069
4119
  }
4070
- const totpResult = d3({ token: parsed.data.code, secret: decrypt(user.two_factor_secret) });
4120
+ const totpResult = d3({
4121
+ token: parsed.data.code,
4122
+ secret: decrypt(user.two_factor_secret)
4123
+ });
4071
4124
  if (!totpResult.valid) {
4072
4125
  res.status(401).json({ error: "Invalid verification code" });
4073
4126
  return;
@@ -4082,15 +4135,36 @@ async function updateMe(req, res) {
4082
4135
  return;
4083
4136
  }
4084
4137
  const { firstName, lastName, jobTitle, organization, country } = parsed.data;
4138
+ const { rows: currentRows } = await pool_default.query("SELECT email, first_name, last_name FROM plank_users WHERE id = $1", [req.user.id]);
4139
+ if (!currentRows[0]) {
4140
+ res.status(404).json({ error: "User not found" });
4141
+ return;
4142
+ }
4143
+ const nextFirstName = firstName ?? currentRows[0].first_name;
4144
+ const nextLastName = lastName ?? currentRows[0].last_name;
4145
+ const publicAuthorSlug = await resolveUniquePublicAuthorSlug({
4146
+ email: currentRows[0].email,
4147
+ firstName: nextFirstName,
4148
+ lastName: nextLastName
4149
+ }, req.user.id);
4085
4150
  const { rows } = await pool_default.query(`UPDATE plank_users
4086
4151
  SET first_name = COALESCE($1, first_name),
4087
4152
  last_name = COALESCE($2, last_name),
4088
4153
  job_title = COALESCE($3, job_title),
4089
4154
  organization = COALESCE($4, organization),
4090
- country = COALESCE($5, country)
4091
- WHERE id = $6
4092
- RETURNING id, email, role_id, first_name, last_name, avatar_url,
4093
- job_title, organization, country, created_at`, [firstName ?? null, lastName ?? null, jobTitle ?? null, organization ?? null, country ?? null, req.user.id]);
4155
+ country = COALESCE($5, country),
4156
+ public_author_slug = $6
4157
+ WHERE id = $7
4158
+ RETURNING id, email, role_id, first_name, last_name, public_author_slug, avatar_url,
4159
+ job_title, organization, country, created_at`, [
4160
+ firstName ?? null,
4161
+ lastName ?? null,
4162
+ jobTitle ?? null,
4163
+ organization ?? null,
4164
+ country ?? null,
4165
+ publicAuthorSlug,
4166
+ req.user.id
4167
+ ]);
4094
4168
  if (!rows[0]) {
4095
4169
  res.status(404).json({ error: "User not found" });
4096
4170
  return;
@@ -4136,7 +4210,9 @@ async function confirmAvatar(req, res) {
4136
4210
  res.json({ avatarUrl, user: await resolveAvatarUrl(rows[0]) });
4137
4211
  }
4138
4212
  async function deleteAvatar(req, res) {
4139
- const { rows } = await pool_default.query("SELECT avatar_url FROM plank_users WHERE id = $1", [req.user.id]);
4213
+ const { rows } = await pool_default.query("SELECT avatar_url FROM plank_users WHERE id = $1", [
4214
+ req.user.id
4215
+ ]);
4140
4216
  const current = rows[0]?.avatar_url;
4141
4217
  if (current && !current.startsWith("http")) {
4142
4218
  const provider = await getProvider();
@@ -4172,7 +4248,7 @@ async function createUser(req, res) {
4172
4248
  res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
4173
4249
  return;
4174
4250
  }
4175
- const { email, password, roleId, enabled } = parsed.data;
4251
+ const { email, password, roleId, firstName, lastName, enabled } = parsed.data;
4176
4252
  const requesterRoleName = await roleNameById(req.user.roleId);
4177
4253
  const targetRoleName = await roleNameById(roleId);
4178
4254
  if (targetRoleName === "Super Admin" && requesterRoleName !== "Super Admin") {
@@ -4184,8 +4260,19 @@ async function createUser(req, res) {
4184
4260
  const nextEnabled = editorialMode || !isEditorialExclusiveRole ? enabled ?? true : false;
4185
4261
  const hashed = await bcrypt2.hash(password, 12);
4186
4262
  const id = createId();
4187
- await pool_default.query("INSERT INTO plank_users (id, email, password, role_id, enabled) VALUES ($1, $2, $3, $4, $5)", [id, email, hashed, roleId, nextEnabled]);
4188
- res.status(201).json({ id, email, roleId, enabled: nextEnabled });
4263
+ const publicAuthorSlug = await resolveUniquePublicAuthorSlug({ email, firstName, lastName });
4264
+ await pool_default.query(`INSERT INTO plank_users
4265
+ (id, email, password, role_id, first_name, last_name, public_author_slug, enabled)
4266
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [id, email, hashed, roleId, firstName, lastName, publicAuthorSlug, nextEnabled]);
4267
+ res.status(201).json({
4268
+ id,
4269
+ email,
4270
+ roleId,
4271
+ first_name: firstName,
4272
+ last_name: lastName,
4273
+ public_author_slug: publicAuthorSlug,
4274
+ enabled: nextEnabled
4275
+ });
4189
4276
  }
4190
4277
  async function updateUser(req, res) {
4191
4278
  const parsed = UpdateUserSchema.safeParse(req.body);
@@ -4217,18 +4304,41 @@ async function updateUser(req, res) {
4217
4304
  resolvedEnabled = false;
4218
4305
  }
4219
4306
  }
4307
+ const targetUserId = String(req.params.id);
4308
+ const { rows: currentRows } = await pool_default.query("SELECT email, first_name, last_name FROM plank_users WHERE id = $1", [targetUserId]);
4309
+ if (!currentRows[0]) {
4310
+ res.status(404).json({ error: "User not found" });
4311
+ return;
4312
+ }
4313
+ const nextFirstName = firstName ?? currentRows[0].first_name;
4314
+ const nextLastName = lastName ?? currentRows[0].last_name;
4315
+ const nextEmail = email ?? currentRows[0].email;
4316
+ const publicAuthorSlug = await resolveUniquePublicAuthorSlug({
4317
+ email: nextEmail,
4318
+ firstName: nextFirstName,
4319
+ lastName: nextLastName
4320
+ }, targetUserId);
4220
4321
  const { rows } = await pool_default.query(`UPDATE plank_users
4221
4322
  SET email = COALESCE($1, email),
4222
4323
  role_id = COALESCE($2, role_id),
4223
4324
  first_name = COALESCE($3, first_name),
4224
4325
  last_name = COALESCE($4, last_name),
4225
4326
  enabled = COALESCE($5, enabled),
4327
+ public_author_slug = $6,
4226
4328
  session_version = CASE
4227
4329
  WHEN $5 IS NOT NULL AND $5 = FALSE AND enabled = TRUE THEN session_version + 1
4228
4330
  ELSE session_version
4229
4331
  END
4230
- WHERE id = $6
4231
- RETURNING id, email, role_id, first_name, last_name, enabled, created_at`, [email ?? null, roleId ?? null, firstName ?? null, lastName ?? null, resolvedEnabled ?? null, req.params.id]);
4332
+ WHERE id = $7
4333
+ RETURNING id, email, role_id, first_name, last_name, public_author_slug, enabled, created_at`, [
4334
+ email ?? null,
4335
+ roleId ?? null,
4336
+ firstName ?? null,
4337
+ lastName ?? null,
4338
+ resolvedEnabled ?? null,
4339
+ publicAuthorSlug,
4340
+ req.params.id
4341
+ ]);
4232
4342
  if (!rows[0]) {
4233
4343
  res.status(404).json({ error: "User not found" });
4234
4344
  return;
@@ -4670,11 +4780,17 @@ async function getAppModes(req, res) {
4670
4780
  res.json(modes);
4671
4781
  }
4672
4782
  async function getClientSettings(_req, res) {
4673
- const settings = await getSettings("general");
4783
+ const [settings, previewSettings] = await Promise.all([
4784
+ getSettings("general"),
4785
+ getSettings("preview")
4786
+ ]);
4674
4787
  res.json({
4675
4788
  timezone: settings.timezone ?? "UTC",
4676
4789
  locales: settings.locales ?? '["en"]',
4677
- default_locale: settings.default_locale ?? "en"
4790
+ default_locale: settings.default_locale ?? "en",
4791
+ preview_enabled: previewSettings.enabled ?? "false",
4792
+ preview_url_template: previewSettings.url_template ?? "",
4793
+ preview_slug_field: previewSettings.slug_field ?? "slug"
4678
4794
  });
4679
4795
  }
4680
4796
  async function getEditorialMode(_req, res) {
@@ -5138,6 +5254,23 @@ async function resolveAuthorAvatars(entries) {
5138
5254
  }
5139
5255
  }));
5140
5256
  }
5257
+ async function resolvePublicAuthorAvatar(author) {
5258
+ if (!author.avatar_url || author.avatar_url.startsWith("http"))
5259
+ return author;
5260
+ const provider = await getProvider();
5261
+ return { ...author, avatar_url: await provider.getUrl(author.avatar_url) };
5262
+ }
5263
+ function serializePublicAuthor(row) {
5264
+ return {
5265
+ first_name: row.first_name,
5266
+ last_name: row.last_name,
5267
+ avatar_url: row.avatar_url,
5268
+ job_title: row.job_title,
5269
+ organization: row.organization,
5270
+ country: row.country,
5271
+ slug: row.public_author_slug
5272
+ };
5273
+ }
5141
5274
  function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
5142
5275
  const { published_data, _author_first_name, _author_last_name, _author_avatar_url, _author_job_title, _author_organization, _author_country, _editor_first_name, _editor_last_name, _editor_avatar_url, _editor_job_title, _editor_organization, _editor_country, ...rest } = row;
5143
5276
  const source = statusParam === "published" && published_data ? published_data : rest;
@@ -5188,7 +5321,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
5188
5321
  avatar_url: _author_avatar_url ?? null,
5189
5322
  job_title: _author_job_title ?? null,
5190
5323
  organization: _author_organization ?? null,
5191
- country: _author_country ?? null
5324
+ country: _author_country ?? null,
5325
+ slug: row._author_slug ?? null
5192
5326
  } : null;
5193
5327
  out.editor = _editor_first_name || _editor_last_name ? {
5194
5328
  first_name: _editor_first_name ?? null,
@@ -5196,7 +5330,8 @@ function serializeEntry(row, ct, statusParam, locale, fallbacks = []) {
5196
5330
  avatar_url: _editor_avatar_url ?? null,
5197
5331
  job_title: _editor_job_title ?? null,
5198
5332
  organization: _editor_organization ?? null,
5199
- country: _editor_country ?? null
5333
+ country: _editor_country ?? null,
5334
+ slug: row._editor_slug ?? null
5200
5335
  } : null;
5201
5336
  return out;
5202
5337
  }
@@ -5218,8 +5353,8 @@ var listPublicEntries = async (req, res) => {
5218
5353
  const fallbacks2 = req.query.fallback ? String(req.query.fallback).split(",") : [];
5219
5354
  const statusClause = statusParam2 === "published" || statusParam2 === "draft" ? `WHERE e.status = $1` : "";
5220
5355
  const values = statusClause ? [statusParam2] : [];
5221
- const { rows: rows2 } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5222
- ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5356
+ const { rows: rows2 } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5357
+ ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5223
5358
  FROM ${ct.tableName} e
5224
5359
  LEFT JOIN plank_users u ON u.id = e.created_by
5225
5360
  LEFT JOIN plank_users ed ON ed.id = e.editor_id
@@ -5257,6 +5392,11 @@ var listPublicEntries = async (req, res) => {
5257
5392
  filterClauses.push(`e.status = $${filterValues.length + 1}`);
5258
5393
  filterValues.push(statusParam);
5259
5394
  }
5395
+ const authorSlug = typeof req.query.author === "string" ? req.query.author.trim() : "";
5396
+ if (authorSlug) {
5397
+ filterClauses.push(`u.public_author_slug = $${filterValues.length + 1}`);
5398
+ filterValues.push(authorSlug);
5399
+ }
5260
5400
  const rawSort = String(req.query.sort ?? "created_at");
5261
5401
  const sortField = knownFields.has(rawSort) || systemSortFields.has(rawSort) ? rawSort : "created_at";
5262
5402
  assertSafeIdentifier(sortField);
@@ -5279,8 +5419,8 @@ var listPublicEntries = async (req, res) => {
5279
5419
  filterClauses.push(parsedFilter.operator === "nin" ? `NOT (e.${fieldName} = ANY($${filterValues.length + 1}))` : `e.${fieldName} = ANY($${filterValues.length + 1})`);
5280
5420
  filterValues.push(coercedValues);
5281
5421
  }
5282
- for (const [key, value] of Object.entries(req.query)) {
5283
- if (key === "page" || key === "limit" || key === "status" || key === "sort" || key === "order" || key === "locale" || key === "fallback" || key === "fields" || key === "select" || key === "exclude" || key === "filters" || key.startsWith("filters["))
5422
+ for (const [key] of Object.entries(req.query)) {
5423
+ if (key === "page" || key === "limit" || key === "status" || key === "sort" || key === "order" || key === "locale" || key === "fallback" || key === "fields" || key === "select" || key === "exclude" || key === "author" || key === "filters" || key.startsWith("filters["))
5284
5424
  continue;
5285
5425
  if (knownFields.has(key))
5286
5426
  continue;
@@ -5291,13 +5431,16 @@ var listPublicEntries = async (req, res) => {
5291
5431
  const limitParam = filterValues.length + 1;
5292
5432
  const offsetParam = filterValues.length + 2;
5293
5433
  const [{ rows }, { rows: countRows }] = await Promise.all([
5294
- pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5295
- ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5434
+ pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5435
+ ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5296
5436
  FROM ${ct.tableName} e
5297
5437
  LEFT JOIN plank_users u ON u.id = e.created_by
5298
5438
  LEFT JOIN plank_users ed ON ed.id = e.editor_id
5299
5439
  ${where} ORDER BY e.${sortField} ${sortDir} LIMIT $${limitParam} OFFSET $${offsetParam}`, [...filterValues, limit, offset]),
5300
- pool_default.query(`SELECT COUNT(*) as count FROM ${ct.tableName} e ${where}`, filterValues)
5440
+ pool_default.query(`SELECT COUNT(*) as count
5441
+ FROM ${ct.tableName} e
5442
+ LEFT JOIN plank_users u ON u.id = e.created_by
5443
+ ${where}`, filterValues)
5301
5444
  ]);
5302
5445
  const data = rows.map((row) => serializeEntry(row, ct, statusParam, locale, fallbacks));
5303
5446
  await Promise.all([
@@ -5327,8 +5470,8 @@ var getPublicEntry = async (req, res) => {
5327
5470
  const statusParam = String(req.query.status ?? "published");
5328
5471
  const statusClause = statusParam === "published" || statusParam === "draft" ? ` AND e.status = $2` : "";
5329
5472
  const values = statusClause ? [req.params.id, statusParam] : [req.params.id];
5330
- const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5331
- ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5473
+ const { rows } = await pool_default.query(`SELECT e.*, u.first_name AS _author_first_name, u.last_name AS _author_last_name, u.public_author_slug AS _author_slug, u.avatar_url AS _author_avatar_url, u.job_title AS _author_job_title, u.organization AS _author_organization, u.country AS _author_country,
5474
+ ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.public_author_slug AS _editor_slug, ed.avatar_url AS _editor_avatar_url, ed.job_title AS _editor_job_title, ed.organization AS _editor_organization, ed.country AS _editor_country
5332
5475
  FROM ${ct.tableName} e
5333
5476
  LEFT JOIN plank_users u ON u.id = e.created_by
5334
5477
  LEFT JOIN plank_users ed ON ed.id = e.editor_id
@@ -5347,10 +5490,22 @@ var getPublicEntry = async (req, res) => {
5347
5490
  ]);
5348
5491
  res.json(selectEntryFields(entry, ct, selection));
5349
5492
  };
5493
+ var getPublicAuthor = async (req, res) => {
5494
+ const { rows } = await pool_default.query(`SELECT public_author_slug, first_name, last_name, avatar_url, job_title, organization, country
5495
+ FROM plank_users
5496
+ WHERE public_author_slug = $1
5497
+ LIMIT 1`, [req.params.slug]);
5498
+ if (!rows[0]) {
5499
+ res.status(404).json({ error: "Not found" });
5500
+ return;
5501
+ }
5502
+ res.json(await resolvePublicAuthorAvatar(serializePublicAuthor(rows[0])));
5503
+ };
5350
5504
 
5351
5505
  // ../core/dist/routes/public.js
5352
5506
  var router3 = Router3();
5353
5507
  router3.use(apiToken);
5508
+ router3.get("/authors/:slug", getPublicAuthor);
5354
5509
  router3.get("/:slug", listPublicEntries);
5355
5510
  router3.get("/:slug/:id", getPublicEntry);
5356
5511
  var public_default = router3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Self-hosted headless CMS. Deploy in minutes on your own infrastructure.",
5
5
  "type": "module",
6
6
  "files": [
@@ -55,9 +55,9 @@
55
55
  "devDependencies": {
56
56
  "@types/fs-extra": "^11.0.4",
57
57
  "tsup": "^8.5.0",
58
- "@plank-cms/db": "0.22.0",
59
- "@plank-cms/core": "0.22.0",
60
- "@plank-cms/schema": "0.22.0"
58
+ "@plank-cms/core": "0.24.0",
59
+ "@plank-cms/db": "0.24.0",
60
+ "@plank-cms/schema": "0.24.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",