@plank-cms/plank 0.19.0 → 0.20.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,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-BepYvDmW.js"></script>
16
- <link rel="stylesheet" crossorigin href="/admin/assets/index-FkEexpp5.css">
15
+ <script type="module" crossorigin src="/admin/assets/index-rXqeM7Q1.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/admin/assets/index-BrTr5m2J.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.19.0";
10
+ var PACKAGE_VERSION = "0.20.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-KVOZGN2H.js");
104
+ const { start: startServer } = await import("./server-V5YKK3DC.js");
105
105
  await startServer();
106
106
  }
107
107
 
@@ -2164,7 +2164,7 @@ async function setSettings(namespace, values) {
2164
2164
  }
2165
2165
 
2166
2166
  // ../core/dist/media/providers/local.js
2167
- import { writeFile, mkdir } from "fs/promises";
2167
+ import { writeFile, mkdir, rm } from "fs/promises";
2168
2168
  import { join as join3, extname } from "path";
2169
2169
  import { randomBytes as randomBytes3 } from "crypto";
2170
2170
  async function uploadsDir() {
@@ -2200,6 +2200,10 @@ var localProvider = {
2200
2200
  const dir = await uploadsDir();
2201
2201
  await unlink(join3(dir, key));
2202
2202
  },
2203
+ async deletePrefix(prefix) {
2204
+ const dir = await uploadsDir();
2205
+ await rm(join3(dir, prefix), { recursive: true, force: true });
2206
+ },
2203
2207
  async getUrl(key) {
2204
2208
  const base = await publicUrl();
2205
2209
  return `${base}/uploads/${key}`;
@@ -2207,7 +2211,7 @@ var localProvider = {
2207
2211
  };
2208
2212
 
2209
2213
  // ../core/dist/media/providers/s3.js
2210
- import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
2214
+ import { S3Client, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from "@aws-sdk/client-s3";
2211
2215
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
2212
2216
  import { extname as extname2 } from "path";
2213
2217
  import { randomBytes as randomBytes4 } from "crypto";
@@ -2237,6 +2241,9 @@ function buildKey(cfg, filename, prefix) {
2237
2241
  const parts = [cfg.pathPrefix?.replace(/\/$/, ""), prefix, name].filter(Boolean);
2238
2242
  return parts.join("/");
2239
2243
  }
2244
+ function withPathPrefix(cfg, key) {
2245
+ return [cfg.pathPrefix?.replace(/\/$/, ""), key.replace(/^\//, "")].filter(Boolean).join("/");
2246
+ }
2240
2247
  function buildStoredUrl(cfg, key) {
2241
2248
  return cfg.publicUrl ? `${cfg.publicUrl.replace(/\/$/, "")}/${key}` : `https://${cfg.bucket}.s3.${cfg.region}.amazonaws.com/${key}`;
2242
2249
  }
@@ -2253,22 +2260,44 @@ var s3Provider = {
2253
2260
  }));
2254
2261
  return { url: buildStoredUrl(cfg, key), key };
2255
2262
  },
2256
- async uploadRaw(buffer, exactKey, mimeType) {
2263
+ async uploadRaw(buffer, key, mimeType) {
2257
2264
  const cfg = await getConfig();
2258
2265
  const client = buildClient(cfg);
2266
+ const fullKey = withPathPrefix(cfg, key);
2259
2267
  await client.send(new PutObjectCommand({
2260
2268
  Bucket: cfg.bucket,
2261
- Key: exactKey,
2269
+ Key: fullKey,
2262
2270
  Body: buffer,
2263
2271
  ContentType: mimeType
2264
2272
  }));
2265
- return { url: buildStoredUrl(cfg, exactKey), key: exactKey };
2273
+ return { url: buildStoredUrl(cfg, fullKey), key: fullKey };
2266
2274
  },
2267
2275
  async delete(key) {
2268
2276
  const cfg = await getConfig();
2269
2277
  const client = buildClient(cfg);
2270
2278
  await client.send(new DeleteObjectCommand({ Bucket: cfg.bucket, Key: key }));
2271
2279
  },
2280
+ async deletePrefix(prefix) {
2281
+ const cfg = await getConfig();
2282
+ const client = buildClient(cfg);
2283
+ const fullPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
2284
+ let continuationToken;
2285
+ do {
2286
+ const list = await client.send(new ListObjectsV2Command({
2287
+ Bucket: cfg.bucket,
2288
+ Prefix: fullPrefix,
2289
+ ContinuationToken: continuationToken
2290
+ }));
2291
+ const objects = (list.Contents ?? []).map((o) => o.Key ? { Key: o.Key } : null).filter((o) => o !== null);
2292
+ if (objects.length > 0) {
2293
+ await client.send(new DeleteObjectsCommand({
2294
+ Bucket: cfg.bucket,
2295
+ Delete: { Objects: objects, Quiet: true }
2296
+ }));
2297
+ }
2298
+ continuationToken = list.IsTruncated ? list.NextContinuationToken : void 0;
2299
+ } while (continuationToken);
2300
+ },
2272
2301
  async getUrl(key) {
2273
2302
  const cfg = await getConfig();
2274
2303
  return buildStoredUrl(cfg, key);
@@ -2284,7 +2313,7 @@ var s3Provider = {
2284
2313
  };
2285
2314
 
2286
2315
  // ../core/dist/media/providers/r2.js
2287
- import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, DeleteObjectCommand as DeleteObjectCommand2 } from "@aws-sdk/client-s3";
2316
+ import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, DeleteObjectCommand as DeleteObjectCommand2, ListObjectsV2Command as ListObjectsV2Command2, DeleteObjectsCommand as DeleteObjectsCommand2 } from "@aws-sdk/client-s3";
2288
2317
  import { getSignedUrl as getSignedUrl2 } from "@aws-sdk/s3-request-presigner";
2289
2318
  import { extname as extname3 } from "path";
2290
2319
  import { randomBytes as randomBytes5 } from "crypto";
@@ -2317,6 +2346,9 @@ function buildKey2(cfg, filename, prefix) {
2317
2346
  const parts = [cfg.pathPrefix?.replace(/\/$/, ""), prefix, name].filter(Boolean);
2318
2347
  return parts.join("/");
2319
2348
  }
2349
+ function withPathPrefix2(cfg, key) {
2350
+ return [cfg.pathPrefix?.replace(/\/$/, ""), key.replace(/^\//, "")].filter(Boolean).join("/");
2351
+ }
2320
2352
  function buildStoredUrl2(cfg, key) {
2321
2353
  if (!cfg.publicUrl) {
2322
2354
  throw new Error("R2 provider requires a public_url configured in Settings > Media.");
@@ -2336,22 +2368,44 @@ var r2Provider = {
2336
2368
  }));
2337
2369
  return { url: buildStoredUrl2(cfg, key), key };
2338
2370
  },
2339
- async uploadRaw(buffer, exactKey, mimeType) {
2371
+ async uploadRaw(buffer, key, mimeType) {
2340
2372
  const cfg = await getConfig2();
2341
2373
  const client = buildClient2(cfg);
2374
+ const fullKey = withPathPrefix2(cfg, key);
2342
2375
  await client.send(new PutObjectCommand2({
2343
2376
  Bucket: cfg.bucket,
2344
- Key: exactKey,
2377
+ Key: fullKey,
2345
2378
  Body: buffer,
2346
2379
  ContentType: mimeType
2347
2380
  }));
2348
- return { url: buildStoredUrl2(cfg, exactKey), key: exactKey };
2381
+ return { url: buildStoredUrl2(cfg, fullKey), key: fullKey };
2349
2382
  },
2350
2383
  async delete(key) {
2351
2384
  const cfg = await getConfig2();
2352
2385
  const client = buildClient2(cfg);
2353
2386
  await client.send(new DeleteObjectCommand2({ Bucket: cfg.bucket, Key: key }));
2354
2387
  },
2388
+ async deletePrefix(prefix) {
2389
+ const cfg = await getConfig2();
2390
+ const client = buildClient2(cfg);
2391
+ const fullPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
2392
+ let continuationToken;
2393
+ do {
2394
+ const list = await client.send(new ListObjectsV2Command2({
2395
+ Bucket: cfg.bucket,
2396
+ Prefix: fullPrefix,
2397
+ ContinuationToken: continuationToken
2398
+ }));
2399
+ const objects = (list.Contents ?? []).map((o) => o.Key ? { Key: o.Key } : null).filter((o) => o !== null);
2400
+ if (objects.length > 0) {
2401
+ await client.send(new DeleteObjectsCommand2({
2402
+ Bucket: cfg.bucket,
2403
+ Delete: { Objects: objects, Quiet: true }
2404
+ }));
2405
+ }
2406
+ continuationToken = list.IsTruncated ? list.NextContinuationToken : void 0;
2407
+ } while (continuationToken);
2408
+ },
2355
2409
  async getUrl(key) {
2356
2410
  const cfg = await getConfig2();
2357
2411
  return buildStoredUrl2(cfg, key);
@@ -3256,15 +3310,40 @@ var listEntries = async (req, res) => {
3256
3310
  const quotedSortField = quoteIdentifier(sortField);
3257
3311
  const locale = req.query.locale ? String(req.query.locale) : void 0;
3258
3312
  const fallbacks = req.query.fallback ? String(req.query.fallback).split(",") : [];
3313
+ const search = req.query.search ? String(req.query.search).trim() : "";
3314
+ const rawSearchFields = req.query.searchFields ? String(req.query.searchFields).split(",") : [];
3315
+ const textLikeTypes = ["string", "uid", "text", "richtext"];
3316
+ const searchFields = rawSearchFields.filter((name) => ct.fields.some((f2) => f2.name === name && textLikeTypes.includes(f2.type)));
3317
+ let mainWhereClause = "";
3318
+ let countWhereClause = "";
3319
+ const mainParams = [limit, offset];
3320
+ const countParams = [];
3321
+ if (search && searchFields.length > 0) {
3322
+ const term = `%${search}%`;
3323
+ mainParams.push(term);
3324
+ countParams.push(term);
3325
+ const mainIdx = mainParams.length;
3326
+ const countIdx = countParams.length;
3327
+ const mainConditions = searchFields.map((name) => {
3328
+ assertSafeIdentifier(name);
3329
+ return `e.${quoteIdentifier(name)}::text ILIKE $${mainIdx}`;
3330
+ });
3331
+ const countConditions = searchFields.map((name) => {
3332
+ return `e.${quoteIdentifier(name)}::text ILIKE $${countIdx}`;
3333
+ });
3334
+ mainWhereClause = `WHERE (${mainConditions.join(" OR ")})`;
3335
+ countWhereClause = `WHERE (${countConditions.join(" OR ")})`;
3336
+ }
3259
3337
  const [{ rows }, { rows: countRows }] = await Promise.all([
3260
3338
  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,
3261
3339
  ed.first_name AS _editor_first_name, ed.last_name AS _editor_last_name, ed.avatar_url AS _editor_avatar_url
3262
3340
  FROM ${quotedTableName} e
3263
3341
  LEFT JOIN plank_users u ON u.id = e.created_by
3264
3342
  LEFT JOIN plank_users ed ON ed.id = e.editor_id
3343
+ ${mainWhereClause}
3265
3344
  ORDER BY e.${quotedSortField} ${sortDir}
3266
- LIMIT $1 OFFSET $2`, [limit, offset]),
3267
- pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName}`)
3345
+ LIMIT $1 OFFSET $2`, mainParams),
3346
+ pool_default.query(`SELECT COUNT(*) as count FROM ${quotedTableName} e ${countWhereClause}`, countParams)
3268
3347
  ]);
3269
3348
  const provider = await getProvider();
3270
3349
  function entryMatchesLocale(row, locale2) {
@@ -4231,16 +4310,40 @@ function buildDefaultAlt(filename) {
4231
4310
  const withoutExtension = baseName.replace(/\.[^.]+$/, "").trim();
4232
4311
  return withoutExtension || baseName.trim();
4233
4312
  }
4313
+ function mimeForHLSFile(filename) {
4314
+ const ext = filename.toLowerCase().split(".").pop();
4315
+ switch (ext) {
4316
+ case "m3u8":
4317
+ return "application/vnd.apple.mpegurl";
4318
+ case "ts":
4319
+ return "video/mp2t";
4320
+ case "m4s":
4321
+ return "video/iso.segment";
4322
+ case "mp4":
4323
+ return "video/mp4";
4324
+ case "aac":
4325
+ return "audio/aac";
4326
+ case "vtt":
4327
+ return "text/vtt";
4328
+ case "key":
4329
+ return "application/octet-stream";
4330
+ default:
4331
+ return null;
4332
+ }
4333
+ }
4234
4334
  async function listMedia(req, res) {
4235
4335
  const page = Math.max(1, parseInt(req.query.page) || 1);
4236
4336
  const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 24));
4237
4337
  const offset = (page - 1) * limit;
4238
4338
  const folderId = req.query.folder_id || null;
4339
+ const search = req.query.search ? String(req.query.search).trim() : null;
4340
+ const searchTerm = search ? `%${search}%` : null;
4239
4341
  const { rows } = await pool_default.query(`SELECT *, COUNT(*) OVER() AS total
4240
4342
  FROM plank_media
4241
4343
  WHERE folder_id IS NOT DISTINCT FROM $3
4344
+ AND ($4::text IS NULL OR filename ILIKE $4 OR alt ILIKE $4 OR caption ILIKE $4)
4242
4345
  ORDER BY created_at DESC
4243
- LIMIT $1 OFFSET $2`, [limit, offset, folderId]);
4346
+ LIMIT $1 OFFSET $2`, [limit, offset, folderId, searchTerm]);
4244
4347
  const provider = await getProvider();
4245
4348
  const items = await Promise.all(rows.map(async (r2) => ({
4246
4349
  id: r2.id,
@@ -4285,30 +4388,25 @@ async function uploadMedia(req, res) {
4285
4388
  const prefix = [MEDIA_PREFIX, folderId, bundleId].filter(Boolean).join("/");
4286
4389
  const rootDir = m3u8File.originalname.includes("/") ? m3u8File.originalname.split("/")[0] : null;
4287
4390
  const stripRoot = (path) => rootDir && path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path;
4288
- await Promise.all(files.map((file2) => {
4391
+ const m3u8Mime = mimeForHLSFile(m3u8File.originalname) ?? "application/vnd.apple.mpegurl";
4392
+ const uploaded = await Promise.all(files.map(async (file2) => {
4289
4393
  const relativePath = stripRoot(file2.originalname);
4290
- const exactKey = `${prefix}/${relativePath}`;
4291
- return provider.uploadRaw(file2.buffer, exactKey, file2.mimetype);
4394
+ const relativeKey = `${prefix}/${relativePath}`;
4395
+ const mimeType = mimeForHLSFile(relativePath) ?? file2.mimetype;
4396
+ const result = await provider.uploadRaw(file2.buffer, relativeKey, mimeType);
4397
+ return { file: file2, result };
4292
4398
  }));
4293
- const m3u8RelPath = stripRoot(m3u8File.originalname);
4294
- const m3u8Key = `${prefix}/${m3u8RelPath}`;
4295
- const m3u8Url = await provider.getUrl(m3u8Key);
4399
+ const m3u8 = uploaded.find((u) => u.file === m3u8File)?.result;
4400
+ if (!m3u8) {
4401
+ res.status(500).json({ error: "Failed to upload HLS playlist" });
4402
+ return;
4403
+ }
4296
4404
  const id2 = createId();
4297
4405
  const filename = m3u8File.originalname.split("/").pop() ?? m3u8File.originalname;
4298
4406
  const alt2 = buildDefaultAlt(filename);
4299
4407
  await pool_default.query(`INSERT INTO plank_media (id, filename, url, provider_key, mime_type, size, alt, folder_id, uploaded_by)
4300
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
4301
- id2,
4302
- filename,
4303
- m3u8Url,
4304
- m3u8Key,
4305
- m3u8File.mimetype,
4306
- m3u8File.size,
4307
- alt2,
4308
- folderId,
4309
- req.user.id
4310
- ]);
4311
- res.status(201).json({ id: id2, url: m3u8Url, filename, alt: alt2, caption: null });
4408
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [id2, filename, m3u8.url, m3u8.key, m3u8Mime, m3u8File.size, alt2, folderId, req.user.id]);
4409
+ res.status(201).json({ id: id2, url: m3u8.url, filename, alt: alt2, caption: null });
4312
4410
  return;
4313
4411
  }
4314
4412
  const file = files[0];
@@ -4345,7 +4443,13 @@ async function deleteMedia(req, res) {
4345
4443
  return;
4346
4444
  }
4347
4445
  const provider = await getProvider();
4348
- await provider.delete(rows[0].provider_key);
4446
+ const key = rows[0].provider_key;
4447
+ if (key.toLowerCase().endsWith(".m3u8")) {
4448
+ const bundlePrefix = key.substring(0, key.lastIndexOf("/"));
4449
+ await provider.deletePrefix(bundlePrefix);
4450
+ } else {
4451
+ await provider.delete(key);
4452
+ }
4349
4453
  await pool_default.query("DELETE FROM plank_media WHERE id = $1", [id]);
4350
4454
  res.status(204).end();
4351
4455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.19.0",
3
+ "version": "0.20.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/core": "0.19.0",
59
- "@plank-cms/db": "0.19.0",
60
- "@plank-cms/schema": "0.19.0"
58
+ "@plank-cms/db": "0.20.0",
59
+ "@plank-cms/schema": "0.20.0",
60
+ "@plank-cms/core": "0.20.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",