@plank-cms/plank 0.18.0 → 0.19.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-yw3F7Equ.js"></script>
15
+ <script type="module" crossorigin src="/admin/assets/index-BepYvDmW.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/admin/assets/index-FkEexpp5.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.18.0";
10
+ var PACKAGE_VERSION = "0.19.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-QZGQPF7F.js");
104
+ const { start: startServer } = await import("./server-KVOZGN2H.js");
105
105
  await startServer();
106
106
  }
107
107
 
@@ -4662,9 +4662,71 @@ var SYSTEM_RESPONSE_FIELDS = [
4662
4662
  "editor"
4663
4663
  ];
4664
4664
  function parseCsvParam(value) {
4665
- if (typeof value !== "string")
4666
- return [];
4667
- return value.split(",").map((item) => item.trim()).filter(Boolean);
4665
+ if (typeof value === "string") {
4666
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
4667
+ }
4668
+ if (Array.isArray(value)) {
4669
+ return value.flatMap((item) => parseCsvParam(item));
4670
+ }
4671
+ return [];
4672
+ }
4673
+ function coerceFilterValue(raw, field) {
4674
+ if (field.type === "number") {
4675
+ const parsed = field.subtype === "float" ? Number.parseFloat(raw) : Number.parseInt(raw, 10);
4676
+ return Number.isNaN(parsed) ? raw : parsed;
4677
+ }
4678
+ if (field.type === "boolean") {
4679
+ if (raw === "true")
4680
+ return true;
4681
+ if (raw === "false")
4682
+ return false;
4683
+ }
4684
+ return raw;
4685
+ }
4686
+ function coerceFilterValues(value, field) {
4687
+ return parseCsvParam(value).map((item) => coerceFilterValue(item, field));
4688
+ }
4689
+ function isFilterOperator(value) {
4690
+ return value === "eq" || value === "ne" || value === "in" || value === "nin";
4691
+ }
4692
+ function parseFilters(query, fieldMap) {
4693
+ const filters = [];
4694
+ const invalidFilters = [];
4695
+ const filtersObject = query.filters && typeof query.filters === "object" && !Array.isArray(query.filters) ? query.filters : null;
4696
+ if (filtersObject) {
4697
+ for (const [fieldName, operatorValue] of Object.entries(filtersObject)) {
4698
+ const field = fieldMap.get(fieldName);
4699
+ if (!field || typeof operatorValue !== "object" || operatorValue === null || Array.isArray(operatorValue)) {
4700
+ invalidFilters.push(`filters.${fieldName}`);
4701
+ continue;
4702
+ }
4703
+ for (const [operatorKey, rawValue] of Object.entries(operatorValue)) {
4704
+ if (!isFilterOperator(operatorKey)) {
4705
+ invalidFilters.push(`filters.${fieldName}.${operatorKey}`);
4706
+ continue;
4707
+ }
4708
+ filters.push({
4709
+ field,
4710
+ operator: operatorKey,
4711
+ rawValue,
4712
+ rawKey: `filters[${fieldName}][${operatorKey}]`
4713
+ });
4714
+ }
4715
+ }
4716
+ }
4717
+ for (const [key, rawValue] of Object.entries(query)) {
4718
+ const match = /^filters\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
4719
+ if (!match)
4720
+ continue;
4721
+ const [, fieldName, operatorKey] = match;
4722
+ const field = fieldMap.get(fieldName);
4723
+ if (!field || !isFilterOperator(operatorKey)) {
4724
+ invalidFilters.push(key);
4725
+ continue;
4726
+ }
4727
+ filters.push({ field, operator: operatorKey, rawValue, rawKey: key });
4728
+ }
4729
+ return { filters, invalidFilters };
4668
4730
  }
