@dyrected/core 2.11.0 → 2.12.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.
package/dist/server.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  touchAuthSession,
31
31
  transitionWorkflow,
32
32
  verifyCollectionToken
33
- } from "./chunk-H7DVVW2P.js";
33
+ } from "./chunk-VOE2LOLF.js";
34
34
  import {
35
35
  attachRequestMetrics,
36
36
  bindObservabilityRuntime,
@@ -870,7 +870,7 @@ function mergeWhereConstraint(where, constraint) {
870
870
 
871
871
  // src/utils/action-mutation.ts
872
872
  function resolveActionMutation(mutation, ctx) {
873
- if (!mutation) return {};
873
+ if (!mutation) return { ...ctx.input ?? {} };
874
874
  return resolveValue(mutation, ctx);
875
875
  }
876
876
  function resolveValue(value, ctx) {
@@ -1032,7 +1032,7 @@ var CollectionController = class {
1032
1032
  if (this.collection.admin?.filterable === false) {
1033
1033
  where = void 0;
1034
1034
  } else {
1035
- const { sanitizeWhereClause } = await import("./where-sanitizer-47QPJQ6O.js");
1035
+ const { sanitizeWhereClause } = await import("./where-sanitizer-CAAIYSSK.js");
1036
1036
  where = sanitizeWhereClause(where, this.collection.fields);
1037
1037
  if (Object.keys(where).length === 0) {
1038
1038
  where = void 0;
@@ -2088,9 +2088,31 @@ var CollectionController = class {
2088
2088
  const body = await c.req.json().catch(() => null);
2089
2089
  if (body?.ids && Array.isArray(body.ids)) {
2090
2090
  ids = body.ids;
2091
+ } else if (Array.isArray(body)) {
2092
+ ids = body;
2091
2093
  }
2092
2094
  } catch {
2093
2095
  }
2096
+ if (!ids.length) {
2097
+ try {
2098
+ const parsed = await c.req.parseBody().catch(() => null);
2099
+ if (parsed) {
2100
+ if (Array.isArray(parsed.ids)) {
2101
+ ids = parsed.ids.filter((x) => typeof x === "string");
2102
+ } else if (Array.isArray(parsed["ids[]"])) {
2103
+ ids = parsed["ids[]"].filter((x) => typeof x === "string");
2104
+ } else if (typeof parsed.ids === "string") {
2105
+ try {
2106
+ const json = JSON.parse(parsed.ids);
2107
+ if (Array.isArray(json)) ids = json;
2108
+ } catch {
2109
+ ids = [parsed.ids];
2110
+ }
2111
+ }
2112
+ }
2113
+ } catch {
2114
+ }
2115
+ }
2094
2116
  if (!ids.length) {
2095
2117
  const raw = c.req.queries("ids") ?? c.req.queries("ids[]") ?? [];
2096
2118
  ids = raw.filter(Boolean);
@@ -2227,7 +2249,7 @@ var CollectionController = class {
2227
2249
  groupBy = queryGroupBy.trim();
2228
2250
  }
2229
2251
  }
2230
- const { sanitizeWhereClause } = await import("./where-sanitizer-47QPJQ6O.js");
2252
+ const { sanitizeWhereClause } = await import("./where-sanitizer-CAAIYSSK.js");
2231
2253
  const sanitizedAggregates = {};
2232
2254
  for (const [key, op] of Object.entries(rawAggregates)) {
2233
2255
  if (!op || typeof op !== "object" || Array.isArray(op)) {
@@ -2508,6 +2530,7 @@ var MediaController = class {
2508
2530
  }
2509
2531
  const body = await c.req.parseBody();
2510
2532
  const file = body["file"];
2533
+ const folderId = body["folderId"] || null;
2511
2534
  const focalPointStr = body["focalPoint"];
2512
2535
  const focalPoint = focalPointStr ? JSON.parse(focalPointStr) : void 0;
2513
2536
  if (!file) {
@@ -2548,8 +2571,8 @@ var MediaController = class {
2548
2571
  config: colConfig?.upload,
2549
2572
  focalPoint
2550
2573
  });
2551
- imageMetadata = processed.metadata;
2552
- imageSizes = processed.sizes;
2574
+ imageMetadata = processed.metadata || {};
2575
+ imageSizes = processed.sizes || {};
2553
2576
  } catch (err) {
2554
2577
  getRequestLogger(c, this.loggerComponent).error({
2555
2578
  err,
@@ -2565,9 +2588,28 @@ var MediaController = class {
2565
2588
  mimeType: file.type,
2566
2589
  prefix
2567
2590
  });
2591
+ const width = fileData.width || imageMetadata.width;
2592
+ const height = fileData.height || imageMetadata.height;
2593
+ const aspectRatio = width && height ? width / height : void 0;
2594
+ const customFields = {};
2595
+ for (const [key, val] of Object.entries(body)) {
2596
+ if (!["file", "focalPoint", "folderId"].includes(key) && typeof val === "string") {
2597
+ try {
2598
+ customFields[key] = JSON.parse(val);
2599
+ } catch {
2600
+ customFields[key] = val;
2601
+ }
2602
+ }
2603
+ }
2568
2604
  const finalFileData = {
2605
+ ...customFields,
2569
2606
  ...fileData,
2570
2607
  ...imageMetadata,
2608
+ originalFilename: file.name,
2609
+ folderId,
2610
+ width,
2611
+ height,
2612
+ aspectRatio,
2571
2613
  focalPoint,
2572
2614
  sizes: {}
2573
2615
  };
@@ -2608,15 +2650,143 @@ var MediaController = class {
2608
2650
  });
2609
2651
  return c.json(doc, 201);
2610
2652
  }
2653
+ async replace(c) {
2654
+ const id = c.req.param("id");
2655
+ const config = c.get("config");
2656
+ const storage = config.storage;
2657
+ const imageService = config.image;
2658
+ const db = config.db;
2659
+ if (!storage) {
2660
+ return c.json({ message: "Storage not configured" }, 500);
2661
+ }
2662
+ if (!db) {
2663
+ return c.json({ message: "Database not configured" }, 500);
2664
+ }
2665
+ if (!id) {
2666
+ return c.json({ message: "Missing ID" }, 400);
2667
+ }
2668
+ const existingDoc = await db.findOne({ collection: this.collection, id });
2669
+ if (!existingDoc) {
2670
+ return c.json({ message: "Asset not found" }, 404);
2671
+ }
2672
+ const body = await c.req.parseBody();
2673
+ const file = body["file"];
2674
+ if (!file) {
2675
+ return c.json({ message: "No file uploaded" }, 400);
2676
+ }
2677
+ const siteId = c.get("siteId");
2678
+ let colConfig = config.collections.find((col) => col.slug === this.collection);
2679
+ if (!colConfig && config.onSchemaFetch && siteId) {
2680
+ const requestConfig = mergeDynamicConfig(config, await config.onSchemaFetch(siteId));
2681
+ colConfig = requestConfig.collections.find((col) => col.slug === this.collection);
2682
+ }
2683
+ const uploadConfig = typeof colConfig?.upload === "object" ? colConfig.upload : void 0;
2684
+ const validationError = validateUpload(file, uploadConfig);
2685
+ if (validationError) {
2686
+ return c.json({ message: validationError.message }, validationError.status);
2687
+ }
2688
+ const buffer = new Uint8Array(await file.arrayBuffer());
2689
+ const workspaceId = c.get("workspaceId");
2690
+ const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId || "default";
2691
+ const uniqueFilename = generateUniqueUploadFilename(file.name);
2692
+ let imageMetadata = {};
2693
+ let imageSizes = {};
2694
+ if (imageService && file.type.startsWith("image/")) {
2695
+ try {
2696
+ const processed = await imageService.process({
2697
+ buffer,
2698
+ mimeType: file.type,
2699
+ config: colConfig?.upload,
2700
+ focalPoint: existingDoc.focalPoint
2701
+ });
2702
+ imageMetadata = processed.metadata || {};
2703
+ imageSizes = processed.sizes || {};
2704
+ } catch (err) {
2705
+ getRequestLogger(c, this.loggerComponent).error({
2706
+ err,
2707
+ msg: "Image processing failed during replace",
2708
+ collection: this.collection,
2709
+ filename: file.name
2710
+ });
2711
+ }
2712
+ }
2713
+ const fileData = await storage.upload({
2714
+ filename: uniqueFilename,
2715
+ buffer,
2716
+ mimeType: file.type,
2717
+ prefix
2718
+ });
2719
+ const width = fileData.width || imageMetadata.width;
2720
+ const height = fileData.height || imageMetadata.height;
2721
+ const aspectRatio = width && height ? width / height : void 0;
2722
+ const finalFileData = {
2723
+ ...fileData,
2724
+ ...imageMetadata,
2725
+ originalFilename: file.name,
2726
+ width,
2727
+ height,
2728
+ aspectRatio,
2729
+ sizes: {}
2730
+ };
2731
+ if (imageSizes) {
2732
+ for (const [sizeName, sizeData] of Object.entries(imageSizes)) {
2733
+ const lastDot = uniqueFilename.lastIndexOf(".");
2734
+ const ext = lastDot !== -1 ? uniqueFilename.substring(lastDot) : "";
2735
+ const baseName = lastDot !== -1 ? uniqueFilename.substring(0, lastDot) : uniqueFilename;
2736
+ const sizeFilename = `${baseName}-${sizeName}${ext}`;
2737
+ try {
2738
+ const sizeFileData = await storage.upload({
2739
+ filename: sizeFilename,
2740
+ buffer: sizeData.buffer,
2741
+ mimeType: file.type,
2742
+ prefix
2743
+ });
2744
+ finalFileData.sizes[sizeName] = {
2745
+ ...sizeFileData,
2746
+ width: sizeData.width,
2747
+ height: sizeData.height
2748
+ };
2749
+ } catch (err) {
2750
+ getRequestLogger(c, this.loggerComponent).error({
2751
+ err,
2752
+ msg: "Failed to upload image size during replace",
2753
+ collection: this.collection,
2754
+ filename: file.name,
2755
+ sizeName
2756
+ });
2757
+ }
2758
+ }
2759
+ }
2760
+ if (existingDoc.filename && existingDoc.filename !== fileData.filename) {
2761
+ try {
2762
+ await storage.delete({ filename: existingDoc.filename });
2763
+ } catch {
2764
+ }
2765
+ }
2766
+ const updatedDoc = await db.update({
2767
+ collection: this.collection,
2768
+ id,
2769
+ data: finalFileData
2770
+ });
2771
+ return c.json(updatedDoc, 200);
2772
+ }
2611
2773
  async find(c) {
2612
2774
  const db = c.get("config").db;
2613
2775
  if (!db) return c.json({ message: "Database not configured" }, 500);
2614
2776
  const limit = Number(c.req.query("limit")) || 10;
2615
2777
  const page = Number(c.req.query("page")) || 1;
2778
+ const sort = c.req.query("sort") || void 0;
2779
+ const folderId = c.req.query("folderId");
2780
+ const where = {};
2781
+ if (folderId !== void 0) {
2782
+ where.folderId = folderId === "root" || folderId === "null" ? null : folderId;
2783
+ }
2616
2784
  const result = await db.find({
2617
2785
  collection: this.collection,
2618
2786
  limit,
2619
- page
2787
+ page,
2788
+ sort,
2789
+ where: Object.keys(where).length > 0 ? where : void 0
2620
2790
  });
2621
2791
  return c.json(result);
2622
2792
  }
@@ -2645,21 +2815,195 @@ var MediaController = class {
2645
2815
  async serve(c) {
2646
2816
  const config = c.get("config");
2647
2817
  const storage = config.storage;
2648
- if (!storage || !storage.resolve) {
2818
+ if (!storage) {
2649
2819
  return c.json({ message: "Storage not configured for serving" }, 404);
2650
2820
  }
2651
2821
  const filename = c.req.param("filename");
2652
2822
  if (!filename) return c.json({ message: "Missing filename" }, 400);
2823
+ const key = c.req.query("key");
2824
+ const width = c.req.query("width") ? Number(c.req.query("width")) : void 0;
2825
+ const height = c.req.query("height") ? Number(c.req.query("height")) : void 0;
2826
+ const aspectRatio = c.req.query("aspectRatio") || c.req.query("aspect_ratio");
2827
+ const crop = c.req.query("crop") || c.req.query("fit");
2828
+ const gravity = c.req.query("gravity");
2829
+ const format = c.req.query("format");
2830
+ const qualityParam = c.req.query("quality");
2831
+ const quality = qualityParam ? isNaN(Number(qualityParam)) ? qualityParam : Number(qualityParam) : void 0;
2832
+ const download = c.req.query("download") === "true" || c.req.query("download") === "1";
2833
+ let transform;
2834
+ if (key && config.media?.presets?.[key]) {
2835
+ transform = { ...config.media.presets[key] };
2836
+ } else if (width || height || aspectRatio || crop || gravity || format || quality) {
2837
+ transform = { width, height, aspectRatio, crop, gravity, format, quality };
2838
+ }
2839
+ if (transform && typeof storage.getURL === "function") {
2840
+ const targetUrl = storage.getURL({ filename, transform });
2841
+ if (targetUrl && targetUrl.startsWith("http")) {
2842
+ return c.redirect(targetUrl, 302);
2843
+ }
2844
+ }
2845
+ if (!storage.resolve) {
2846
+ const publicUrl = storage.getURL({ filename, transform });
2847
+ return c.redirect(publicUrl, 302);
2848
+ }
2653
2849
  let res = await storage.resolve({ filename });
2654
2850
  if (!res && !filename.includes("/")) {
2655
2851
  res = await storage.resolve({ filename: `default/${filename}` });
2656
2852
  }
2657
2853
  if (!res) return c.json({ message: "Not Found" }, 404);
2658
2854
  c.header("Content-Type", res.mimeType);
2855
+ c.header("Cache-Control", "public, max-age=31536000, immutable");
2856
+ if (download) {
2857
+ const downloadName = filename.split("/").pop() || "download";
2858
+ c.header("Content-Disposition", `attachment; filename="${downloadName}"`);
2859
+ }
2659
2860
  return c.body(res.buffer);
2660
2861
  }
2661
2862
  };
2662
2863
 
2864
+ // src/controllers/media-folders.controller.ts
2865
+ var MediaFolderController = class {
2866
+ collection;
2867
+ constructor(collection = "media") {
2868
+ this.collection = collection;
2869
+ }
2870
+ async list(c) {
2871
+ const config = c.get("config");
2872
+ const db = config.db;
2873
+ if (!db) {
2874
+ return c.json({ message: "Database not configured" }, 500);
2875
+ }
2876
+ try {
2877
+ const result = await db.find({
2878
+ collection: "_media_folders",
2879
+ where: {
2880
+ collection: { equals: this.collection }
2881
+ },
2882
+ limit: 1e3,
2883
+ sort: "name"
2884
+ });
2885
+ return c.json(result);
2886
+ } catch {
2887
+ return c.json({
2888
+ docs: [],
2889
+ total: 0,
2890
+ limit: 1e3,
2891
+ page: 1,
2892
+ totalPages: 1,
2893
+ hasNextPage: false,
2894
+ hasPrevPage: false
2895
+ });
2896
+ }
2897
+ }
2898
+ async create(c) {
2899
+ const config = c.get("config");
2900
+ const db = config.db;
2901
+ if (!db) {
2902
+ return c.json({ message: "Database not configured" }, 500);
2903
+ }
2904
+ const body = await c.req.json().catch(() => ({}));
2905
+ const name = String(body.name || "").trim();
2906
+ const parentId = body.parentId ? String(body.parentId) : null;
2907
+ const color = body.color ? String(body.color) : null;
2908
+ if (!name) {
2909
+ return c.json({ message: "Folder name is required" }, 400);
2910
+ }
2911
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2912
+ let path = `/${slug}`;
2913
+ if (parentId) {
2914
+ try {
2915
+ const parent = await db.findOne({
2916
+ collection: "_media_folders",
2917
+ id: parentId
2918
+ });
2919
+ if (parent && parent.path) {
2920
+ path = `${parent.path}/${slug}`;
2921
+ }
2922
+ } catch {
2923
+ }
2924
+ }
2925
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2926
+ const folderData = {
2927
+ name,
2928
+ slug,
2929
+ collection: this.collection,
2930
+ parentId,
2931
+ path,
2932
+ color,
2933
+ createdAt: now,
2934
+ updatedAt: now
2935
+ };
2936
+ const doc = await db.create({
2937
+ collection: "_media_folders",
2938
+ data: folderData
2939
+ });
2940
+ return c.json(doc, 201);
2941
+ }
2942
+ async update(c) {
2943
+ const config = c.get("config");
2944
+ const db = config.db;
2945
+ const id = c.req.param("id");
2946
+ if (!db) {
2947
+ return c.json({ message: "Database not configured" }, 500);
2948
+ }
2949
+ if (!id) {
2950
+ return c.json({ message: "Folder ID is required" }, 400);
2951
+ }
2952
+ const body = await c.req.json().catch(() => ({}));
2953
+ const existing = await db.findOne({
2954
+ collection: "_media_folders",
2955
+ id
2956
+ });
2957
+ if (!existing) {
2958
+ return c.json({ message: "Folder not found" }, 404);
2959
+ }
2960
+ const updateData = {
2961
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2962
+ };
2963
+ if (body.name !== void 0) {
2964
+ const name = String(body.name).trim();
2965
+ if (!name) return c.json({ message: "Folder name cannot be empty" }, 400);
2966
+ updateData.name = name;
2967
+ updateData.slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2968
+ }
2969
+ if (body.color !== void 0) {
2970
+ updateData.color = body.color || null;
2971
+ }
2972
+ if (body.parentId !== void 0) {
2973
+ updateData.parentId = body.parentId || null;
2974
+ }
2975
+ const updated = await db.update({
2976
+ collection: "_media_folders",
2977
+ id,
2978
+ data: updateData
2979
+ });
2980
+ return c.json(updated);
2981
+ }
2982
+ async delete(c) {
2983
+ const config = c.get("config");
2984
+ const db = config.db;
2985
+ const id = c.req.param("id");
2986
+ if (!db) {
2987
+ return c.json({ message: "Database not configured" }, 500);
2988
+ }
2989
+ if (!id) {
2990
+ return c.json({ message: "Folder ID is required" }, 400);
2991
+ }
2992
+ const existing = await db.findOne({
2993
+ collection: "_media_folders",
2994
+ id
2995
+ });
2996
+ if (!existing) {
2997
+ return c.json({ message: "Folder not found" }, 404);
2998
+ }
2999
+ await db.delete({
3000
+ collection: "_media_folders",
3001
+ id
3002
+ });
3003
+ return c.json({ success: true, id });
3004
+ }
3005
+ };
3006
+
2663
3007
  // src/controllers/auth.controller.ts
2664
3008
  import { randomBytes as randomBytes2 } from "crypto";
2665
3009
 
@@ -4465,6 +4809,9 @@ function accessGate(config, target, action) {
4465
4809
  function serializeFieldForApi(f) {
4466
4810
  if (!f) return f;
4467
4811
  const serialized = { ...f };
4812
+ if (typeof serialized.defaultValue === "function") {
4813
+ serialized.defaultValue = `${SERIALIZED_ADMIN_HOOK_PREFIX}${serialized.defaultValue.toString()}`;
4814
+ }
4468
4815
  if (serialized.admin?.hooks) {
4469
4816
  const hooks = { ...serialized.admin.hooks };
4470
4817
  if (typeof hooks.onChange === "function") {
@@ -4990,6 +5337,22 @@ function registerRoutes(app, config) {
4990
5337
  });
4991
5338
  return c.json({ success: true });
4992
5339
  });
5340
+ app.get("/api/media/folders", async (c) => {
5341
+ const folderController = new MediaFolderController("media");
5342
+ return folderController.list(c);
5343
+ });
5344
+ app.post("/api/media/folders", async (c) => {
5345
+ const folderController = new MediaFolderController("media");
5346
+ return folderController.create(c);
5347
+ });
5348
+ app.patch("/api/media/folders/:id", async (c) => {
5349
+ const folderController = new MediaFolderController("media");
5350
+ return folderController.update(c);
5351
+ });
5352
+ app.delete("/api/media/folders/:id", async (c) => {
5353
+ const folderController = new MediaFolderController("media");
5354
+ return folderController.delete(c);
5355
+ });
4993
5356
  app.get("/api/media/:filename{.+$}", async (c) => {
4994
5357
  const mediaController = new MediaController("media");
4995
5358
  return mediaController.serve(c);
@@ -5002,10 +5365,17 @@ function registerRoutes(app, config) {
5002
5365
  const uploadCollections = config.collections.filter((c) => c.upload);
5003
5366
  for (const col of uploadCollections) {
5004
5367
  const mediaController = new MediaController(col.slug);
5368
+ const folderController = new MediaFolderController(col.slug);
5005
5369
  const prefix = `/api/collections/${col.slug}`;
5370
+ app.get(`${prefix}/folders`, accessGate(config, col, "read"), (c) => folderController.list(c));
5371
+ app.post(`${prefix}/folders`, accessGate(config, col, "create"), (c) => folderController.create(c));
5372
+ app.patch(`${prefix}/folders/:id`, accessGate(config, col, "update"), (c) => folderController.update(c));
5373
+ app.delete(`${prefix}/folders/:id`, accessGate(config, col, "delete"), (c) => folderController.delete(c));
5006
5374
  app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
5007
5375
  app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
5008
5376
  app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
5377
+ app.post(`${prefix}/media/:id/file`, accessGate(config, col, "update"), (c) => mediaController.replace(c));
5378
+ app.put(`${prefix}/media/:id/file`, accessGate(config, col, "update"), (c) => mediaController.replace(c));
5009
5379
  app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
5010
5380
  }
5011
5381
  }
@@ -10,6 +10,25 @@ var NEVER_FILTERABLE_TYPES = [
10
10
  "join",
11
11
  "collapsible"
12
12
  ];
13
+ var SYSTEM_FILTERABLE_FIELDS = /* @__PURE__ */ new Set([
14
+ "id",
15
+ "createdAt",
16
+ "updatedAt",
17
+ "createdBy",
18
+ "updatedBy",
19
+ "folderId",
20
+ "filename",
21
+ "originalFilename",
22
+ "mimeType",
23
+ "filesize",
24
+ "url",
25
+ "width",
26
+ "height",
27
+ "aspectRatio",
28
+ "blurhash",
29
+ "alt",
30
+ "caption"
31
+ ]);
13
32
  function sanitizeWhereClause(where, fields) {
14
33
  const fieldMap = /* @__PURE__ */ new Map();
15
34
  function addFieldsToMap(fList, prefix = "") {
@@ -38,7 +57,7 @@ function sanitizeWhereClause(where, fields) {
38
57
  }
39
58
  continue;
40
59
  }
41
- if (key === "id") {
60
+ if (SYSTEM_FILTERABLE_FIELDS.has(key)) {
42
61
  result[key] = value;
43
62
  continue;
44
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/core",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",