@plank-cms/plank 0.26.1 → 0.27.1

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,8 +12,8 @@
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-BLYpKKlP.js"></script>
16
- <link rel="stylesheet" crossorigin href="/admin/assets/index-BWA7n9y8.css">
15
+ <script type="module" crossorigin src="/admin/assets/index-DZUtOtML.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/admin/assets/index-BTElP7oS.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
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.26.1";
10
+ var PACKAGE_VERSION = "0.27.1";
11
11
  function generateSecret() {
12
12
  return randomBytes(32).toString("hex");
13
13
  }
@@ -106,7 +106,7 @@ import { dirname, join as join2, resolve as resolve2 } from "path";
106
106
  async function start() {
107
107
  config({ path: resolve2(process.cwd(), ".env") });
108
108
  process.env.PLANK_ADMIN_DIST = join2(dirname(fileURLToPath(import.meta.url)), "admin");
109
- const { start: startServer } = await import("./server-7LSVEAFA.js");
109
+ const { start: startServer } = await import("./server-5JMBMGIN.js");
110
110
  await startServer();
111
111
  }
112
112
 
@@ -651,7 +651,7 @@ import express from "express";
651
651
  import cors from "cors";
652
652
  import helmet from "helmet";
653
653
  import { join as join4, dirname as dirname2 } from "path";
654
- import { fileURLToPath as fileURLToPath2 } from "url";
654
+ import { fileURLToPath as fileURLToPath3 } from "url";
655
655
 
656
656
  // ../core/dist/routes/auth.js
657
657
  import { Router } from "express";
@@ -2445,6 +2445,32 @@ async function getProvider() {
2445
2445
  }
2446
2446
  var upload = multer({ storage: multer.memoryStorage() });
2447
2447
 
2448
+ // ../core/dist/lib/publicAuthorSlug.js
2449
+ function slugifySegment(value) {
2450
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
2451
+ }
2452
+ function baseSlugFromUser(input) {
2453
+ const fullName = [input.firstName, input.lastName].map((value) => value?.trim() ?? "").filter(Boolean).join(" ");
2454
+ const base = fullName || input.email.split("@")[0] || "author";
2455
+ return slugifySegment(base) || "author";
2456
+ }
2457
+ async function resolveUniquePublicAuthorSlug(input, excludeUserId) {
2458
+ const baseSlug = baseSlugFromUser(input);
2459
+ let slug = baseSlug;
2460
+ let suffix = 2;
2461
+ while (true) {
2462
+ const { rows } = await pool_default.query(`SELECT id
2463
+ FROM plank_users
2464
+ WHERE public_author_slug = $1
2465
+ AND ($2::text IS NULL OR id != $2)
2466
+ LIMIT 1`, [slug, excludeUserId ?? null]);
2467
+ if (!rows[0])
2468
+ return slug;
2469
+ slug = `${baseSlug}-${suffix}`;
2470
+ suffix += 1;
2471
+ }
2472
+ }
2473
+
2448
2474
  // ../core/dist/controllers/auth.js