4669
4731
  function parseFieldSelection(query, ct) {
4670
4732
  const includeFields = [...parseCsvParam(query.fields), ...parseCsvParam(query.select)];
@@ -4998,9 +5060,15 @@ var listPublicEntries = async (req, res) => {
4998
5060
  const locale = req.query.locale ? String(req.query.locale) : void 0;
4999
5061
  const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
5000
5062
  const knownFields = new Set(ct.fields.map((f2) => f2.name));
5063
+ const fieldMap = new Map(ct.fields.map((field) => [field.name, field]));
5001
5064
  const systemSortFields = /* @__PURE__ */ new Set(["created_at", "updated_at", "published_at"]);
5002
5065
  const filterClauses = [];
5003
5066
  const filterValues = [];
5067
+ const { filters: parsedFilters, invalidFilters } = parseFilters(req.query, fieldMap);
5068
+ if (invalidFilters.length > 0) {
5069
+ res.status(400).json({ error: `Invalid filters: ${invalidFilters.join(", ")}` });
5070
+ return;
5071
+ }
5004
5072
  const statusParam = String(req.query.status ?? "published");
5005
5073
  if (statusParam === "published" || statusParam === "draft") {
5006
5074
  filterClauses.push(`e.status = $${filterValues.length + 1}`);
@@ -5010,14 +5078,31 @@ var listPublicEntries = async (req, res) => {
5010
5078
  const sortField = knownFields.has(rawSort) || systemSortFields.has(rawSort) ? rawSort : "created_at";
5011
5079
  assertSafeIdentifier(sortField);
5012
5080
  const sortDir = String(req.query.order ?? "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
5013
- for (const [key, value] of Object.entries(req.query)) {
5014
- if (key === "page" || key === "limit" || key === "status" || key === "sort" || key === "order" || key === "locale" || key === "fallback" || key === "fields" || key === "select" || key === "exclude")
5081
+ for (const parsedFilter of parsedFilters) {
5082
+ const fieldName = parsedFilter.field.name;
5083
+ assertSafeIdentifier(fieldName);
5084
+ if (parsedFilter.operator === "eq" || parsedFilter.operator === "ne") {
5085
+ const rawValue = Array.isArray(parsedFilter.rawValue) ? parsedFilter.rawValue[0] : parsedFilter.rawValue;
5086
+ const coercedValue = typeof rawValue === "string" ? coerceFilterValue(rawValue, parsedFilter.field) : rawValue;
5087
+ filterClauses.push(`e.${fieldName} ${parsedFilter.operator === "ne" ? "!=" : "="} $${filterValues.length + 1}`);
5088
+ filterValues.push(coercedValue);
5015
5089
  continue;
5016
- if (knownFields.has(key)) {
5017
- assertSafeIdentifier(key);
5018
- filterClauses.push(`e.${key} = $${filterValues.length + 1}`);
5019
- filterValues.push(value);
5020
5090
  }
5091
+ const coercedValues = coerceFilterValues(parsedFilter.rawValue, parsedFilter.field);
5092
+ if (coercedValues.length === 0) {
5093
+ res.status(400).json({ error: `Filter "${parsedFilter.rawKey}" requires at least one value` });
5094
+ return;
5095
+ }
5096
+ filterClauses.push(parsedFilter.operator === "nin" ? `NOT (e.${fieldName} = ANY($${filterValues.length + 1}))` : `e.${fieldName} = ANY($${filterValues.length + 1})`);
5097
+ filterValues.push(coercedValues);
5098
+ }
5099
+ for (const [key, value] of Object.entries(req.query)) {
5100
+ 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["))
5101
+ continue;
5102
+ if (knownFields.has(key))
5103
+ continue;
5104
+ if (/_((?:n)?in|ne)$/.test(key))
5105
+ continue;
5021
5106
  }
5022
5107
  const where = filterClauses.length > 0 ? `WHERE ${filterClauses.join(" AND ")}` : "";
5023
5108
  const limitParam = filterValues.length + 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.18.0",
3
+ "version": "0.19.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.18.0",
59
- "@plank-cms/core": "0.18.0",
60
- "@plank-cms/schema": "0.18.0"
58
+ "@plank-cms/core": "0.19.0",
59
+ "@plank-cms/db": "0.19.0",
60
+ "@plank-cms/schema": "0.19.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",