2449
2475
  var LoginSchema = z.object({
2450
2476
  email: z.email(),
@@ -2456,6 +2482,8 @@ var Login2FASchema = z.object({
2456
2482
  });
2457
2483
  var RegisterSchema = z.object({
2458
2484
  email: z.email(),
2485
+ firstName: z.string().trim().min(1).max(100),
2486
+ lastName: z.string().trim().min(1).max(100),
2459
2487
  password: z.string().min(8)
2460
2488
  });
2461
2489
  var RATE_LIMIT_WINDOW_MS = 15 * 60 * 1e3;
@@ -2712,7 +2740,7 @@ async function register(req, res) {
2712
2740
  res.status(400).json({ errors: flattenError(parsed.error, (i2) => i2.message) });
2713
2741
  return;
2714
2742
  }
2715
- const { email, password } = parsed.data;
2743
+ const { email, firstName, lastName, password } = parsed.data;
2716
2744
  const hashed = await bcrypt.hash(password, 12);
2717
2745
  const { rows: roleRows } = await pool_default.query("SELECT id, name FROM plank_roles WHERE name = $1", ["Super Admin"]);
2718
2746
  const superAdminRole = roleRows[0];
@@ -2721,7 +2749,10 @@ async function register(req, res) {
2721
2749
  return;
2722
2750
  }
2723
2751
  const id = createId();
2724
- await pool_default.query("INSERT INTO plank_users (id, email, password, role_id) VALUES ($1, $2, $3, $4)", [id, email, hashed, superAdminRole.id]);
2752
+ const publicAuthorSlug = await resolveUniquePublicAuthorSlug({ email, firstName, lastName });
2753
+ await pool_default.query(`INSERT INTO plank_users
2754
+ (id, email, password, role_id, first_name, last_name, public_author_slug)
2755
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [id, email, hashed, superAdminRole.id, firstName, lastName, publicAuthorSlug]);
2725
2756
  res.status(201).json({ id, email });
2726
2757
  }
2727
2758
 
@@ -3329,6 +3360,21 @@ function resolveLocalizedRow(row, ct, locale, fallbacks = []) {
3329
3360
  function junctionTableName2(sourceTable, fieldName) {
3330
3361
  return `_rel_${sourceTable}_${fieldName}`;
3331
3362
  }
3363
+ function resolveManyToManyBinding(tableName, field) {
3364
+ const isInverse = (field.relationType ?? "many-to-one") === "many-to-many" && typeof field.relatedTable === "string" && field.relatedTable.length > 0 && typeof field.relatedField === "string" && field.relatedField.length > 0;
3365
+ if (isInverse) {
3366
+ return {
3367
+ junctionTable: junctionTableName2(field.relatedTable, field.relatedField),
3368
+ currentIdColumn: "target_id",
3369
+ relatedIdColumn: "source_id"
3370
+ };
3371
+ }
3372
+ return {
3373
+ junctionTable: junctionTableName2(tableName, field.name),
3374
+ currentIdColumn: "source_id",
3375
+ relatedIdColumn: "target_id"
3376
+ };
3377
+ }
3332
3378
  function normalizeNavigationItems(value) {
3333
3379
  if (!Array.isArray(value))
3334
3380
  return value;
@@ -3366,12 +3412,12 @@ function normalizeNavigationFields(row, fields) {
3366
3412
  return out;
3367
3413
  }
3368
3414
  async function syncManyToMany(entryId, tableName, field, targetIds) {
3369
- const jt = junctionTableName2(tableName, field.name);
3370
- await pool_default.query(`DELETE FROM ${quoteIdentifier(jt)} WHERE source_id = $1`, [entryId]);
3415
+ const binding = resolveManyToManyBinding(tableName, field);
3416
+ await pool_default.query(`DELETE FROM ${quoteIdentifier(binding.junctionTable)} WHERE ${quoteIdentifier(binding.currentIdColumn)} = $1`, [entryId]);
3371
3417
  if (targetIds.length === 0)
3372
3418
  return;
3373
- const placeholders = targetIds.map((_3, i2) => `($1, $${i2 + 2})`).join(", ");
3374
- await pool_default.query(`INSERT INTO ${quoteIdentifier(jt)} (source_id, target_id) VALUES ${placeholders} ON CONFLICT DO NOTHING`, [entryId, ...targetIds]);
3419
+ const tuples = targetIds.map((_3, i2) => binding.currentIdColumn === "source_id" ? `($1, $${i2 + 2})` : `($${i2 + 2}, $1)`).join(", ");
3420
+ await pool_default.query(`INSERT INTO ${quoteIdentifier(binding.junctionTable)} (source_id, target_id) VALUES ${tuples} ON CONFLICT DO NOTHING`, [entryId, ...targetIds]);
3375
3421
  }
3376
3422
  async function isContributorRole(roleId) {
3377
3423
  if (!roleId)
@@ -3512,9 +3558,11 @@ async function loadManyToManyIds(entryId, tableName, fields) {
3512
3558
  return {};
3513
3559
  const result = {};
3514
3560
  await Promise.all(mmFields.map(async (f2) => {
3515
- const jt = junctionTableName2(tableName, f2.name);
3516
- const { rows } = await pool_default.query(`SELECT target_id FROM ${quoteIdentifier(jt)} WHERE source_id = $1`, [entryId]);
3517
- result[f2.name] = rows.map((r2) => r2.target_id);
3561
+ const binding = resolveManyToManyBinding(tableName, f2);
3562
+ const { rows } = await pool_default.query(`SELECT ${quoteIdentifier(binding.relatedIdColumn)} AS related_id
3563
+ FROM ${quoteIdentifier(binding.junctionTable)}
3564
+ WHERE ${quoteIdentifier(binding.currentIdColumn)} = $1`, [entryId]);
3565
+ result[f2.name] = rows.map((r2) => r2.related_id);
3518
3566
  }));
3519
3567
  return result;
3520
3568
  }
@@ -3966,34 +4014,6 @@ var deleteEntry = async (req, res) => {
3966
4014
  import bcrypt2 from "bcryptjs";
3967
4015
  import { randomBytes as randomBytes6 } from "crypto";
3968
4016
  import { z as z4, flattenError as flattenError4 } from "zod";
3969
-
3970
- // ../core/dist/lib/publicAuthorSlug.js
3971
- function slugifySegment(value) {
3972
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
3973
- }
3974
- function baseSlugFromUser(input) {
3975
- const fullName = [input.firstName, input.lastName].map((value) => value?.trim() ?? "").filter(Boolean).join(" ");
3976
- const base = fullName || input.email.split("@")[0] || "author";
3977
- return slugifySegment(base) || "author";
3978
- }
3979
- async function resolveUniquePublicAuthorSlug(input, excludeUserId) {
3980
- const baseSlug = baseSlugFromUser(input);
3981
- let slug = baseSlug;
3982
- let suffix = 2;
3983
- while (true) {
3984
- const { rows } = await pool_default.query(`SELECT id
3985
- FROM plank_users
3986
- WHERE public_author_slug = $1
3987
- AND ($2::text IS NULL OR id != $2)
3988
- LIMIT 1`, [slug, excludeUserId ?? null]);
3989
- if (!rows[0])
3990
- return slug;
3991
- slug = `${baseSlug}-${suffix}`;
3992
- suffix += 1;
3993
- }
3994
- }
3995
-
3996
- // ../core/dist/controllers/users.js
3997
4017
  var CreateUserSchema = z4.object({
3998
4018
  email: z4.email(),
3999
4019
  password: z4.string().min(8),
@@ -4907,6 +4927,82 @@ async function updateNamespaceSettings(req, res) {
4907
4927
  res.json(maskSettings(namespace, updated));
4908
4928
  }
4909
4929
 
4930
+ // ../core/dist/lib/version.js
4931
+ import { readFile as readFile2 } from "fs/promises";
4932
+ import { fileURLToPath as fileURLToPath2 } from "url";
4933
+ var PACKAGE_NAME = "@plank-cms/plank";
4934
+ var CHANGELOG_BASE_URL = "https://github.com/plank-cms/plank/releases";
4935
+ var UPDATE_COMMAND = "npm run update";
4936
+ var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
4937
+ var CACHE_TTL_MS = 1e3 * 60 * 30;
4938
+ var packageJsonUrl = new URL("../../package.json", import.meta.url);
4939
+ var cachedVersionCheck = null;
4940
+ function normalizeVersion(value) {
4941
+ return value.trim().replace(/^v/i, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
4942
+ }
4943
+ function compareVersions(a2, b3) {
4944
+ const left = normalizeVersion(a2);
4945
+ const right = normalizeVersion(b3);
4946
+ const maxLength = Math.max(left.length, right.length);
4947
+ for (let index = 0; index < maxLength; index += 1) {
4948
+ const leftPart = left[index] ?? 0;
4949
+ const rightPart = right[index] ?? 0;
4950
+ if (leftPart > rightPart)
4951
+ return 1;
4952
+ if (leftPart < rightPart)
4953
+ return -1;
4954
+ }
4955
+ return 0;
4956
+ }
4957
+ function getChangelogUrl(version) {
4958
+ return version ? `${CHANGELOG_BASE_URL}/tag/${version}` : CHANGELOG_BASE_URL;
4959
+ }
4960
+ async function readCurrentVersion() {
4961
+ const packageJsonPath = fileURLToPath2(packageJsonUrl);
4962
+ const raw = await readFile2(packageJsonPath, "utf8");
4963
+ const parsed = JSON.parse(raw);
4964
+ return parsed.version ?? "0.0.0";
4965
+ }
4966
+ async function getVersionCheck() {
4967
+ if (cachedVersionCheck && cachedVersionCheck.expiresAt > Date.now()) {
4968
+ return cachedVersionCheck.value;
4969
+ }
4970
+ const currentVersion = await readCurrentVersion();
4971
+ let latestVersion = null;
4972
+ try {
4973
+ const response = await fetch(REGISTRY_URL, {
4974
+ signal: AbortSignal.timeout(4e3),
4975
+ headers: {
4976
+ Accept: "application/json"
4977
+ }
4978
+ });
4979
+ if (response.ok) {
4980
+ const payload = await response.json();
4981
+ latestVersion = payload.version ?? null;
4982
+ }
4983
+ } catch {
4984
+ }
4985
+ const value = {
4986
+ currentVersion,
4987
+ latestVersion,
4988
+ updateAvailable: latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false,
4989
+ changelogUrl: getChangelogUrl(latestVersion),
4990
+ updateCommand: UPDATE_COMMAND,
4991
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
4992
+ };
4993
+ cachedVersionCheck = {
4994
+ expiresAt: Date.now() + CACHE_TTL_MS,
4995
+ value
4996
+ };
4997
+ return value;
4998
+ }
4999
+
5000
+ // ../core/dist/controllers/version.js
5001
+ async function getVersionInfo(_req, res) {
5002
+ const versionInfo = await getVersionCheck();
5003
+ res.json(versionInfo);
5004
+ }
5005
+
4910
5006
  // ../core/dist/routes/admin.js
4911
5007
  var router2 = Router2();
4912
5008
  router2.use(authenticate);
@@ -4962,6 +5058,7 @@ router2.delete("/media/:id", authorize("media:delete"), deleteMedia);
4962
5058
  router2.get("/modes", getAppModes);
4963
5059
  router2.get("/client-settings", getClientSettings);
4964
5060
  router2.get("/editorial-mode", getEditorialMode);
5061
+ router2.get("/version", getVersionInfo);
4965
5062
  router2.get("/settings/:namespace", authorize("settings:overview:read"), getNamespaceSettings);
4966
5063
  router2.put("/settings/:namespace", authorize("settings:overview:write"), updateNamespaceSettings);
4967
5064
  router2.get("/webhooks", authorize("settings:webhooks:read"), listWebhooks);
@@ -5675,7 +5772,7 @@ if (isDev) {
5675
5772
  app.get("/admin/*path", (_req, res) => res.redirect(adminDevUrl));
5676
5773
  app.get("/admin", (_req, res) => res.redirect(adminDevUrl));
5677
5774
  } else {
5678
- const adminDist = process.env.PLANK_ADMIN_DIST ?? join4(dirname2(fileURLToPath2(import.meta.url)), "../public/admin");
5775
+ const adminDist = process.env.PLANK_ADMIN_DIST ?? join4(dirname2(fileURLToPath3(import.meta.url)), "../public/admin");
5679
5776
  app.use("/admin", express.static(adminDist));
5680
5777
  app.get("/admin/*path", (_req, res) => res.sendFile(join4(adminDist, "index.html")));
5681
5778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.26.1",
3
+ "version": "0.27.1",
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.26.1",
59
- "@plank-cms/schema": "0.26.1",
60
- "@plank-cms/core": "0.26.1"
58
+ "@plank-cms/db": "0.27.1",
59
+ "@plank-cms/schema": "0.27.1",
60
+ "@plank-cms/core": "0.27.1"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",