@murumets-ee/media 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,53 +1,44 @@
1
1
  {
2
2
  "name": "@murumets-ee/media",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js",
10
- "require": "./dist/index.cjs"
9
+ "import": "./dist/index.js"
11
10
  },
12
11
  "./client": {
13
12
  "types": "./dist/client.d.ts",
14
- "import": "./dist/client.js",
15
- "require": "./dist/client.cjs"
13
+ "import": "./dist/client.js"
16
14
  },
17
15
  "./query": {
18
16
  "types": "./dist/query-client.d.ts",
19
- "import": "./dist/query-client.js",
20
- "require": "./dist/query-client.cjs"
17
+ "import": "./dist/query-client.js"
21
18
  },
22
19
  "./plugin": {
23
20
  "types": "./dist/plugin.d.ts",
24
- "import": "./dist/plugin.js",
25
- "require": "./dist/plugin.cjs"
21
+ "import": "./dist/plugin.js"
26
22
  },
27
23
  "./ref": {
28
24
  "types": "./dist/ref.d.ts",
29
- "import": "./dist/ref.js",
30
- "require": "./dist/ref.cjs"
25
+ "import": "./dist/ref.js"
31
26
  },
32
27
  "./picker": {
33
28
  "types": "./dist/picker.d.ts",
34
- "import": "./dist/picker.js",
35
- "require": "./dist/picker.cjs"
29
+ "import": "./dist/picker.js"
36
30
  },
37
31
  "./admin": {
38
32
  "types": "./dist/admin.d.ts",
39
- "import": "./dist/admin.js",
40
- "require": "./dist/admin.cjs"
33
+ "import": "./dist/admin.js"
41
34
  },
42
35
  "./usage": {
43
36
  "types": "./dist/usage.d.ts",
44
- "import": "./dist/usage.js",
45
- "require": "./dist/usage.cjs"
37
+ "import": "./dist/usage.js"
46
38
  },
47
39
  "./image-styles": {
48
40
  "types": "./dist/image-styles.d.ts",
49
- "import": "./dist/image-styles.js",
50
- "require": "./dist/image-styles.cjs"
41
+ "import": "./dist/image-styles.js"
51
42
  }
52
43
  },
53
44
  "files": [
@@ -61,18 +52,18 @@
61
52
  "server-only": "^0.0.1",
62
53
  "sharp": "^0.34.5",
63
54
  "tailwind-merge": "^2.6.0",
64
- "@murumets-ee/core": "0.1.1",
65
- "@murumets-ee/db": "0.1.1",
66
- "@murumets-ee/logging": "0.1.1",
67
- "@murumets-ee/entity": "0.1.1",
68
- "@murumets-ee/settings": "0.1.1",
69
- "@murumets-ee/storage": "0.1.1"
55
+ "@murumets-ee/core": "0.1.3",
56
+ "@murumets-ee/db": "0.1.2",
57
+ "@murumets-ee/logging": "0.1.3",
58
+ "@murumets-ee/entity": "0.1.2",
59
+ "@murumets-ee/settings": "0.1.3",
60
+ "@murumets-ee/storage": "0.1.3"
70
61
  },
71
62
  "peerDependencies": {
72
63
  "lucide-react": ">=0.400.0",
73
64
  "react": ">=19.0.0",
74
65
  "react-dom": ">=19.0.0",
75
- "@murumets-ee/ui": "0.1.1"
66
+ "@murumets-ee/ui": "0.1.2"
76
67
  },
77
68
  "peerDependenciesMeta": {
78
69
  "@murumets-ee/ui": {
package/dist/admin.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var ge=Object.create;var E=Object.defineProperty;var ue=Object.getOwnPropertyDescriptor;var fe=Object.getOwnPropertyNames;var ye=Object.getPrototypeOf,pe=Object.prototype.hasOwnProperty;var C=(a,e)=>()=>(a&&(e=a(a=0)),e);var x=(a,e)=>{for(var t in e)E(a,t,{get:e[t],enumerable:!0})},X=(a,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of fe(e))!pe.call(a,r)&&r!==t&&E(a,r,{get:()=>e[r],enumerable:!(i=ue(e,r))||i.enumerable});return a};var m=(a,e,t)=>(t=a!=null?ge(ye(a)):{},X(e||!a||!a.__esModule?E(t,"default",{value:a,enumerable:!0}):t,a)),he=a=>X(E({},"__esModule",{value:!0}),a);var Y={};x(Y,{Media:()=>F});var w,F,q=C(()=>{"use strict";w=require("@murumets-ee/entity"),F=(0,w.defineEntity)({name:"media",fields:{title:w.field.text({translatable:!0}),alt:w.field.text({translatable:!0}),description:w.field.text({translatable:!0}),fileKey:w.field.text({required:!0,indexed:!0}),filename:w.field.text({required:!0}),mimeType:w.field.text({required:!0,indexed:!0}),size:w.field.number({required:!0,integer:!0}),width:w.field.number({integer:!0}),height:w.field.number({integer:!0}),mediaType:w.field.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[w.behavior.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}})});function O(a){return a.startsWith("image/")&&!we.has(a)}async function V(a,e){let t=await(0,_.default)(a).metadata(),i=t.width??0,r=t.height??0,s=new Map,o=Object.entries(e);return await Promise.all(o.map(async([l,g])=>{let u=g.format??"webp",c=g.quality??80,f=g.fit??"cover",y=(0,_.default)(a).resize({width:g.width,height:g.height,fit:f,withoutEnlargement:!0}),{data:d,info:n}=await y[u]({quality:c}).toBuffer({resolveWithObject:!0});s.set(l,{buffer:d,format:u,mimeType:`image/${u}`,width:n.width,height:n.height})})),{width:i,height:r,variants:s}}var _,we,W=C(()=>{"use strict";_=m(require("sharp"),1),we=new Set(["image/svg+xml","image/gif"])});function j(a,e,t="webp"){let i=a.lastIndexOf("/"),r=a.substring(0,i),o=a.substring(i+1).replace(/\.[^.]+$/,"");return`${r}/${e}_${o}.${t}`}var J=C(()=>{"use strict"});var z={};x(z,{imageStylesSettings:()=>be});var D,be,K=C(()=>{"use strict";D=require("@murumets-ee/settings"),be=(0,D.defineSettings)({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:D.setting.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}})});var Q={};x(Q,{getMediaConfig:()=>ve,media:()=>Se});function ve(){if(!N)throw new Error("@murumets-ee/media plugin not initialized. Add media() to your plugins array.");return N}function Se(a){let e={acceptedTypes:a?.acceptedTypes??["image/*","video/*","audio/*","application/pdf"],maxUploadSize:a?.maxUploadSize??52428800,defaultVisibility:a?.defaultVisibility??"public",imageStyles:a?.imageStyles??{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}}};return{name:"@murumets-ee/media",entities:[F],init:async t=>{if(!t.plugins.has("@murumets-ee/storage"))throw new Error("@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.");if(!t.plugins.has("@murumets-ee/settings"))throw new Error("@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.");N=e;try{let{createSettingsClient:i}=await import("@murumets-ee/settings"),{imageStylesSettings:r}=await Promise.resolve().then(()=>(K(),z)),s=i(r,{app:t});await s.has("imageStyles")||(await s.set("imageStyles",e.imageStyles),t.logger.info({styles:Object.keys(e.imageStyles)},"Seeded default image styles to settings DB"))}catch(i){t.logger.warn({error:i},"Failed to seed image styles to settings DB (non-fatal)")}t.logger.info({acceptedTypes:e.acceptedTypes,maxUploadSize:e.maxUploadSize,defaultVisibility:e.defaultVisibility},"Media plugin initialized")}}}var N,ee=C(()=>{"use strict";q();N=null});var te={};x(te,{MediaClient:()=>$,createMediaClient:()=>Ie,getMediaClient:()=>ke});function Me(a){return a.startsWith("image/")?"image":a.startsWith("video/")?"video":a.startsWith("audio/")?"audio":a==="application/pdf"||a.startsWith("application/msword")||a.startsWith("application/vnd.")?"document":"other"}function Re(a){return a.replace(/\.[^.]+$/,"").replace(/[-_]/g," ")}async function Ie(a){let{getApp:e}=await import("@murumets-ee/core"),t=e();return new $({db:t.db.readWrite,storage:a,logger:t.logger.child({media:!0})})}function ke(){return G||(G=(async()=>{let{getApp:a}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),i=a(),r=t(),s=e(r,{app:i});return new $({db:i.db.readWrite,storage:s,logger:i.logger.child({media:!0})})})()),G}var Ee,$,G,ie=C(()=>{"use strict";Ee=require("server-only");q();W();J();$=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:e}=await import("@murumets-ee/entity/admin");this.admin=new e({entity:F,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import("@murumets-ee/settings"),{imageStylesSettings:t}=await Promise.resolve().then(()=>(K(),z)),{getApp:i}=await import("@murumets-ee/core"),r=i(),o=await e(t,{app:r}).get("imageStyles");if(o&&Object.keys(o).length>0)return this.imageStyles=o,o}catch{}try{let{getMediaConfig:e}=await Promise.resolve().then(()=>(ee(),Q)),t=e();return this.imageStyles=t.imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let i=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),r=t.width??null,s=t.height??null,o={};if(e instanceof Buffer&&O(t.mimeType))try{let u=await this.resolveImageStyles(),c=await V(e,u);r=c.width,s=c.height;let f=i.visibility;await Promise.all([...c.variants.entries()].map(async([y,d])=>{let n=j(i.key,y,d.format);try{await this.storage.upload(d.buffer,{key:n,filename:`${y}_${t.filename}`,mimeType:d.mimeType,size:d.buffer.byteLength,visibility:f,metadata:{variantOf:i.key,style:y},uploadedBy:t.uploadedBy}),o[y]=n,this.logger?.debug({style:y,key:n,width:d.width,height:d.height},"Variant uploaded")}catch(p){this.logger?.warn({style:y,key:n,error:p},"Failed to upload variant (non-fatal)")}})),Object.keys(o).length>0&&await this.storage.updateMetadata(i.key,{metadata:{...i.metadata??{},variants:o}}).catch(y=>{this.logger?.warn({key:i.key,error:y},"Failed to update original file metadata with variant keys (non-fatal)")}),this.logger?.info({width:r,height:s,variants:Object.keys(o)},"Image processed")}catch(u){this.logger?.warn({filename:t.filename,error:u},"Image processing failed (non-fatal, original saved)")}let l=Me(t.mimeType),g=await this.getAdmin();try{let u=await g.create({title:t.title??Re(t.filename),alt:t.alt??null,description:t.description??null,fileKey:i.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:r,height:s,mediaType:l}),c=await this.storage.getUrl(i.key);return this.logger?.info({id:u.id,fileKey:i.key,mediaType:l},"Media uploaded"),{media:u,url:c}}catch(u){this.logger?.error({fileKey:i.key,error:u},"Media entity creation failed, rolling back storage uploads");for(let c of Object.values(o))await this.storage.delete(c).catch(()=>{});throw await this.storage.delete(i.key).catch(c=>{this.logger?.error({fileKey:i.key,error:c},"Storage rollback also failed")}),u}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{and:r,asc:s,desc:o,eq:l,ilike:g,sql:u}=await import("drizzle-orm"),c=i.get("media");if(!c)throw new Error("Media schema not registered. Is the media() plugin loaded?");let f=[];if(e?.mediaType&&f.push(l(c.mediaType,e.mediaType)),e?.mimeTypePrefix&&f.push(g(c.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let R=`%${e.search}%`;f.push(u`(${g(c.filename,R)} OR ${c.fields} ->> 'title' ILIKE ${R})`)}let y=e?.limit??50,d=e?.offset??0,n=f.length>0?r(...f):void 0,[p]=await this.db.select({count:u`count(*)::int`}).from(c).where(n),h=e?.orderBy==="filename"?c.filename:c.createdAt,S=(e?.orderDirection??"desc")==="asc"?s:o;return{items:await t.findMany({where:n,limit:y,offset:d,orderBy:S(h)}),total:p?.count??0,limit:y,offset:d}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),i=await t.findById(e);if(!i)throw new Error(`Media not found: ${e}`);let r=i.fileKey;await t.delete(e);let o=(await this.storage.getMetadata(r))?.metadata?.variants;if(o)for(let l of Object.values(o))await this.storage.delete(l).catch(g=>{this.logger?.warn({variantKey:l,error:g},"Failed to delete variant file")});await this.storage.delete(r).catch(l=>{this.logger?.error({id:e,fileKey:r,error:l},"Failed to delete file from storage after entity deletion")}),this.logger?.info({id:e,fileKey:r,deletedVariants:o?Object.keys(o).length:0},"Media deleted")}async getUrl(e){let i=await(await this.getAdmin()).findById(e);if(!i)throw new Error(`Media not found: ${e}`);return this.storage.getUrl(i.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{inArray:r}=await import("drizzle-orm"),s=i.get("media");if(!s)return new Map;let o=await t.findMany({where:r(s.id,e),limit:e.length}),l=new Map;return await Promise.all(o.map(async g=>{let u=await this.storage.getUrl(g.fileKey);l.set(g.id,u)})),l}async getVariantUrl(e,t){let r=await(await this.getAdmin()).findById(e);if(!r)return null;let s=r.fileKey,l=(await this.resolveImageStyles())[t];if(l){let g=j(s,t,l.format??"webp");try{return await this.storage.getUrl(g)}catch{}}try{return await this.storage.getUrl(s)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let i=await this.getAdmin(),{schemaRegistry:r}=await import("@murumets-ee/db"),{inArray:s}=await import("drizzle-orm"),o=r.get("media");if(!o)return new Map;let l=await i.findMany({where:s(o.id,e),limit:e.length}),u=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(l.map(async f=>{if(u){let y=j(f.fileKey,t,u.format??"webp");try{let d=await this.storage.getUrl(y);c.set(f.id,d);return}catch{}}try{let y=await this.storage.getUrl(f.fileKey);c.set(f.id,y)}catch{}})),c}};G=null});var re={};x(re,{findMediaUsages:()=>Pe});async function Pe(a,e){let t=await(0,ae.findEntityUsages)("media",a,e),{getApp:i}=await import("@murumets-ee/core"),r=i();return t.map(s=>{let o="field",l=r.entities.get(s.sourceEntity);if(l){let g=l.allFields[s.sourceField];g&&g.type==="blocks"&&(o="block")}return{entityName:s.sourceEntity,entityId:s.sourceId,fieldName:s.sourceField,context:o}})}var De,ae,ne=C(()=>{"use strict";De=require("server-only"),ae=require("@murumets-ee/entity/refs")});var se={};x(se,{regenerateAllVariants:()=>Ce});async function Ce(a){let{db:e,storage:t,logger:i,styles:r}=a,{AdminClient:s}=await import("@murumets-ee/entity/admin"),{Media:o}=await Promise.resolve().then(()=>(q(),Y)),{schemaRegistry:l}=await import("@murumets-ee/db"),{eq:g}=await import("drizzle-orm"),u=new s({entity:o,db:e,logger:i}),c=l.get("media");if(!c)throw new Error("Media schema not registered");let f={total:0,processed:0,skipped:0,errors:0},y=0;for(i?.info({styles:Object.keys(r)},"Starting variant regeneration");;){let d=await u.findMany({where:g(c.mediaType,"image"),limit:Z,offset:y});if(d.length===0)break;f.total+=d.length;for(let n of d)try{if(!O(n.mimeType)){f.skipped++;continue}let p=await t.download(n.fileKey),h;if(Buffer.isBuffer(p.body))h=p.body;else{let b=[],I=p.body.getReader();for(;;){let{done:A,value:U}=await I.read();if(A)break;U&&b.push(U)}h=Buffer.concat(b)}let S=await V(h,r),M=await t.getMetadata(n.fileKey),R=M?.metadata?.variants;if(R)for(let b of Object.values(R))await t.delete(b).catch(()=>{});let P={},T=M?.visibility??"public";for(let[b,I]of S.variants.entries()){let A=j(n.fileKey,b,I.format);try{await t.upload(I.buffer,{key:A,filename:`${b}_${n.filename}`,mimeType:I.mimeType,size:I.buffer.byteLength,visibility:T,metadata:{variantOf:n.fileKey,style:b}}),P[b]=A}catch(U){i?.warn({style:b,key:A,error:U},"Failed to upload regenerated variant (non-fatal)")}}Object.keys(P).length>0&&await t.updateMetadata(n.fileKey,{metadata:{...M?.metadata??{},variants:P}}).catch(b=>{i?.warn({key:n.fileKey,error:b},"Failed to update variant metadata (non-fatal)")}),f.processed++,i?.debug({id:n.id,variants:Object.keys(P)},"Regenerated variants")}catch(p){f.errors++,i?.error({id:n.id,fileKey:n.fileKey,error:p},"Failed to regenerate variants for media record")}if(y+=Z,d.length<Z)break}return i?.info(f,"Variant regeneration complete"),f}var Le,Z,oe=C(()=>{"use strict";Le=require("server-only");W();J();Z=100});var ze={};x(ze,{mediaRoutes:()=>ce});module.exports=he(ze);var le=["cover","contain","inside","outside","fill"],de=["webp","jpeg","png","avif"];function Te(a){for(let[e,t]of Object.entries(a)){if(typeof e!="string"||e.length===0||!/^[a-z][a-z0-9_-]*$/.test(e))return{valid:!1,error:`Invalid style name "${e}": must be lowercase alphanumeric (a-z, 0-9, -, _)`};if(!t||typeof t!="object")return{valid:!1,error:`Style "${e}" must be an object`};let i=t;if(i.width!==void 0&&(typeof i.width!="number"||i.width<=0))return{valid:!1,error:`Invalid width for style "${e}": must be a positive number`};if(i.height!==void 0&&(typeof i.height!="number"||i.height<=0))return{valid:!1,error:`Invalid height for style "${e}": must be a positive number`};if(i.width===void 0&&i.height===void 0)return{valid:!1,error:`Style "${e}" must have at least width or height`};if(i.quality!==void 0&&(typeof i.quality!="number"||i.quality<1||i.quality>100))return{valid:!1,error:`Invalid quality for style "${e}": must be 1-100`};if(i.fit!==void 0&&!le.includes(i.fit))return{valid:!1,error:`Invalid fit for style "${e}": must be one of ${le.join(", ")}`};if(i.format!==void 0&&!de.includes(i.format))return{valid:!1,error:`Invalid format for style "${e}": must be one of ${de.join(", ")}`}}return{valid:!0,styles:a}}var Ae=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function me(a){return Ae.test(a)}var H=null;function L(){return H||(H=(async()=>{let{getApp:a}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),{MediaClient:i}=await Promise.resolve().then(()=>(ie(),te)),r=a(),s=t(),o=e(s,{app:r});return new i({db:r.db.readWrite,storage:o,logger:r.logger.child({media:!0})})})()),H}function k(a,e=200){return new Response(JSON.stringify(a),{status:e,headers:{"Content-Type":"application/json"}})}function v(a,e){return k({error:a},e)}async function xe(a,{segments:e}){let t=await L();if(e.length===1&&e[0]==="settings"){let{createSettingsClient:n}=await import("@murumets-ee/settings"),{imageStylesSettings:p}=await Promise.resolve().then(()=>(K(),z)),{getApp:h}=await import("@murumets-ee/core"),S=h(),R=await n(p,{app:S}).get("imageStyles");return k({imageStyles:R??{}})}if(e.length===2&&e[1]==="usage"){let n=e[0];if(!me(n))return v("Invalid media ID format",400);let{findMediaUsages:p}=await Promise.resolve().then(()=>(ne(),re)),{getApp:h}=await import("@murumets-ee/core"),S=h(),M=await p(n,S.db.readWrite);return k({usages:M})}if(e.length>0){let n=e[0],p=await t.findById(n);if(!p)return v("Media not found",404);let h=await t.getUrl(n);return k({...p,url:h})}let i=new URL(a.url),r=i.searchParams.get("search")??void 0,s=i.searchParams.get("mediaType")??void 0,o=Math.min(Math.max(Number(i.searchParams.get("limit"))||24,1),100),l=Math.max(Number(i.searchParams.get("offset"))||0,0),g=await t.findMany({search:r,mediaType:s,limit:o,offset:l}),u=g.items.map(n=>n.id),[c,f]=await Promise.all([t.getUrls(u),t.getVariantUrls(u,"thumbnail")]),d={items:g.items.map(n=>({id:n.id,title:n.title,alt:n.alt,filename:n.filename,mimeType:n.mimeType,size:n.size,mediaType:n.mediaType,url:c.get(n.id)??"",thumbnailUrl:f.get(n.id),width:n.width,height:n.height})),total:g.total};return k(d)}async function Ue(a,{segments:e,user:t,audit:i,checkPermission:r}){if(e.length===1&&e[0]==="regenerate-variants"){if(!r("media","update"))return v("Forbidden: media update permission required for variant regeneration",403);let{createSettingsClient:p}=await import("@murumets-ee/settings"),{imageStylesSettings:h}=await Promise.resolve().then(()=>(K(),z)),{regenerateAllVariants:S}=await Promise.resolve().then(()=>(oe(),se)),{getApp:M}=await import("@murumets-ee/core"),{createStorageClient:R}=await import("@murumets-ee/storage"),{getStorageConfig:P}=await import("@murumets-ee/storage/plugin"),T=M(),I=await p(h,{app:T}).get("imageStyles");if(!I||Object.keys(I).length===0)return v("No image styles configured",400);let A=P(),U=R(A,{app:T}),B=await S({db:T.db.readWrite,storage:U,logger:T.logger.child({media:!0}),styles:I});return i?.({action:"media.regenerate_variants",userId:t.id,userName:t.name,metadata:{total:B.total,processed:B.processed,errors:B.errors}}),k(B)}if(e.length===1&&e[0]==="settings"){if(!r("media","update"))return v("Forbidden: media update permission required for image style management",403);let p=await a.json();if(!p.imageStyles||typeof p.imageStyles!="object")return v('Body must contain "imageStyles" object',400);let h=Te(p.imageStyles);if(!h.valid)return v(h.error,400);let{createSettingsClient:S}=await import("@murumets-ee/settings"),{imageStylesSettings:M}=await Promise.resolve().then(()=>(K(),z)),{getApp:R}=await import("@murumets-ee/core"),P=R();return await S(M,{app:P}).set("imageStyles",h.styles),(await L()).invalidateImageStylesCache(),i?.({action:"media.settings.update",entityType:"settings",userId:t.id,userName:t.name,changes:{imageStyles:h.styles}}),k({imageStyles:h.styles})}let s=await L(),l=(await a.formData()).get("file");if(!l||l.size===0)return v("No file provided",400);let g=50*1024*1024;if(l.size>g)return v(`File too large: ${(l.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let u=Buffer.from(await l.arrayBuffer()),{detectMimeType:c}=await import("@murumets-ee/storage"),{mimeType:f,mismatch:y}=await c(u,l.type||"application/octet-stream");if(y)return v(`File content doesn't match declared type: claimed ${l.type}, detected ${f}`,400);let d=await s.upload(u,{filename:l.name,mimeType:f,size:l.size,uploadedBy:t.id}),n={id:d.media.id,title:d.media.title,alt:d.media.alt,filename:d.media.filename,mimeType:d.media.mimeType,size:d.media.size,mediaType:d.media.mediaType,url:d.url,width:d.media.width,height:d.media.height};return i?.({action:"media.upload",entityType:"media",entityId:d.media.id,userId:t.id,userName:t.name,changes:{filename:d.media.filename,mimeType:d.media.mimeType,size:d.media.size,mediaType:d.media.mediaType}}),k(n,201)}async function je(a,{segments:e,user:t,audit:i}){if(e.length===0)return v("Media ID required",400);let r=e[0];return me(r)?(await(await L()).delete(r),i?.({action:"media.delete",entityType:"media",entityId:r,userId:t.id,userName:t.name}),k({deleted:1})):v("Invalid media ID format",400)}function ce(){return{prefix:"media",resource:"media",actions:["view","create","update","delete"],handlers:{GET:xe,POST:Ue,DELETE:je}}}0&&(module.exports={mediaRoutes});
package/dist/admin.d.cts DELETED
@@ -1,47 +0,0 @@
1
- import { AdminRoute } from '@murumets-ee/core';
2
-
3
- /**
4
- * Media admin routes for the centralized admin API handler.
5
- *
6
- * Provides media-specific operations: upload, URL resolution, delete with storage cleanup,
7
- * image style settings, and variant regeneration.
8
- *
9
- * Standard entity CRUD (PATCH with translations) falls through to the generic entity
10
- * handler — add `Media` to the `entities` array in your handler config.
11
- *
12
- * @example
13
- * ```typescript
14
- * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
15
- * import { mediaRoutes } from '@murumets-ee/media/admin'
16
- * import { Media } from '@murumets-ee/media'
17
- *
18
- * const handler = createAdminApiHandler({
19
- * authenticate: async (req) => { ... },
20
- * entities: [Article, Media],
21
- * routes: [mediaRoutes()],
22
- * })
23
- * ```
24
- */
25
-
26
- /**
27
- * Create admin API routes for media-specific operations.
28
- *
29
- * Standard entity CRUD (PATCH with translations) is handled by the generic entity
30
- * handler — add `Media` to the `entities` array in your handler config.
31
- *
32
- * Routes handled by this plugin:
33
- * - `GET /api/admin/media` — List media with search/filter + URLs
34
- * - `GET /api/admin/media/settings` — Get current image styles
35
- * - `GET /api/admin/media/:id` — Get single media item with URL
36
- * - `GET /api/admin/media/:id/usage` — Find entities referencing this media
37
- * - `POST /api/admin/media` — Upload file (FormData with `file` field)
38
- * - `POST /api/admin/media/settings` — Save image styles (admin only)
39
- * - `POST /api/admin/media/regenerate-variants` — Regenerate all image variants (admin only)
40
- * - `DELETE /api/admin/media/:id` — Delete media + storage file (blocked if in use)
41
- *
42
- * Routes handled by generic entity handler (via fallthrough):
43
- * - `PATCH /api/admin/media/:id` — Update metadata with translation support
44
- */
45
- declare function mediaRoutes(): AdminRoute;
46
-
47
- export { mediaRoutes };
package/dist/client.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var $=Object.create;var w=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var L=Object.getPrototypeOf,W=Object.prototype.hasOwnProperty;var P=(i,e)=>()=>(i&&(e=i(i=0)),e);var R=(i,e)=>{for(var t in e)w(i,t,{get:e[t],enumerable:!0})},B=(i,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of F(e))!W.call(i,r)&&r!==t&&w(i,r,{get:()=>e[r],enumerable:!(a=j(e,r))||a.enumerable});return i};var c=(i,e,t)=>(t=i!=null?$(L(i)):{},B(e||!i||!i.__esModule?w(t,"default",{value:i,enumerable:!0}):t,i)),D=i=>B(w({},"__esModule",{value:!0}),i);var u,b,k=P(()=>{"use strict";u=require("@murumets-ee/entity"),b=(0,u.defineEntity)({name:"media",fields:{title:u.field.text({translatable:!0}),alt:u.field.text({translatable:!0}),description:u.field.text({translatable:!0}),fileKey:u.field.text({required:!0,indexed:!0}),filename:u.field.text({required:!0}),mimeType:u.field.text({required:!0,indexed:!0}),size:u.field.number({required:!0,integer:!0}),width:u.field.number({integer:!0}),height:u.field.number({integer:!0}),mediaType:u.field.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[u.behavior.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}})});var C={};R(C,{imageStylesSettings:()=>J});var S,J,x=P(()=>{"use strict";S=require("@murumets-ee/settings"),J=(0,S.defineSettings)({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:S.setting.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}})});var q={};R(q,{getMediaConfig:()=>Y,media:()=>G});function Y(){if(!T)throw new Error("@murumets-ee/media plugin not initialized. Add media() to your plugins array.");return T}function G(i){let e={acceptedTypes:i?.acceptedTypes??["image/*","video/*","audio/*","application/pdf"],maxUploadSize:i?.maxUploadSize??52428800,defaultVisibility:i?.defaultVisibility??"public",imageStyles:i?.imageStyles??{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}}};return{name:"@murumets-ee/media",entities:[b],init:async t=>{if(!t.plugins.has("@murumets-ee/storage"))throw new Error("@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.");if(!t.plugins.has("@murumets-ee/settings"))throw new Error("@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.");T=e;try{let{createSettingsClient:a}=await import("@murumets-ee/settings"),{imageStylesSettings:r}=await Promise.resolve().then(()=>(x(),C)),s=a(r,{app:t});await s.has("imageStyles")||(await s.set("imageStyles",e.imageStyles),t.logger.info({styles:Object.keys(e.imageStyles)},"Seeded default image styles to settings DB"))}catch(a){t.logger.warn({error:a},"Failed to seed image styles to settings DB (non-fatal)")}t.logger.info({acceptedTypes:e.acceptedTypes,maxUploadSize:e.maxUploadSize,defaultVisibility:e.defaultVisibility},"Media plugin initialized")}}}var T,z=P(()=>{"use strict";k();T=null});var N={};R(N,{MediaClient:()=>p,createMediaClient:()=>X,getMediaClient:()=>Z});module.exports=D(N);var se=require("server-only");k();var I=c(require("sharp"),1),_=new Set(["image/svg+xml","image/gif"]);function U(i){return i.startsWith("image/")&&!_.has(i)}async function E(i,e){let t=await(0,I.default)(i).metadata(),a=t.width??0,r=t.height??0,s=new Map,n=Object.entries(e);return await Promise.all(n.map(async([d,g])=>{let l=g.format??"webp",o=g.quality??80,f=g.fit??"cover",m=(0,I.default)(i).resize({width:g.width,height:g.height,fit:f,withoutEnlargement:!0}),{data:y,info:h}=await m[l]({quality:o}).toBuffer({resolveWithObject:!0});s.set(d,{buffer:y,format:l,mimeType:`image/${l}`,width:h.width,height:h.height})})),{width:a,height:r,variants:s}}function M(i,e,t="webp"){let a=i.lastIndexOf("/"),r=i.substring(0,a),n=i.substring(a+1).replace(/\.[^.]+$/,"");return`${r}/${e}_${n}.${t}`}var p=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:e}=await import("@murumets-ee/entity/admin");this.admin=new e({entity:b,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import("@murumets-ee/settings"),{imageStylesSettings:t}=await Promise.resolve().then(()=>(x(),C)),{getApp:a}=await import("@murumets-ee/core"),r=a(),n=await e(t,{app:r}).get("imageStyles");if(n&&Object.keys(n).length>0)return this.imageStyles=n,n}catch{}try{let{getMediaConfig:e}=await Promise.resolve().then(()=>(z(),q)),t=e();return this.imageStyles=t.imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let a=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),r=t.width??null,s=t.height??null,n={};if(e instanceof Buffer&&U(t.mimeType))try{let l=await this.resolveImageStyles(),o=await E(e,l);r=o.width,s=o.height;let f=a.visibility;await Promise.all([...o.variants.entries()].map(async([m,y])=>{let h=M(a.key,m,y.format);try{await this.storage.upload(y.buffer,{key:h,filename:`${m}_${t.filename}`,mimeType:y.mimeType,size:y.buffer.byteLength,visibility:f,metadata:{variantOf:a.key,style:m},uploadedBy:t.uploadedBy}),n[m]=h,this.logger?.debug({style:m,key:h,width:y.width,height:y.height},"Variant uploaded")}catch(v){this.logger?.warn({style:m,key:h,error:v},"Failed to upload variant (non-fatal)")}})),Object.keys(n).length>0&&await this.storage.updateMetadata(a.key,{metadata:{...a.metadata??{},variants:n}}).catch(m=>{this.logger?.warn({key:a.key,error:m},"Failed to update original file metadata with variant keys (non-fatal)")}),this.logger?.info({width:r,height:s,variants:Object.keys(n)},"Image processed")}catch(l){this.logger?.warn({filename:t.filename,error:l},"Image processing failed (non-fatal, original saved)")}let d=H(t.mimeType),g=await this.getAdmin();try{let l=await g.create({title:t.title??Q(t.filename),alt:t.alt??null,description:t.description??null,fileKey:a.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:r,height:s,mediaType:d}),o=await this.storage.getUrl(a.key);return this.logger?.info({id:l.id,fileKey:a.key,mediaType:d},"Media uploaded"),{media:l,url:o}}catch(l){this.logger?.error({fileKey:a.key,error:l},"Media entity creation failed, rolling back storage uploads");for(let o of Object.values(n))await this.storage.delete(o).catch(()=>{});throw await this.storage.delete(a.key).catch(o=>{this.logger?.error({fileKey:a.key,error:o},"Storage rollback also failed")}),l}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{and:r,asc:s,desc:n,eq:d,ilike:g,sql:l}=await import("drizzle-orm"),o=a.get("media");if(!o)throw new Error("Media schema not registered. Is the media() plugin loaded?");let f=[];if(e?.mediaType&&f.push(d(o.mediaType,e.mediaType)),e?.mimeTypePrefix&&f.push(g(o.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let K=`%${e.search}%`;f.push(l`(${g(o.filename,K)} OR ${o.fields} ->> 'title' ILIKE ${K})`)}let m=e?.limit??50,y=e?.offset??0,h=f.length>0?r(...f):void 0,[v]=await this.db.select({count:l`count(*)::int`}).from(o).where(h),O=e?.orderBy==="filename"?o.filename:o.createdAt,V=(e?.orderDirection??"desc")==="asc"?s:n;return{items:await t.findMany({where:h,limit:m,offset:y,orderBy:V(O)}),total:v?.count??0,limit:m,offset:y}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),a=await t.findById(e);if(!a)throw new Error(`Media not found: ${e}`);let r=a.fileKey;await t.delete(e);let n=(await this.storage.getMetadata(r))?.metadata?.variants;if(n)for(let d of Object.values(n))await this.storage.delete(d).catch(g=>{this.logger?.warn({variantKey:d,error:g},"Failed to delete variant file")});await this.storage.delete(r).catch(d=>{this.logger?.error({id:e,fileKey:r,error:d},"Failed to delete file from storage after entity deletion")}),this.logger?.info({id:e,fileKey:r,deletedVariants:n?Object.keys(n).length:0},"Media deleted")}async getUrl(e){let a=await(await this.getAdmin()).findById(e);if(!a)throw new Error(`Media not found: ${e}`);return this.storage.getUrl(a.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{inArray:r}=await import("drizzle-orm"),s=a.get("media");if(!s)return new Map;let n=await t.findMany({where:r(s.id,e),limit:e.length}),d=new Map;return await Promise.all(n.map(async g=>{let l=await this.storage.getUrl(g.fileKey);d.set(g.id,l)})),d}async getVariantUrl(e,t){let r=await(await this.getAdmin()).findById(e);if(!r)return null;let s=r.fileKey,d=(await this.resolveImageStyles())[t];if(d){let g=M(s,t,d.format??"webp");try{return await this.storage.getUrl(g)}catch{}}try{return await this.storage.getUrl(s)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let a=await this.getAdmin(),{schemaRegistry:r}=await import("@murumets-ee/db"),{inArray:s}=await import("drizzle-orm"),n=r.get("media");if(!n)return new Map;let d=await a.findMany({where:s(n.id,e),limit:e.length}),l=(await this.resolveImageStyles())[t],o=new Map;return await Promise.all(d.map(async f=>{if(l){let m=M(f.fileKey,t,l.format??"webp");try{let y=await this.storage.getUrl(m);o.set(f.id,y);return}catch{}}try{let m=await this.storage.getUrl(f.fileKey);o.set(f.id,m)}catch{}})),o}};function H(i){return i.startsWith("image/")?"image":i.startsWith("video/")?"video":i.startsWith("audio/")?"audio":i==="application/pdf"||i.startsWith("application/msword")||i.startsWith("application/vnd.")?"document":"other"}function Q(i){return i.replace(/\.[^.]+$/,"").replace(/[-_]/g," ")}async function X(i){let{getApp:e}=await import("@murumets-ee/core"),t=e();return new p({db:t.db.readWrite,storage:i,logger:t.logger.child({media:!0})})}var A=null;function Z(){return A||(A=(async()=>{let{getApp:i}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),a=i(),r=t(),s=e(r,{app:a});return new p({db:a.db.readWrite,storage:s,logger:a.logger.child({media:!0})})})()),A}0&&(module.exports={MediaClient,createMediaClient,getMediaClient});
package/dist/client.d.cts DELETED
@@ -1,102 +0,0 @@
1
- import { Logger } from '@murumets-ee/core';
2
- import { StorageClient } from '@murumets-ee/storage';
3
- import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
4
- import { I as ImageStyle, M as MediaUploadOptions, a as MediaUploadResult, b as MediaRecord, c as MediaListOptions, d as MediaListResult } from './types-ChlTxvlq.cjs';
5
-
6
- /**
7
- * MediaClient — wraps AdminClient + StorageClient for media management.
8
- *
9
- * Usage:
10
- * import { createMediaClient } from '@murumets-ee/media/client'
11
- * const media = await createMediaClient(storageClient)
12
- * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })
13
- */
14
-
15
- interface MediaClientConfig {
16
- db: PostgresJsDatabase;
17
- storage: StorageClient;
18
- logger?: Logger;
19
- /** Image styles to generate on upload. Loaded from plugin config if not provided. */
20
- imageStyles?: Record<string, ImageStyle>;
21
- }
22
- declare class MediaClient {
23
- private db;
24
- private storage;
25
- private logger?;
26
- private admin;
27
- private imageStyles;
28
- constructor(config: MediaClientConfig);
29
- private getAdmin;
30
- /**
31
- * Resolve image styles: settings DB → plugin config → hardcoded defaults.
32
- * Result is cached; call `invalidateImageStylesCache()` after settings update.
33
- */
34
- private resolveImageStyles;
35
- /** Clear cached styles so next access re-reads from settings DB. */
36
- invalidateImageStylesCache(): void;
37
- /**
38
- * Upload a file and create a media entity record in one step.
39
- * 1. Uploads original to storage (StorageClient)
40
- * 2. If image: extracts dimensions via Sharp + generates variants
41
- * 3. Creates media entity record (AdminClient)
42
- * 4. Returns the media record + URL
43
- *
44
- * Rolls back storage upload if entity creation fails.
45
- * Variant generation failures are logged but don't fail the upload.
46
- */
47
- upload(data: Buffer | ReadableStream<Uint8Array>, options: MediaUploadOptions): Promise<MediaUploadResult>;
48
- findById(id: string, options?: {
49
- locale?: string;
50
- }): Promise<MediaRecord | null>;
51
- findMany(options?: MediaListOptions): Promise<MediaListResult>;
52
- update(id: string, data: {
53
- title?: string;
54
- alt?: string;
55
- description?: string;
56
- }): Promise<MediaRecord>;
57
- /**
58
- * Delete a media entity, its variants, and its original file in storage.
59
- */
60
- delete(id: string): Promise<void>;
61
- /**
62
- * Get URL for a media entity by its ID.
63
- * Resolves entity -> fileKey -> storage URL.
64
- */
65
- getUrl(id: string): Promise<string>;
66
- /**
67
- * Get URLs for multiple media entities (batch).
68
- * Returns a Map of mediaId -> url.
69
- */
70
- getUrls(ids: string[]): Promise<Map<string, string>>;
71
- /**
72
- * Get variant URL for a specific image style.
73
- * Falls back to original URL if the variant doesn't exist.
74
- *
75
- * @param id - Media entity ID
76
- * @param styleName - Image style name (e.g., 'thumbnail')
77
- * @returns The variant URL, or original URL as fallback, or null if media not found
78
- */
79
- getVariantUrl(id: string, styleName: string): Promise<string | null>;
80
- /**
81
- * Get variant URLs for multiple media entities (batch).
82
- * Falls back to original URL per item if the variant doesn't exist.
83
- *
84
- * @param ids - Media entity IDs
85
- * @param styleName - Image style name (e.g., 'thumbnail')
86
- * @returns Map of mediaId -> variant URL (or original URL as fallback)
87
- */
88
- getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>>;
89
- }
90
- /**
91
- * Factory — creates a MediaClient with an explicit StorageClient.
92
- * Must be called after createApp().
93
- */
94
- declare function createMediaClient(storage: StorageClient): Promise<MediaClient>;
95
- /**
96
- * Lazy singleton — returns a shared MediaClient instance.
97
- * Auto-discovers storage configuration from the storage plugin.
98
- * Must be called after createApp().
99
- */
100
- declare function getMediaClient(): Promise<MediaClient>;
101
-
102
- export { MediaClient, type MediaClientConfig, createMediaClient, getMediaClient };
@@ -1 +0,0 @@
1
- "use strict";var m=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var o=Object.getOwnPropertyNames;var r=Object.prototype.hasOwnProperty;var n=(t,e)=>{for(var a in e)m(t,a,{get:e[a],enumerable:!0})},p=(t,e,a,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of o(e))!r.call(t,s)&&s!==a&&m(t,s,{get:()=>e[s],enumerable:!(l=g(e,s))||l.enumerable});return t};var y=t=>p(m({},"__esModule",{value:!0}),t);var c={};n(c,{imageStylesSettings:()=>S});module.exports=y(c);var i=require("@murumets-ee/settings"),S=(0,i.defineSettings)({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:i.setting.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}});0&&(module.exports={imageStylesSettings});
@@ -1,9 +0,0 @@
1
- import * as _murumets_ee_settings from '@murumets-ee/settings';
2
- import { I as ImageStyle } from './types-ChlTxvlq.cjs';
3
- import '@murumets-ee/storage';
4
-
5
- declare const imageStylesSettings: _murumets_ee_settings.SettingsDefinition<{
6
- readonly imageStyles: _murumets_ee_settings.JsonSettingConfig<Record<string, ImageStyle>>;
7
- }>;
8
-
9
- export { imageStylesSettings };
@@ -1,2 +0,0 @@
1
- "use client";
2
- "use strict";var j=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var re=Object.getOwnPropertyNames;var ie=Object.prototype.hasOwnProperty;var le=(m,l)=>{for(var y in l)j(m,y,{get:l[y],enumerable:!0})},ne=(m,l,y,d)=>{if(l&&typeof l=="object"||typeof l=="function")for(let s of re(l))!ie.call(m,s)&&s!==y&&j(m,s,{get:()=>l[s],enumerable:!(d=se(l,s))||d.enumerable});return m};var oe=m=>ne(j({},"__esModule",{value:!0}),m);var ge={};le(ge,{ImageStylesManager:()=>W});module.exports=oe(ge);var t=require("@murumets-ee/ui"),o=require("lucide-react"),i=require("react"),e=require("react/jsx-runtime"),me=["cover","contain","inside","outside","fill"],de=["webp","jpeg","png","avif"],ce={title:"Image Styles",description:'Configure image processing presets. Variants are generated on upload for each style. Use "Regenerate" to update existing images.',addStyle:"Add Style",editStyle:"Edit Style",styleName:"Style Name",width:"Width",height:"Height",fit:"Fit",format:"Format",quality:"Quality",save:"Save",cancel:"Cancel",delete:"Delete",deleteConfirmTitle:"Delete Image Style",deleteConfirmDescription:"Are you sure? This will not remove existing variant files.",systemBadge:"system",noStyles:"No image styles configured.",regenerate:"Regenerate All Variants",regenerateDescription:"Reprocess all images with the current styles. This may take a while for large libraries.",regenerating:"Regenerating...",saving:"Saving...",px:"px"};function W({initialStyles:m,apiBasePath:l="/api/admin/media",labels:y,classNames:d}){let s={...ce,...y},[u,J]=(0,i.useState)(m),[v,q]=(0,i.useState)(!1),[A,C]=(0,i.useState)(null),[U,h]=(0,i.useState)(!1),[c,M]=(0,i.useState)(null),[T,D]=(0,i.useState)(""),[b,I]=(0,i.useState)(""),[N,F]=(0,i.useState)(""),[k,R]=(0,i.useState)("cover"),[H,O]=(0,i.useState)("webp"),[E,B]=(0,i.useState)("80"),[z,g]=(0,i.useState)(null),[V,x]=(0,i.useState)(!1),[f,P]=(0,i.useState)(null),[_,K]=(0,i.useState)(!1),[p,Q]=(0,i.useState)(null),S=(0,i.useCallback)(async a=>{q(!0),C(null);try{let r=await fetch(`${l}/settings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({imageStyles:a})});if(!r.ok){let n=await r.json();throw new Error(n.error??`Save failed (${r.status})`)}J(a)}catch(r){let n=r instanceof Error?r.message:"Save failed";throw C(n),r}finally{q(!1)}},[l]),G=(0,i.useCallback)(()=>{M(null),D(""),I(""),F(""),R("cover"),O("webp"),B("80"),g(null),h(!0)},[]),X=(0,i.useCallback)(a=>{let r=u[a];r&&(M(a),D(a),I(r.width?.toString()??""),F(r.height?.toString()??""),R(r.fit??"cover"),O(r.format??"webp"),B((r.quality??80).toString()),g(null),h(!0))},[u]),Y=(0,i.useCallback)(async()=>{g(null);let a=T.trim().toLowerCase();if(!a||!/^[a-z][a-z0-9_-]*$/.test(a)){g("Name must be lowercase alphanumeric (a-z, 0-9, -, _)");return}if(a!==c&&u[a]){g(`A style named "${a}" already exists`);return}let r=b?Number.parseInt(b,10):void 0,n=N?Number.parseInt(N,10):void 0;if(!r&&!n){g("At least width or height is required");return}if(r!==void 0&&(Number.isNaN(r)||r<=0)){g("Width must be a positive number");return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){g("Height must be a positive number");return}let w=Number.parseInt(E,10);if(Number.isNaN(w)||w<1||w>100){g("Quality must be 1-100");return}let ae={...r?{width:r}:{},...n?{height:n}:{},fit:k,format:H,quality:w},$={...u};c&&c!==a&&delete $[c],$[a]=ae;try{await S($),h(!1)}catch{}},[u,c,T,b,N,k,H,E,S]),Z=(0,i.useCallback)(a=>{P(a),x(!0)},[]),ee=(0,i.useCallback)(async()=>{if(!f)return;let a={...u};delete a[f];try{await S(a),x(!1),P(null)}catch{}},[f,u,S]),te=(0,i.useCallback)(async()=>{K(!0),Q(null);try{let a=await fetch(`${l}/regenerate-variants`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});if(!a.ok){let n=await a.json();throw new Error(n.error??`Regeneration failed (${a.status})`)}let r=await a.json();Q(r)}catch(a){let r=a instanceof Error?a.message:"Regeneration failed";C(r)}finally{K(!1)}},[l]),L=Object.entries(u).sort(([a],[r])=>a.localeCompare(r));return(0,e.jsxs)("div",{className:(0,t.cn)("space-y-6",d?.root),children:[(0,e.jsxs)("div",{className:(0,t.cn)("flex items-start justify-between",d?.header),children:[(0,e.jsxs)("div",{children:[(0,e.jsx)("h2",{className:"text-2xl font-semibold tracking-tight",children:s.title}),(0,e.jsx)("p",{className:"text-sm text-muted-foreground mt-1",children:s.description})]}),(0,e.jsxs)(t.Button,{onClick:G,size:"sm",children:[(0,e.jsx)(o.Plus,{className:"h-4 w-4 mr-1"}),s.addStyle]})]}),A&&(0,e.jsx)("div",{className:"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive",children:A}),L.length===0?(0,e.jsx)("p",{className:"text-sm text-muted-foreground py-8 text-center",children:s.noStyles}):(0,e.jsx)("div",{className:(0,t.cn)("rounded-md border",d?.table),children:(0,e.jsxs)(t.Table,{children:[(0,e.jsx)(t.TableHeader,{children:(0,e.jsxs)(t.TableRow,{children:[(0,e.jsx)(t.TableHead,{children:s.styleName}),(0,e.jsx)(t.TableHead,{className:"text-center",children:s.width}),(0,e.jsx)(t.TableHead,{className:"text-center",children:s.height}),(0,e.jsx)(t.TableHead,{className:"text-center",children:s.fit}),(0,e.jsx)(t.TableHead,{className:"text-center",children:s.format}),(0,e.jsx)(t.TableHead,{className:"text-center",children:s.quality}),(0,e.jsx)(t.TableHead,{className:"w-24"})]})}),(0,e.jsx)(t.TableBody,{children:L.map(([a,r])=>(0,e.jsxs)(t.TableRow,{children:[(0,e.jsxs)(t.TableCell,{className:"font-mono text-sm",children:[a,a==="thumbnail"&&(0,e.jsx)(t.Badge,{variant:"secondary",className:"ml-2 text-xs",children:s.systemBadge})]}),(0,e.jsx)(t.TableCell,{className:"text-center tabular-nums",children:r.width?`${r.width}${s.px}`:"\u2014"}),(0,e.jsx)(t.TableCell,{className:"text-center tabular-nums",children:r.height?`${r.height}${s.px}`:"\u2014"}),(0,e.jsx)(t.TableCell,{className:"text-center text-sm text-muted-foreground",children:r.fit??"cover"}),(0,e.jsx)(t.TableCell,{className:"text-center text-sm text-muted-foreground",children:r.format??"webp"}),(0,e.jsx)(t.TableCell,{className:"text-center tabular-nums",children:r.quality??80}),(0,e.jsx)(t.TableCell,{children:(0,e.jsxs)("div",{className:(0,t.cn)("flex items-center gap-1 justify-end",d?.actions),children:[(0,e.jsx)(t.Button,{variant:"ghost",size:"sm",onClick:()=>X(a),title:s.editStyle,children:(0,e.jsx)(o.Pencil,{className:"h-3.5 w-3.5"})}),(0,e.jsx)(t.Button,{variant:"ghost",size:"sm",onClick:()=>Z(a),title:s.delete,className:"text-destructive hover:text-destructive",children:(0,e.jsx)(o.Trash2,{className:"h-3.5 w-3.5"})})]})})]},a))})]})}),(0,e.jsxs)("div",{className:(0,t.cn)("rounded-md border p-4 space-y-3",d?.regenerateSection),children:[(0,e.jsxs)("div",{className:"flex items-center justify-between",children:[(0,e.jsxs)("div",{children:[(0,e.jsx)("h3",{className:"text-sm font-medium",children:s.regenerate}),(0,e.jsx)("p",{className:"text-xs text-muted-foreground mt-0.5",children:s.regenerateDescription})]}),(0,e.jsx)(t.Button,{variant:"outline",size:"sm",onClick:te,disabled:_||L.length===0,children:_?(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(o.Loader2,{className:"h-4 w-4 mr-1 animate-spin"}),s.regenerating]}):(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(o.RefreshCw,{className:"h-4 w-4 mr-1"}),s.regenerate]})})]}),p&&(0,e.jsxs)("div",{className:"text-sm text-muted-foreground bg-muted/50 rounded px-3 py-2",children:["Processed ",p.processed," of ",p.total," images",p.skipped>0&&`, ${p.skipped} skipped`,p.errors>0&&(0,e.jsxs)("span",{className:"text-destructive",children:[", ",p.errors," errors"]})]})]}),(0,e.jsx)(t.Dialog,{open:U,onOpenChange:h,children:(0,e.jsxs)(t.DialogContent,{className:d?.dialog,children:[(0,e.jsxs)(t.DialogHeader,{children:[(0,e.jsx)(t.DialogTitle,{children:c?s.editStyle:s.addStyle}),(0,e.jsx)(t.DialogDescription,{children:c?`Editing "${c}" image style.`:"Create a new image processing preset."})]}),(0,e.jsxs)("div",{className:"space-y-4 py-2",children:[z&&(0,e.jsx)("div",{className:"text-sm text-destructive",children:z}),(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsx)(t.Label,{htmlFor:"style-name",children:s.styleName}),(0,e.jsx)(t.Input,{id:"style-name",value:T,onChange:a=>D(a.target.value),placeholder:"e.g. thumbnail, medium, large",disabled:!!c})]}),(0,e.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsxs)(t.Label,{htmlFor:"style-width",children:[s.width," (",s.px,")"]}),(0,e.jsx)(t.Input,{id:"style-width",type:"number",value:b,onChange:a=>I(a.target.value),placeholder:"e.g. 200",min:1})]}),(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsxs)(t.Label,{htmlFor:"style-height",children:[s.height," (",s.px,")"]}),(0,e.jsx)(t.Input,{id:"style-height",type:"number",value:N,onChange:a=>F(a.target.value),placeholder:"e.g. 200",min:1})]})]}),(0,e.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsx)(t.Label,{htmlFor:"style-fit",children:s.fit}),(0,e.jsx)(t.Select,{id:"style-fit",value:k,onChange:a=>R(a.target.value),children:me.map(a=>(0,e.jsx)("option",{value:a,children:a},a))})]}),(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsx)(t.Label,{htmlFor:"style-format",children:s.format}),(0,e.jsx)(t.Select,{id:"style-format",value:H,onChange:a=>O(a.target.value),children:de.map(a=>(0,e.jsx)("option",{value:a,children:a},a))})]})]}),(0,e.jsxs)("div",{className:"space-y-2",children:[(0,e.jsxs)(t.Label,{htmlFor:"style-quality",children:[s.quality," (1-100)"]}),(0,e.jsx)(t.Input,{id:"style-quality",type:"number",value:E,onChange:a=>B(a.target.value),min:1,max:100})]})]}),(0,e.jsxs)(t.DialogFooter,{children:[(0,e.jsx)(t.Button,{variant:"outline",onClick:()=>h(!1),children:s.cancel}),(0,e.jsx)(t.Button,{onClick:Y,disabled:v,children:v?(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(o.Loader2,{className:"h-4 w-4 mr-1 animate-spin"}),s.saving]}):s.save})]})]})}),(0,e.jsx)(t.Dialog,{open:V,onOpenChange:x,children:(0,e.jsxs)(t.DialogContent,{children:[(0,e.jsxs)(t.DialogHeader,{children:[(0,e.jsx)(t.DialogTitle,{children:s.deleteConfirmTitle}),(0,e.jsxs)(t.DialogDescription,{children:[s.deleteConfirmDescription,f&&(0,e.jsx)("span",{className:"block mt-2 font-mono text-foreground",children:f})]})]}),(0,e.jsxs)(t.DialogFooter,{children:[(0,e.jsx)(t.Button,{variant:"outline",onClick:()=>x(!1),children:s.cancel}),(0,e.jsxs)(t.Button,{variant:"destructive",onClick:ee,disabled:v,children:[v?(0,e.jsx)(o.Loader2,{className:"h-4 w-4 mr-1 animate-spin"}):null,s.delete]})]})]})})]})}0&&(module.exports={ImageStylesManager});
@@ -1,62 +0,0 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- /** Named image processing preset — defines how to resize/convert an image variant */
4
- interface ImageStyle {
5
- /** Max width in pixels (omit for height-only constraint) */
6
- width?: number;
7
- /** Max height in pixels (omit for height-only constraint) */
8
- height?: number;
9
- /** Resize strategy (default: 'cover') */
10
- fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
11
- /** Output format (default: 'webp') */
12
- format?: 'webp' | 'jpeg' | 'png' | 'avif';
13
- /** Output quality 1–100 (default: 80) */
14
- quality?: number;
15
- }
16
-
17
- interface ImageStylesManagerLabels {
18
- title?: string;
19
- description?: string;
20
- addStyle?: string;
21
- editStyle?: string;
22
- styleName?: string;
23
- width?: string;
24
- height?: string;
25
- fit?: string;
26
- format?: string;
27
- quality?: string;
28
- save?: string;
29
- cancel?: string;
30
- delete?: string;
31
- deleteConfirmTitle?: string;
32
- deleteConfirmDescription?: string;
33
- systemBadge?: string;
34
- noStyles?: string;
35
- regenerate?: string;
36
- regenerateDescription?: string;
37
- regenerating?: string;
38
- saving?: string;
39
- px?: string;
40
- }
41
- interface ImageStylesManagerClassNames {
42
- root?: string;
43
- header?: string;
44
- table?: string;
45
- dialog?: string;
46
- actions?: string;
47
- regenerateSection?: string;
48
- }
49
- interface ImageStylesManagerProps {
50
- /** Current image styles from server (required, server-first pattern) */
51
- initialStyles: Record<string, ImageStyle>;
52
- /** API base path for media admin routes (default: '/api/admin/media') */
53
- apiBasePath?: string;
54
- /** i18n labels with English defaults */
55
- labels?: ImageStylesManagerLabels;
56
- /** Per-element class overrides */
57
- classNames?: ImageStylesManagerClassNames;
58
- }
59
-
60
- declare function ImageStylesManager({ initialStyles, apiBasePath, labels: userLabels, classNames, }: ImageStylesManagerProps): react_jsx_runtime.JSX.Element;
61
-
62
- export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps };
package/dist/index.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var _=Object.create;var M=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var p=(i,e)=>()=>(i&&(e=i(i=0)),e);var S=(i,e)=>{for(var t in e)M(i,t,{get:e[t],enumerable:!0})},B=(i,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Y(e))!H.call(i,r)&&r!==t&&M(i,r,{get:()=>e[r],enumerable:!(a=J(e,r))||a.enumerable});return i};var c=(i,e,t)=>(t=i!=null?_(G(i)):{},B(e||!i||!i.__esModule?M(t,"default",{value:i,enumerable:!0}):t,i)),Q=i=>B(M({},"__esModule",{value:!0}),i);var u,w,v=p(()=>{"use strict";u=require("@murumets-ee/entity"),w=(0,u.defineEntity)({name:"media",fields:{title:u.field.text({translatable:!0}),alt:u.field.text({translatable:!0}),description:u.field.text({translatable:!0}),fileKey:u.field.text({required:!0,indexed:!0}),filename:u.field.text({required:!0}),mimeType:u.field.text({required:!0,indexed:!0}),size:u.field.number({required:!0,integer:!0}),width:u.field.number({integer:!0}),height:u.field.number({integer:!0}),mediaType:u.field.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[u.behavior.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}})});function E(i){return i.startsWith("image/")&&!X.has(i)}async function O(i,e){let t=await(0,I.default)(i).metadata(),a=t.width??0,r=t.height??0,l=new Map,s=Object.entries(e);return await Promise.all(s.map(async([g,o])=>{let d=o.format??"webp",n=o.quality??80,f=o.fit??"cover",m=(0,I.default)(i).resize({width:o.width,height:o.height,fit:f,withoutEnlargement:!0}),{data:y,info:h}=await m[d]({quality:n}).toBuffer({resolveWithObject:!0});l.set(g,{buffer:y,format:d,mimeType:`image/${d}`,width:h.width,height:h.height})})),{width:a,height:r,variants:l}}var I,X,j=p(()=>{"use strict";I=c(require("sharp"),1),X=new Set(["image/svg+xml","image/gif"])});function P(i,e,t="webp"){let a=i.lastIndexOf("/"),r=i.substring(0,a),s=i.substring(a+1).replace(/\.[^.]+$/,"");return`${r}/${e}_${s}.${t}`}var q=p(()=>{"use strict"});var T={};S(T,{imageStylesSettings:()=>C});var R,C,k=p(()=>{"use strict";R=require("@murumets-ee/settings"),C=(0,R.defineSettings)({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:R.setting.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}})});var z={};S(z,{getMediaConfig:()=>Z,media:()=>N});function Z(){if(!U)throw new Error("@murumets-ee/media plugin not initialized. Add media() to your plugins array.");return U}function N(i){let e={acceptedTypes:i?.acceptedTypes??["image/*","video/*","audio/*","application/pdf"],maxUploadSize:i?.maxUploadSize??52428800,defaultVisibility:i?.defaultVisibility??"public",imageStyles:i?.imageStyles??{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}}};return{name:"@murumets-ee/media",entities:[w],init:async t=>{if(!t.plugins.has("@murumets-ee/storage"))throw new Error("@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.");if(!t.plugins.has("@murumets-ee/settings"))throw new Error("@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.");U=e;try{let{createSettingsClient:a}=await import("@murumets-ee/settings"),{imageStylesSettings:r}=await Promise.resolve().then(()=>(k(),T)),l=a(r,{app:t});await l.has("imageStyles")||(await l.set("imageStyles",e.imageStyles),t.logger.info({styles:Object.keys(e.imageStyles)},"Seeded default image styles to settings DB"))}catch(a){t.logger.warn({error:a},"Failed to seed image styles to settings DB (non-fatal)")}t.logger.info({acceptedTypes:e.acceptedTypes,maxUploadSize:e.maxUploadSize,defaultVisibility:e.defaultVisibility},"Media plugin initialized")}}}var U,F=p(()=>{"use strict";v();U=null});var V={};S(V,{MediaClient:()=>b,createMediaClient:()=>ie,getMediaClient:()=>ae});function ee(i){return i.startsWith("image/")?"image":i.startsWith("video/")?"video":i.startsWith("audio/")?"audio":i==="application/pdf"||i.startsWith("application/msword")||i.startsWith("application/vnd.")?"document":"other"}function te(i){return i.replace(/\.[^.]+$/,"").replace(/[-_]/g," ")}async function ie(i){let{getApp:e}=await import("@murumets-ee/core"),t=e();return new b({db:t.db.readWrite,storage:i,logger:t.logger.child({media:!0})})}function ae(){return A||(A=(async()=>{let{getApp:i}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),a=i(),r=t(),l=e(r,{app:a});return new b({db:a.db.readWrite,storage:l,logger:a.logger.child({media:!0})})})()),A}var me,b,A,$=p(()=>{"use strict";me=require("server-only");v();j();q();b=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:e}=await import("@murumets-ee/entity/admin");this.admin=new e({entity:w,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import("@murumets-ee/settings"),{imageStylesSettings:t}=await Promise.resolve().then(()=>(k(),T)),{getApp:a}=await import("@murumets-ee/core"),r=a(),s=await e(t,{app:r}).get("imageStyles");if(s&&Object.keys(s).length>0)return this.imageStyles=s,s}catch{}try{let{getMediaConfig:e}=await Promise.resolve().then(()=>(F(),z)),t=e();return this.imageStyles=t.imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let a=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),r=t.width??null,l=t.height??null,s={};if(e instanceof Buffer&&E(t.mimeType))try{let d=await this.resolveImageStyles(),n=await O(e,d);r=n.width,l=n.height;let f=a.visibility;await Promise.all([...n.variants.entries()].map(async([m,y])=>{let h=P(a.key,m,y.format);try{await this.storage.upload(y.buffer,{key:h,filename:`${m}_${t.filename}`,mimeType:y.mimeType,size:y.buffer.byteLength,visibility:f,metadata:{variantOf:a.key,style:m},uploadedBy:t.uploadedBy}),s[m]=h,this.logger?.debug({style:m,key:h,width:y.width,height:y.height},"Variant uploaded")}catch(x){this.logger?.warn({style:m,key:h,error:x},"Failed to upload variant (non-fatal)")}})),Object.keys(s).length>0&&await this.storage.updateMetadata(a.key,{metadata:{...a.metadata??{},variants:s}}).catch(m=>{this.logger?.warn({key:a.key,error:m},"Failed to update original file metadata with variant keys (non-fatal)")}),this.logger?.info({width:r,height:l,variants:Object.keys(s)},"Image processed")}catch(d){this.logger?.warn({filename:t.filename,error:d},"Image processing failed (non-fatal, original saved)")}let g=ee(t.mimeType),o=await this.getAdmin();try{let d=await o.create({title:t.title??te(t.filename),alt:t.alt??null,description:t.description??null,fileKey:a.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:r,height:l,mediaType:g}),n=await this.storage.getUrl(a.key);return this.logger?.info({id:d.id,fileKey:a.key,mediaType:g},"Media uploaded"),{media:d,url:n}}catch(d){this.logger?.error({fileKey:a.key,error:d},"Media entity creation failed, rolling back storage uploads");for(let n of Object.values(s))await this.storage.delete(n).catch(()=>{});throw await this.storage.delete(a.key).catch(n=>{this.logger?.error({fileKey:a.key,error:n},"Storage rollback also failed")}),d}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{and:r,asc:l,desc:s,eq:g,ilike:o,sql:d}=await import("drizzle-orm"),n=a.get("media");if(!n)throw new Error("Media schema not registered. Is the media() plugin loaded?");let f=[];if(e?.mediaType&&f.push(g(n.mediaType,e.mediaType)),e?.mimeTypePrefix&&f.push(o(n.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let K=`%${e.search}%`;f.push(d`(${o(n.filename,K)} OR ${n.fields} ->> 'title' ILIKE ${K})`)}let m=e?.limit??50,y=e?.offset??0,h=f.length>0?r(...f):void 0,[x]=await this.db.select({count:d`count(*)::int`}).from(n).where(h),W=e?.orderBy==="filename"?n.filename:n.createdAt,D=(e?.orderDirection??"desc")==="asc"?l:s;return{items:await t.findMany({where:h,limit:m,offset:y,orderBy:D(W)}),total:x?.count??0,limit:m,offset:y}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),a=await t.findById(e);if(!a)throw new Error(`Media not found: ${e}`);let r=a.fileKey;await t.delete(e);let s=(await this.storage.getMetadata(r))?.metadata?.variants;if(s)for(let g of Object.values(s))await this.storage.delete(g).catch(o=>{this.logger?.warn({variantKey:g,error:o},"Failed to delete variant file")});await this.storage.delete(r).catch(g=>{this.logger?.error({id:e,fileKey:r,error:g},"Failed to delete file from storage after entity deletion")}),this.logger?.info({id:e,fileKey:r,deletedVariants:s?Object.keys(s).length:0},"Media deleted")}async getUrl(e){let a=await(await this.getAdmin()).findById(e);if(!a)throw new Error(`Media not found: ${e}`);return this.storage.getUrl(a.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{inArray:r}=await import("drizzle-orm"),l=a.get("media");if(!l)return new Map;let s=await t.findMany({where:r(l.id,e),limit:e.length}),g=new Map;return await Promise.all(s.map(async o=>{let d=await this.storage.getUrl(o.fileKey);g.set(o.id,d)})),g}async getVariantUrl(e,t){let r=await(await this.getAdmin()).findById(e);if(!r)return null;let l=r.fileKey,g=(await this.resolveImageStyles())[t];if(g){let o=P(l,t,g.format??"webp");try{return await this.storage.getUrl(o)}catch{}}try{return await this.storage.getUrl(l)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let a=await this.getAdmin(),{schemaRegistry:r}=await import("@murumets-ee/db"),{inArray:l}=await import("drizzle-orm"),s=r.get("media");if(!s)return new Map;let g=await a.findMany({where:l(s.id,e),limit:e.length}),d=(await this.resolveImageStyles())[t],n=new Map;return await Promise.all(g.map(async f=>{if(d){let m=P(f.fileKey,t,d.format??"webp");try{let y=await this.storage.getUrl(m);n.set(f.id,y);return}catch{}}try{let m=await this.storage.getUrl(f.fileKey);n.set(f.id,m)}catch{}})),n}};A=null});var re={};S(re,{Media:()=>w,enrichWithMediaUrls:()=>L,imageStylesSettings:()=>C});module.exports=Q(re);var ye=require("server-only");async function L(i,e,t="thumbnail"){let a=Object.entries(i.allFields).filter(([,o])=>o.type==="media").map(([o])=>o);if(a.length===0)return;let r=new Set;for(let o of e)for(let d of a){let n=o[d];typeof n=="string"&&n.length>0&&r.add(n)}if(r.size===0)return;let{getMediaClient:l}=await Promise.resolve().then(()=>($(),V)),g=await(await l()).getVariantUrls([...r],t);for(let o of e)for(let d of a){let n=o[d];typeof n=="string"&&g.has(n)&&(o[`${d}Url`]=g.get(n))}}v();k();0&&(module.exports={Media,enrichWithMediaUrls,imageStylesSettings});
package/dist/index.d.cts DELETED
@@ -1,131 +0,0 @@
1
- import * as _murumets_ee_entity from '@murumets-ee/entity';
2
- export { imageStylesSettings } from './image-styles-settings.cjs';
3
- export { I as ImageStyle, c as MediaListOptions, d as MediaListResult, e as MediaPluginConfig, b as MediaRecord, f as MediaType, M as MediaUploadOptions, a as MediaUploadResult } from './types-ChlTxvlq.cjs';
4
- import '@murumets-ee/settings';
5
- import '@murumets-ee/storage';
6
-
7
- /**
8
- * Server-side utility to enrich entity list items with resolved media URLs.
9
- *
10
- * For entities with `field.media()` columns, this scans items for media UUIDs,
11
- * batch-resolves them via MediaClient, and injects `${fieldName}Url` into each item.
12
- *
13
- * Convention: a media field named `coverImage` (UUID) gets enriched with
14
- * `coverImageUrl` (resolved URL string).
15
- *
16
- * @example
17
- * ```typescript
18
- * import { enrichWithMediaUrls } from '@murumets-ee/media'
19
- *
20
- * const items = await adminClient.findMany({ limit: 20 })
21
- * await enrichWithMediaUrls(Article, items)
22
- * // items[0].coverImageUrl → 'https://cdn.example.com/uploads/.../thumbnail_photo.webp'
23
- * ```
24
- */
25
-
26
- /**
27
- * Enrich entity list items by resolving media field UUIDs to variant URLs.
28
- *
29
- * Mutates items in place — injects `${fieldName}Url` for each media field.
30
- *
31
- * @param entity - Entity definition (or any object with allFields containing type info)
32
- * @param items - Array of entity records to enrich
33
- * @param styleName - Image style to resolve (default: 'thumbnail')
34
- */
35
- declare function enrichWithMediaUrls(entity: {
36
- allFields: Record<string, {
37
- type: string;
38
- }>;
39
- }, items: Record<string, unknown>[], styleName?: string): Promise<void>;
40
-
41
- /**
42
- * Pre-defined Media entity.
43
- *
44
- * Auto-registered by the media() plugin — users do NOT add this to their entities array.
45
- * Connected to toolkit_files via the fileKey field (stores the StorageClient file key).
46
- *
47
- * @example
48
- * ```typescript
49
- * import { media } from '@murumets-ee/media/plugin'
50
- *
51
- * export default defineConfig({
52
- * plugins: [storage(), media()],
53
- * // Media entity is auto-added — no need to list it here
54
- * })
55
- * ```
56
- */
57
- declare const Media: _murumets_ee_entity.Entity<{
58
- id: _murumets_ee_entity.IdField;
59
- } & _murumets_ee_entity.AuditableFields & {
60
- title: _murumets_ee_entity.TextField & {
61
- readonly translatable: true;
62
- };
63
- alt: _murumets_ee_entity.TextField & {
64
- readonly translatable: true;
65
- };
66
- description: _murumets_ee_entity.TextField & {
67
- readonly translatable: true;
68
- };
69
- /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */
70
- fileKey: _murumets_ee_entity.TextField & {
71
- readonly required: true;
72
- readonly indexed: true;
73
- };
74
- filename: _murumets_ee_entity.TextField & {
75
- readonly required: true;
76
- };
77
- mimeType: _murumets_ee_entity.TextField & {
78
- readonly required: true;
79
- readonly indexed: true;
80
- };
81
- size: _murumets_ee_entity.NumberField & {
82
- readonly required: true;
83
- readonly integer: true;
84
- };
85
- width: _murumets_ee_entity.NumberField & {
86
- readonly integer: true;
87
- };
88
- height: _murumets_ee_entity.NumberField & {
89
- readonly integer: true;
90
- };
91
- mediaType: _murumets_ee_entity.SelectField & {
92
- options: readonly ["image", "video", "audio", "document", "other"];
93
- } & {
94
- readonly options: readonly ["image", "video", "audio", "document", "other"];
95
- readonly required: true;
96
- readonly indexed: true;
97
- };
98
- }>;
99
-
100
- /** Lightweight media item for the picker (no server-only deps) */
101
- interface MediaPickerItem {
102
- id: string;
103
- title: string | null;
104
- alt: string | null;
105
- filename: string;
106
- mimeType: string;
107
- size: number;
108
- width: number | null;
109
- height: number | null;
110
- mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
111
- url: string;
112
- }
113
- /** Paginated result from fetchMedia callback */
114
- interface MediaPickerListResult {
115
- items: MediaPickerItem[];
116
- total: number;
117
- }
118
- /** Callbacks the consumer provides — framework-agnostic */
119
- interface MediaPickerCallbacks {
120
- /** Fetch media items with filtering and pagination */
121
- fetchMedia: (options: {
122
- search?: string;
123
- mediaType?: string;
124
- limit: number;
125
- offset: number;
126
- }) => Promise<MediaPickerListResult>;
127
- /** Upload a file and return the created media item */
128
- uploadMedia: (file: File) => Promise<MediaPickerItem>;
129
- }
130
-
131
- export { Media, type MediaPickerCallbacks, type MediaPickerItem, type MediaPickerListResult, enrichWithMediaUrls };
package/dist/picker.cjs DELETED
@@ -1,2 +0,0 @@
1
- "use client";
2
- "use strict";var ke=Object.create;var D=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var ve=Object.getOwnPropertyNames;var Pe=Object.getPrototypeOf,xe=Object.prototype.hasOwnProperty;var he=(e,i)=>{for(var t in i)D(e,t,{get:i[t],enumerable:!0})},Q=(e,i,t,r)=>{if(i&&typeof i=="object"||typeof i=="function")for(let a of ve(i))!xe.call(e,a)&&a!==t&&D(e,a,{get:()=>i[a],enumerable:!(r=ye(i,a))||r.enumerable});return e};var Y=(e,i,t)=>(t=e!=null?ke(Pe(e)):{},Q(i||!e||!e.__esModule?D(t,"default",{value:e,enumerable:!0}):t,e)),Me=e=>Q(D({},"__esModule",{value:!0}),e);var we={};he(we,{MediaCard:()=>I,MediaGrid:()=>T,MediaPicker:()=>ce,MediaPickerProvider:()=>ae,SearchBar:()=>R,UploadZone:()=>U,createAdminMediaCallbacks:()=>ee,useMediaPicker:()=>S});module.exports=Me(we);function ee(e="/api/admin"){let i=`${e}/media`;return{fetchMedia:async t=>{let r=new URLSearchParams;t.search&&r.set("search",t.search),t.mediaType&&r.set("mediaType",t.mediaType),r.set("limit",String(t.limit)),r.set("offset",String(t.offset));let a=`${i}?${r.toString()}`,l=await fetch(a);if(!l.ok)throw new Error(`Failed to fetch media: ${l.status}`);return l.json()},uploadMedia:async t=>{let r=new FormData;r.append("file",t);let a=await fetch(i,{method:"POST",body:r});if(!a.ok)throw new Error(`Upload failed: ${a.status}`);return a.json()},getMediaUrl:async t=>{try{let r=await fetch(`${i}/${t}`);return r.ok?(await r.json()).url??null:null}catch{return null}}}}var k=require("lucide-react");var te=require("clsx"),ie=require("tailwind-merge");function o(...e){return(0,ie.twMerge)((0,te.clsx)(e))}var y=require("react/jsx-runtime"),ze={video:k.Film,audio:k.Music,document:k.FileText,other:k.File};function I({item:e,isSelected:i,onToggle:t,classNames:r}){let a=e.mediaType==="image",l=a?null:ze[e.mediaType]??k.File;return(0,y.jsxs)("button",{type:"button",onClick:t,className:o("group relative aspect-square overflow-hidden rounded-lg border-2 transition-all","focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950",i?"border-blue-500 ring-2 ring-blue-500/20":"border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700",r?.card,i&&r?.cardSelected),children:[a?(0,y.jsx)("img",{src:e.url,alt:e.alt??e.filename,className:o("h-full w-full object-cover",r?.cardImage),loading:"lazy"}):(0,y.jsx)("div",{className:"flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800",children:l&&(0,y.jsx)(l,{className:"h-8 w-8 text-zinc-400 dark:text-zinc-500"})}),i&&(0,y.jsx)("div",{className:"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white",children:(0,y.jsx)(k.Check,{className:"h-3 w-3"})}),(0,y.jsx)("div",{className:o("absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5","opacity-0 transition-opacity group-hover:opacity-100",r?.cardLabel),children:(0,y.jsx)("p",{className:"truncate text-xs text-white",children:e.title??e.filename})})]})}var f=require("react/jsx-runtime");function T({items:e,selected:i,onToggle:t,isLoading:r,total:a,offset:l,limit:p,onPageChange:c,classNames:v}){if(r)return(0,f.jsx)("div",{className:o("grid grid-cols-4 gap-3 sm:grid-cols-6",v?.grid),children:Array.from({length:12}).map((h,L)=>(0,f.jsx)("div",{className:o("aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800",v?.loading)},`skeleton-${L.toString()}`))});if(e.length===0)return(0,f.jsx)("div",{className:o("py-12 text-center text-sm text-zinc-500 dark:text-zinc-400",v?.empty),children:"No media found. Upload a file to get started."});let m=Math.ceil(a/p),s=Math.floor(l/p)+1;return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsx)("div",{className:o("grid grid-cols-4 gap-3 sm:grid-cols-6",v?.grid),children:e.map(h=>(0,f.jsx)(I,{item:h,isSelected:i.has(h.id),onToggle:()=>t(h),classNames:v},h.id))}),m>1&&(0,f.jsxs)("div",{className:"mt-4 flex items-center justify-center gap-2",children:[(0,f.jsx)("button",{type:"button",disabled:s<=1,onClick:()=>c(l-p),className:"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700",children:"Prev"}),(0,f.jsxs)("span",{className:"text-sm text-zinc-500 dark:text-zinc-400",children:[s," / ",m]}),(0,f.jsx)("button",{type:"button",disabled:s>=m,onClick:()=>c(l+p),className:"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700",children:"Next"})]})]})}var u=Y(require("@radix-ui/react-dialog"),1),B=Y(require("@radix-ui/react-visually-hidden"),1),le=require("lucide-react"),d=require("react");var C=require("react"),oe=require("react/jsx-runtime"),re=(0,C.createContext)(null);function ae({children:e,fetchMedia:i,uploadMedia:t}){let r=(0,C.useMemo)(()=>({fetchMedia:i,uploadMedia:t}),[i,t]);return(0,oe.jsx)(re,{value:r,children:e})}function S(){let e=(0,C.useContext)(re);if(!e)throw new Error("useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.");return e}var ne=require("lucide-react");var x=require("react/jsx-runtime"),Ce=[{value:void 0,label:"All"},{value:"image",label:"Images"},{value:"video",label:"Videos"},{value:"audio",label:"Audio"},{value:"document",label:"Docs"}];function R({value:e,onChange:i,mediaType:t,onMediaTypeChange:r,locked:a,classNames:l}){let p=!a;return(0,x.jsxs)("div",{className:"flex items-center gap-3",children:[(0,x.jsxs)("div",{className:"relative flex-1",children:[(0,x.jsx)(ne.Search,{className:"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"}),(0,x.jsx)("input",{type:"text",value:e,onChange:c=>i(c.target.value),placeholder:"Search media...",className:o("w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm","placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500","dark:border-zinc-700 dark:placeholder:text-zinc-500",l?.searchInput)})]}),p&&(0,x.jsx)("div",{className:o("flex gap-1",l?.filterTabs),children:Ce.map(c=>(0,x.jsx)("button",{type:"button",onClick:()=>r(c.value),className:o("rounded-md px-3 py-1.5 text-xs font-medium transition-colors",t===c.value?"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900":"text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800"),children:c.label},c.label))})]})}var se=require("lucide-react"),z=require("react");var w=require("react/jsx-runtime");function U({onUpload:e,isUploading:i,accept:t,classNames:r}){let a=(0,z.useRef)(null),[l,p]=(0,z.useState)(!1),c=(0,z.useCallback)(async m=>{m.preventDefault(),p(!1);let s=m.dataTransfer.files[0];s&&await e(s)},[e]),v=(0,z.useCallback)(async m=>{let s=m.target.files?.[0];s&&(await e(s),m.target.value="")},[e]);return(0,w.jsxs)("button",{type:"button",onDragOver:m=>{m.preventDefault(),p(!0)},onDragLeave:()=>p(!1),onDrop:c,onClick:()=>a.current?.click(),className:o("mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors",l?"border-blue-500 bg-blue-50 dark:bg-blue-950/20":"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600",r?.uploadZone,l&&r?.uploadZoneActive),children:[(0,w.jsx)(se.Upload,{className:"mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500"}),(0,w.jsx)("p",{className:"text-sm text-zinc-600 dark:text-zinc-400",children:i?"Uploading...":"Drop a file here or click to upload"}),(0,w.jsx)("input",{ref:a,type:"file",accept:t?.join(","),onChange:v,className:"hidden"})]})}var n=require("react/jsx-runtime"),de=24;function ce({open:e,onOpenChange:i,onSelect:t,mode:r="single",accept:a,mediaType:l,maxSelect:p,selectedIds:c=[],title:v="Select Media",description:m,classNames:s,className:h,children:L}){let{fetchMedia:H,uploadMedia:Z}=S(),[$,G]=(0,d.useState)([]),[N,j]=(0,d.useState)(0),[M,E]=(0,d.useState)(()=>new Set(c)),[F,_]=(0,d.useState)(""),[A,me]=(0,d.useState)(l),[ue,q]=(0,d.useState)(!1),[pe,O]=(0,d.useState)(!1),[V,W]=(0,d.useState)(0),X=(0,d.useCallback)(async()=>{q(!0);try{let b=await H({search:F||void 0,mediaType:A,limit:de,offset:V});G(b.items),j(b.total)}catch{}finally{q(!1)}},[H,F,A,V]);(0,d.useEffect)(()=>{e&&X()},[e,X]);let J=(0,d.useRef)(e),K=(0,d.useRef)(c);K.current=c,(0,d.useEffect)(()=>{J.current&&!e&&(_(""),W(0),E(new Set(K.current))),J.current=e},[e]);let fe=(0,d.useCallback)(b=>{if(r==="single"){t([b]),i(!1);return}E(P=>{let g=new Set(P);if(g.has(b.id))g.delete(b.id);else{if(p&&g.size>=p)return P;g.add(b.id)}return g})},[r,p,t,i]),be=(0,d.useCallback)(()=>{let b=$.filter(P=>M.has(P.id));t(b),i(!1)},[$,M,t,i]),ge=(0,d.useCallback)(async b=>{O(!0);try{let P=await Z(b);if(r==="single"){t([P]),i(!1);return}G(g=>[P,...g]),j(g=>g+1),E(g=>new Set([...g,P.id]))}finally{O(!1)}},[Z,r,t,i]);return(0,n.jsx)(u.Root,{open:e,onOpenChange:i,children:(0,n.jsxs)(u.Portal,{children:[(0,n.jsx)(u.Overlay,{className:o("fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",s?.overlay)}),(0,n.jsxs)(u.Content,{...!m&&{"aria-describedby":void 0},className:o("fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2","flex max-h-[85vh] w-[90vw] max-w-4xl flex-col","rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950",s?.content,h),children:[(0,n.jsxs)("div",{className:o("flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800",s?.header),children:[(0,n.jsx)(u.Title,{className:o("text-lg font-semibold text-zinc-900 dark:text-zinc-50",s?.title),children:v}),m&&(0,n.jsx)(B.Root,{asChild:!0,children:(0,n.jsx)(u.Description,{children:m})}),(0,n.jsxs)(u.Close,{className:"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600",children:[(0,n.jsx)(le.X,{className:"h-4 w-4"}),(0,n.jsx)(B.Root,{children:"Close"})]})]}),(0,n.jsx)("div",{className:o("border-b border-zinc-200 px-6 py-3 dark:border-zinc-800",s?.toolbar),children:(0,n.jsx)(R,{value:F,onChange:_,mediaType:A,onMediaTypeChange:me,locked:!!l,classNames:s})}),(0,n.jsxs)("div",{className:"flex-1 overflow-y-auto px-6 py-4",children:[(0,n.jsx)(U,{onUpload:ge,isUploading:pe,accept:a,classNames:s}),(0,n.jsx)(T,{items:$,selected:M,onToggle:fe,isLoading:ue,total:N,offset:V,limit:de,onPageChange:W,classNames:s})]}),(0,n.jsxs)("div",{className:o("flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800",s?.footer),children:[(0,n.jsx)("span",{className:"text-sm text-zinc-500 dark:text-zinc-400",children:r==="single"?`${N.toString()} item${N!==1?"s":""} \u2014 click to select`:M.size>0?`${M.size.toString()} selected`:`${N.toString()} item${N!==1?"s":""}`}),(0,n.jsxs)("div",{className:"flex gap-2",children:[L,(0,n.jsx)(u.Close,{asChild:!0,children:(0,n.jsx)("button",{type:"button",className:o("rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50","dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900",s?.cancelButton),children:"Cancel"})}),r!=="single"&&(0,n.jsx)("button",{type:"button",onClick:be,disabled:M.size===0,className:o("rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500","disabled:cursor-not-allowed disabled:opacity-50",s?.confirmButton),children:`Select (${M.size.toString()})`})]})]})]})]})})}0&&(module.exports={MediaCard,MediaGrid,MediaPicker,MediaPickerProvider,SearchBar,UploadZone,createAdminMediaCallbacks,useMediaPicker});
package/dist/picker.d.cts DELETED
@@ -1,183 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import * as react_jsx_runtime from 'react/jsx-runtime';
3
-
4
- /** Lightweight media item for the picker (no server-only deps) */
5
- interface MediaPickerItem {
6
- id: string;
7
- title: string | null;
8
- alt: string | null;
9
- filename: string;
10
- mimeType: string;
11
- size: number;
12
- width: number | null;
13
- height: number | null;
14
- mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
15
- url: string;
16
- }
17
- /** Paginated result from fetchMedia callback */
18
- interface MediaPickerListResult {
19
- items: MediaPickerItem[];
20
- total: number;
21
- }
22
- /** Callbacks the consumer provides — framework-agnostic */
23
- interface MediaPickerCallbacks {
24
- /** Fetch media items with filtering and pagination */
25
- fetchMedia: (options: {
26
- search?: string;
27
- mediaType?: string;
28
- limit: number;
29
- offset: number;
30
- }) => Promise<MediaPickerListResult>;
31
- /** Upload a file and return the created media item */
32
- uploadMedia: (file: File) => Promise<MediaPickerItem>;
33
- }
34
- /** Selection mode */
35
- type MediaPickerMode = 'single' | 'multiple';
36
- /** Per-element class overrides for MediaPicker */
37
- interface MediaPickerClassNames {
38
- overlay?: string;
39
- content?: string;
40
- header?: string;
41
- title?: string;
42
- toolbar?: string;
43
- searchInput?: string;
44
- filterTabs?: string;
45
- grid?: string;
46
- card?: string;
47
- cardSelected?: string;
48
- cardImage?: string;
49
- cardLabel?: string;
50
- uploadZone?: string;
51
- uploadZoneActive?: string;
52
- footer?: string;
53
- confirmButton?: string;
54
- cancelButton?: string;
55
- loading?: string;
56
- empty?: string;
57
- }
58
- /** Props for the MediaPicker dialog */
59
- interface MediaPickerProps {
60
- /** Whether the dialog is open */
61
- open: boolean;
62
- /** Called when open state changes */
63
- onOpenChange: (open: boolean) => void;
64
- /** Called when user confirms selection */
65
- onSelect: (items: MediaPickerItem[]) => void;
66
- /** Selection mode (default: 'single') */
67
- mode?: MediaPickerMode;
68
- /** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
69
- accept?: string[];
70
- /** Filter browse results to a specific media classification (e.g., 'image', 'video') */
71
- mediaType?: string;
72
- /** Maximum items selectable in multi mode */
73
- maxSelect?: number;
74
- /** Currently selected item IDs (for pre-selection) */
75
- selectedIds?: string[];
76
- /** Dialog title (default: 'Select Media') */
77
- title?: string;
78
- /** Dialog description for screen readers (optional) */
79
- description?: string;
80
- /** Per-element class overrides */
81
- classNames?: MediaPickerClassNames;
82
- /** Additional className for the dialog content */
83
- className?: string;
84
- /** Children rendered in dialog footer (extra actions) */
85
- children?: ReactNode;
86
- }
87
-
88
- /**
89
- * Pre-built media callbacks that talk to the admin API.
90
- *
91
- * Eliminates boilerplate in every project — just call:
92
- * const media = createAdminMediaCallbacks('/api/admin')
93
- *
94
- * Returns callbacks compatible with both MediaPickerProvider and
95
- * BlockEditor's `media` prop (with getMediaUrl for thumbnail previews).
96
- */
97
-
98
- /** Extended callbacks with getMediaUrl (for editor thumbnail previews) */
99
- type AdminMediaCallbacks = MediaPickerCallbacks & {
100
- getMediaUrl: (id: string) => Promise<string | null>;
101
- };
102
- /**
103
- * Create media callbacks that talk to the admin API.
104
- *
105
- * @param apiBasePath - Base path for admin API (default: '/api/admin')
106
- * @returns Callbacks for MediaPickerProvider + BlockEditor media prop
107
- *
108
- * @example
109
- * ```tsx
110
- * import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'
111
- *
112
- * const media = createAdminMediaCallbacks()
113
- * // Use with BlockEditor:
114
- * <BlockEditor media={media} ... />
115
- * // Use with MediaPickerProvider:
116
- * <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>
117
- * ```
118
- */
119
- declare function createAdminMediaCallbacks(apiBasePath?: string): AdminMediaCallbacks;
120
-
121
- interface MediaCardProps {
122
- item: MediaPickerItem;
123
- isSelected: boolean;
124
- onToggle: () => void;
125
- classNames?: MediaPickerClassNames;
126
- }
127
- declare function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps): react_jsx_runtime.JSX.Element;
128
-
129
- interface MediaGridProps {
130
- items: MediaPickerItem[];
131
- selected: Set<string>;
132
- onToggle: (item: MediaPickerItem) => void;
133
- isLoading: boolean;
134
- total: number;
135
- offset: number;
136
- limit: number;
137
- onPageChange: (offset: number) => void;
138
- classNames?: MediaPickerClassNames;
139
- }
140
- declare function MediaGrid({ items, selected, onToggle, isLoading, total, offset, limit, onPageChange, classNames, }: MediaGridProps): react_jsx_runtime.JSX.Element;
141
-
142
- declare function MediaPicker({ open, onOpenChange, onSelect, mode, accept, mediaType, maxSelect, selectedIds, title, description, classNames, className, children, }: MediaPickerProps): react_jsx_runtime.JSX.Element;
143
-
144
- interface MediaPickerProviderProps extends MediaPickerCallbacks {
145
- children: ReactNode;
146
- }
147
- /**
148
- * Provides media picker callbacks to all picker instances below.
149
- * Wrap your admin layout with this provider.
150
- *
151
- * @example
152
- * ```tsx
153
- * <MediaPickerProvider
154
- * fetchMedia={fetchMediaAction}
155
- * uploadMedia={uploadMediaAction}
156
- * >
157
- * <AdminShell>...</AdminShell>
158
- * </MediaPickerProvider>
159
- * ```
160
- */
161
- declare function MediaPickerProvider({ children, fetchMedia, uploadMedia, }: MediaPickerProviderProps): react_jsx_runtime.JSX.Element;
162
- declare function useMediaPicker(): MediaPickerCallbacks;
163
-
164
- interface SearchBarProps {
165
- value: string;
166
- onChange: (value: string) => void;
167
- mediaType: string | undefined;
168
- onMediaTypeChange: (type: string | undefined) => void;
169
- /** When true, the media type filter is locked (no tabs shown) */
170
- locked?: boolean;
171
- classNames?: MediaPickerClassNames;
172
- }
173
- declare function SearchBar({ value, onChange, mediaType, onMediaTypeChange, locked, classNames, }: SearchBarProps): react_jsx_runtime.JSX.Element;
174
-
175
- interface UploadZoneProps {
176
- onUpload: (file: File) => Promise<void>;
177
- isUploading: boolean;
178
- accept?: string[];
179
- classNames?: MediaPickerClassNames;
180
- }
181
- declare function UploadZone({ onUpload, isUploading, accept, classNames }: UploadZoneProps): react_jsx_runtime.JSX.Element;
182
-
183
- export { type AdminMediaCallbacks, MediaCard, MediaGrid, MediaPicker, type MediaPickerCallbacks, type MediaPickerClassNames, type MediaPickerItem, type MediaPickerListResult, type MediaPickerMode, type MediaPickerProps, MediaPickerProvider, SearchBar, UploadZone, createAdminMediaCallbacks, useMediaPicker };
package/dist/plugin.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var c=Object.create;var o=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,S=Object.prototype.hasOwnProperty;var w=(e,t)=>()=>(e&&(t=e(e=0)),t);var d=(e,t)=>{for(var i in t)o(e,i,{get:t[i],enumerable:!0})},m=(e,t,i,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of b(t))!S.call(e,a)&&a!==i&&o(e,a,{get:()=>t[a],enumerable:!(s=f(t,a))||s.enumerable});return e};var x=(e,t,i)=>(i=e!=null?c(h(e)):{},m(t||!e||!e.__esModule?o(i,"default",{value:e,enumerable:!0}):i,e)),q=e=>m(o({},"__esModule",{value:!0}),e);var p={};d(p,{imageStylesSettings:()=>C});var n,C,y=w(()=>{"use strict";n=require("@murumets-ee/settings"),C=(0,n.defineSettings)({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:n.setting.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}})});var z={};d(z,{getMediaConfig:()=>M,media:()=>v});module.exports=q(z);var r=require("@murumets-ee/entity"),g=(0,r.defineEntity)({name:"media",fields:{title:r.field.text({translatable:!0}),alt:r.field.text({translatable:!0}),description:r.field.text({translatable:!0}),fileKey:r.field.text({required:!0,indexed:!0}),filename:r.field.text({required:!0}),mimeType:r.field.text({required:!0,indexed:!0}),size:r.field.number({required:!0,integer:!0}),width:r.field.number({integer:!0}),height:r.field.number({integer:!0}),mediaType:r.field.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[r.behavior.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}});var l=null;function M(){if(!l)throw new Error("@murumets-ee/media plugin not initialized. Add media() to your plugins array.");return l}function v(e){let t={acceptedTypes:e?.acceptedTypes??["image/*","video/*","audio/*","application/pdf"],maxUploadSize:e?.maxUploadSize??52428800,defaultVisibility:e?.defaultVisibility??"public",imageStyles:e?.imageStyles??{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}}};return{name:"@murumets-ee/media",entities:[g],init:async i=>{if(!i.plugins.has("@murumets-ee/storage"))throw new Error("@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.");if(!i.plugins.has("@murumets-ee/settings"))throw new Error("@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.");l=t;try{let{createSettingsClient:s}=await import("@murumets-ee/settings"),{imageStylesSettings:a}=await Promise.resolve().then(()=>(y(),p)),u=s(a,{app:i});await u.has("imageStyles")||(await u.set("imageStyles",t.imageStyles),i.logger.info({styles:Object.keys(t.imageStyles)},"Seeded default image styles to settings DB"))}catch(s){i.logger.warn({error:s},"Failed to seed image styles to settings DB (non-fatal)")}i.logger.info({acceptedTypes:t.acceptedTypes,maxUploadSize:t.maxUploadSize,defaultVisibility:t.defaultVisibility},"Media plugin initialized")}}}0&&(module.exports={getMediaConfig,media});
package/dist/plugin.d.cts DELETED
@@ -1,35 +0,0 @@
1
- import { Plugin } from '@murumets-ee/core';
2
- import { e as MediaPluginConfig } from './types-ChlTxvlq.cjs';
3
- import '@murumets-ee/storage';
4
-
5
- /**
6
- * Media plugin — auto-registers the Media entity and validates storage dependency.
7
- *
8
- * @example
9
- * ```typescript
10
- * import { media } from '@murumets-ee/media/plugin'
11
- *
12
- * export default defineConfig({
13
- * plugins: [
14
- * storage(),
15
- * media({ maxUploadSize: 10 * 1024 * 1024 }),
16
- * ],
17
- * })
18
- * ```
19
- */
20
-
21
- /**
22
- * Get the resolved media plugin configuration.
23
- * Throws if plugin not initialized.
24
- */
25
- declare function getMediaConfig(): Required<MediaPluginConfig>;
26
- /**
27
- * Media plugin factory.
28
- *
29
- * - Auto-registers the Media entity (no need to add to entities array)
30
- * - Validates that @murumets-ee/storage plugin is registered
31
- * - Stores resolved configuration
32
- */
33
- declare function media(config?: MediaPluginConfig): Plugin;
34
-
35
- export { getMediaConfig, media };
@@ -1 +0,0 @@
1
- "use strict";var y=Object.create;var a=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var m=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty;var b=(t,e)=>{for(var i in e)a(t,i,{get:e[i],enumerable:!0})},u=(t,e,i,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of p(e))!c.call(t,n)&&n!==i&&a(t,n,{get:()=>e[n],enumerable:!(o=g(e,n))||o.enumerable});return t};var d=(t,e,i)=>(i=t!=null?y(m(t)):{},u(e||!t||!t.__esModule?a(i,"default",{value:t,enumerable:!0}):i,t)),f=t=>u(a({},"__esModule",{value:!0}),t);var q={};b(q,{MediaQueryClient:()=>s,createMediaQueryClient:()=>h});module.exports=f(q);var r=require("@murumets-ee/entity"),l=(0,r.defineEntity)({name:"media",fields:{title:r.field.text({translatable:!0}),alt:r.field.text({translatable:!0}),description:r.field.text({translatable:!0}),fileKey:r.field.text({required:!0,indexed:!0}),filename:r.field.text({required:!0}),mimeType:r.field.text({required:!0,indexed:!0}),size:r.field.number({required:!0,integer:!0}),width:r.field.number({integer:!0}),height:r.field.number({integer:!0}),mediaType:r.field.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[r.behavior.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}});var s=class{db;logger;query=null;constructor(e){this.db=e.db,this.logger=e.logger}async getQuery(){if(!this.query){let{QueryClient:e}=await import("@murumets-ee/entity/query");this.query=new e({entity:l,db:this.db,logger:this.logger})}return this.query}async findById(e,i){return await(await this.getQuery()).findById(e,i)}async findMany(e){return await(await this.getQuery()).findMany(e)}async count(e){return(await this.getQuery()).count(e)}};async function h(){let{getApp:t}=await import("@murumets-ee/core"),e=t();return new s({db:e.db.readOnly,logger:e.logger.child({mediaQuery:!0})})}0&&(module.exports={MediaQueryClient,createMediaQueryClient});
@@ -1,36 +0,0 @@
1
- import { Logger } from '@murumets-ee/core';
2
- import { FindByIdOptions, FindManyOptions, CountOptions } from '@murumets-ee/entity/query';
3
- import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
4
- import { b as MediaRecord } from './types-ChlTxvlq.cjs';
5
- import '@murumets-ee/storage';
6
-
7
- /**
8
- * MediaQueryClient — read-only media client for frontends.
9
- *
10
- * Usage:
11
- * import { createMediaQueryClient } from '@murumets-ee/media/query'
12
- * const media = await createMediaQueryClient()
13
- * const image = await media.findById(id)
14
- */
15
-
16
- interface MediaQueryClientConfig {
17
- db: PostgresJsDatabase;
18
- logger?: Logger;
19
- }
20
- declare class MediaQueryClient {
21
- private db;
22
- private logger?;
23
- private query;
24
- constructor(config: MediaQueryClientConfig);
25
- private getQuery;
26
- findById(id: string, options?: FindByIdOptions): Promise<MediaRecord | null>;
27
- findMany(options?: FindManyOptions): Promise<MediaRecord[]>;
28
- count(options?: CountOptions): Promise<number>;
29
- }
30
- /**
31
- * Factory — creates a MediaQueryClient.
32
- * Must be called after createApp().
33
- */
34
- declare function createMediaQueryClient(): Promise<MediaQueryClient>;
35
-
36
- export { MediaQueryClient, type MediaQueryClientConfig, createMediaQueryClient };
package/dist/ref.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var s=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var M=(e,i)=>{for(var t in i)s(e,t,{get:i[t],enumerable:!0})},y=(e,i,t,a)=>{if(i&&typeof i=="object"||typeof i=="function")for(let r of u(i))!g.call(e,r)&&r!==t&&s(e,r,{get:()=>i[r],enumerable:!(a=p(i,r))||a.enumerable});return e};var m=e=>y(s({},"__esModule",{value:!0}),e);var w={};M(w,{createMediaRef:()=>$,extractAllMediaIds:()=>h,extractMediaIds:()=>c,parseMediaRefs:()=>l,resolveMediaRefs:()=>x});module.exports=m(w);var o=/\[media:(image|video|audio|file):([a-f0-9-]{36})(?::([a-z0-9-]+))?\]/g;function l(e){let i=[];o.lastIndex=0;let t=o.exec(e);for(;t!==null;)i.push({raw:t[0],type:t[1],id:t[2],variant:t[3]??null}),t=o.exec(e);return i}function c(e,i){let t=l(e),a=i?.type?t.filter(r=>r.type===i.type):t;return[...new Set(a.map(r=>r.id))]}function h(e,i){let t=e.flatMap(a=>c(a,i));return[...new Set(t)]}function $(e,i,t){return t?`[media:${e}:${i}:${t}]`:`[media:${e}:${i}]`}var v={image:e=>`<img ${[`src="${e.url}"`,`alt="${e.alt??e.filename??""}"`,e.width?`width="${e.width}"`:"",e.height?`height="${e.height}"`:""].filter(Boolean).join(" ")} />`,video:e=>`<video src="${e.url}" controls></video>`,audio:e=>`<audio src="${e.url}" controls></audio>`,file:e=>`<a href="${e.url}" download="${e.filename??""}">${e.filename??e.url}</a>`};async function x(e,i,t){let a=l(e);if(a.length===0)return e;let r=await i(a),d=e;for(let n of a){let f=r.get(n.id);if(!f)continue;let R=t?.[n.type]??v[n.type];R&&(d=d.replace(n.raw,R(f)))}return d}0&&(module.exports={createMediaRef,extractAllMediaIds,extractMediaIds,parseMediaRefs,resolveMediaRefs});
package/dist/ref.d.cts DELETED
@@ -1,100 +0,0 @@
1
- /**
2
- * Media reference system — parse, extract, and resolve [media:type:id:variant] tags.
3
- *
4
- * Two complementary ways to reference media:
5
- * 1. field.media() — stores UUID directly (cover image, hero background)
6
- * 2. [media:type:id:variant] — inline in text/richtext prose
7
- *
8
- * Both resolve to the same Media entity.
9
- *
10
- * @example
11
- * ```typescript
12
- * import { parseMediaRefs, resolveMediaRefs, createMediaRef } from '@murumets-ee/media/ref'
13
- *
14
- * // Create a tag
15
- * const tag = createMediaRef('image', 'abc-123', 'thumbnail')
16
- * // → '[media:image:abc-123:thumbnail]'
17
- *
18
- * // Parse tags from text
19
- * const refs = parseMediaRefs('Here is [media:image:abc-123:hero] a photo')
20
- * // → [{ raw: '[media:image:abc-123:hero]', type: 'image', id: 'abc-123', variant: 'hero' }]
21
- *
22
- * // Resolve tags to HTML
23
- * const html = await resolveMediaRefs(text, myResolver, {
24
- * image: (ref) => ref.variant === 'url' ? ref.url : `<img src="${ref.url}" />`,
25
- * })
26
- * ```
27
- */
28
- /** Supported media reference types in tags */
29
- type MediaRefType = 'image' | 'video' | 'audio' | 'file';
30
- /** Parsed media reference from a [media:type:id] or [media:type:id:variant] tag */
31
- interface MediaRef {
32
- /** The full original tag string */
33
- raw: string;
34
- /** The type prefix (image, video, audio, file) */
35
- type: MediaRefType;
36
- /** The media entity UUID */
37
- id: string;
38
- /** Optional rendering variant hint (thumbnail, hero, url, banner, etc.) */
39
- variant: string | null;
40
- }
41
- /** Resolved media reference with URL and metadata */
42
- interface ResolvedMediaRef extends MediaRef {
43
- /** Resolved URL for the media file */
44
- url: string;
45
- /** Alt text (if available from entity) */
46
- alt?: string;
47
- /** Original filename */
48
- filename?: string;
49
- /** MIME type */
50
- mimeType?: string;
51
- /** Image width */
52
- width?: number;
53
- /** Image height */
54
- height?: number;
55
- }
56
- /**
57
- * Resolver function — provided by consumer to batch-resolve media IDs.
58
- * Receives parsed references, returns a Map of id → resolved reference.
59
- */
60
- type MediaRefResolver = (refs: MediaRef[]) => Promise<Map<string, ResolvedMediaRef>>;
61
- /**
62
- * Renderer function — converts a resolved reference to a string representation.
63
- * Consumer provides custom renderers per type.
64
- */
65
- type MediaRefRenderer = (ref: ResolvedMediaRef) => string;
66
- /**
67
- * Parse all media reference tags from a text string.
68
- */
69
- declare function parseMediaRefs(text: string): MediaRef[];
70
- /**
71
- * Extract unique media entity UUIDs from text.
72
- * Useful for preloading media before rendering.
73
- */
74
- declare function extractMediaIds(text: string, options?: {
75
- type?: MediaRefType;
76
- }): string[];
77
- /**
78
- * Extract unique media entity UUIDs from multiple text strings.
79
- */
80
- declare function extractAllMediaIds(texts: string[], options?: {
81
- type?: MediaRefType;
82
- }): string[];
83
- /**
84
- * Create a media reference tag string.
85
- */
86
- declare function createMediaRef(type: MediaRefType, id: string, variant?: string): string;
87
- /**
88
- * Resolve all media reference tags in a text string.
89
- *
90
- * 1. Parses all [media:type:id:variant] tags
91
- * 2. Calls the resolver function to get URLs + metadata
92
- * 3. Replaces each tag with rendered output
93
- *
94
- * @param text - Input text containing media reference tags
95
- * @param resolver - Function that batch-resolves MediaRef[] → Map<id, ResolvedMediaRef>
96
- * @param renderers - Optional custom renderers per type (override defaults)
97
- */
98
- declare function resolveMediaRefs(text: string, resolver: MediaRefResolver, renderers?: Partial<Record<MediaRefType, MediaRefRenderer>>): Promise<string>;
99
-
100
- export { type MediaRef, type MediaRefRenderer, type MediaRefResolver, type MediaRefType, type ResolvedMediaRef, createMediaRef, extractAllMediaIds, extractMediaIds, parseMediaRefs, resolveMediaRefs };
@@ -1,101 +0,0 @@
1
- import { FileVisibility } from '@murumets-ee/storage';
2
-
3
- /** Media type derived from MIME type */
4
- type MediaType = 'image' | 'video' | 'audio' | 'document' | 'other';
5
- /** Options for uploading media through MediaClient */
6
- interface MediaUploadOptions {
7
- /** Original filename */
8
- filename: string;
9
- /** MIME type */
10
- mimeType: string;
11
- /** File size in bytes */
12
- size: number;
13
- /** File visibility (default: from plugin config) */
14
- visibility?: FileVisibility;
15
- /** Display title (defaults to filename without extension) */
16
- title?: string;
17
- /** Alt text for images */
18
- alt?: string;
19
- /** Description */
20
- description?: string;
21
- /** Image width in pixels (client-measured) */
22
- width?: number;
23
- /** Image height in pixels (client-measured) */
24
- height?: number;
25
- /** UUID of the uploading user */
26
- uploadedBy?: string;
27
- }
28
- /** Result of a media upload: entity record + storage URL */
29
- interface MediaUploadResult {
30
- /** The created media entity record */
31
- media: MediaRecord;
32
- /** The file's public/signed URL */
33
- url: string;
34
- }
35
- /** Lightweight view of a media entity for API consumers */
36
- interface MediaRecord {
37
- id: string;
38
- title: string | null;
39
- alt: string | null;
40
- description: string | null;
41
- fileKey: string;
42
- filename: string;
43
- mimeType: string;
44
- size: number;
45
- width: number | null;
46
- height: number | null;
47
- mediaType: MediaType;
48
- createdBy: string | null;
49
- createdAt: Date | string;
50
- updatedAt: Date | string;
51
- }
52
- /** Options for listing media */
53
- interface MediaListOptions {
54
- /** Filter by media type */
55
- mediaType?: MediaType;
56
- /** Filter by MIME type prefix (e.g., 'image/') */
57
- mimeTypePrefix?: string;
58
- /** Search in title and filename */
59
- search?: string;
60
- /** Pagination limit (default: 50) */
61
- limit?: number;
62
- /** Pagination offset (default: 0) */
63
- offset?: number;
64
- /** Order by field (default: 'createdAt') */
65
- orderBy?: 'createdAt' | 'filename' | 'size';
66
- /** Order direction (default: 'desc') */
67
- orderDirection?: 'asc' | 'desc';
68
- }
69
- /** Paginated media list result */
70
- interface MediaListResult {
71
- items: MediaRecord[];
72
- total: number;
73
- limit: number;
74
- offset: number;
75
- }
76
- /** Named image processing preset — defines how to resize/convert an image variant */
77
- interface ImageStyle {
78
- /** Max width in pixels (omit for height-only constraint) */
79
- width?: number;
80
- /** Max height in pixels (omit for height-only constraint) */
81
- height?: number;
82
- /** Resize strategy (default: 'cover') */
83
- fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
84
- /** Output format (default: 'webp') */
85
- format?: 'webp' | 'jpeg' | 'png' | 'avif';
86
- /** Output quality 1–100 (default: 80) */
87
- quality?: number;
88
- }
89
- /** Plugin configuration for @murumets-ee/media */
90
- interface MediaPluginConfig {
91
- /** Accepted MIME type patterns (default: ['image/*', 'video/*', 'audio/*', 'application/pdf']) */
92
- acceptedTypes?: string[];
93
- /** Max upload size in bytes (default: 50MB) */
94
- maxUploadSize?: number;
95
- /** Default file visibility (default: 'public') */
96
- defaultVisibility?: FileVisibility;
97
- /** Named image processing presets. Variants generated on upload for each style. */
98
- imageStyles?: Record<string, ImageStyle>;
99
- }
100
-
101
- export type { ImageStyle as I, MediaUploadOptions as M, MediaUploadResult as a, MediaRecord as b, MediaListOptions as c, MediaListResult as d, MediaPluginConfig as e, MediaType as f };
package/dist/usage.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";var m=Object.create;var r=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var y=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var u=(t,e)=>{for(var i in e)r(t,i,{get:e[i],enumerable:!0})},l=(t,e,i,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of g(e))!b.call(t,s)&&s!==i&&r(t,s,{get:()=>e[s],enumerable:!(n=p(e,s))||n.enumerable});return t};var k=(t,e,i)=>(i=t!=null?m(y(t)):{},l(e||!t||!t.__esModule?r(i,"default",{value:t,enumerable:!0}):i,t)),x=t=>l(r({},"__esModule",{value:!0}),t);var U={};u(U,{findMediaUsages:()=>N});module.exports=x(U);var F=require("server-only"),f=require("@murumets-ee/entity/refs");async function N(t,e){let i=await(0,f.findEntityUsages)("media",t,e),{getApp:n}=await import("@murumets-ee/core"),s=n();return i.map(o=>{let a="field",c=s.entities.get(o.sourceEntity);if(c){let d=c.allFields[o.sourceField];d&&d.type==="blocks"&&(a="block")}return{entityName:o.sourceEntity,entityId:o.sourceId,fieldName:o.sourceField,context:a}})}0&&(module.exports={findMediaUsages});
package/dist/usage.d.cts DELETED
@@ -1,28 +0,0 @@
1
- import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
2
-
3
- /**
4
- * Media usage lookup — finds all entity references to a media item.
5
- *
6
- * Delegates to the universal entity_refs tracking table (populated at write time
7
- * by AdminClient). One indexed query instead of scanning every table.
8
- *
9
- * Used by delete protection (409 Conflict) and the usage API endpoint.
10
- */
11
-
12
- interface MediaUsage {
13
- entityName: string;
14
- entityId: string;
15
- fieldName: string;
16
- /** 'field' = entity column, 'block' = layout/blocks field */
17
- context: 'field' | 'block';
18
- }
19
- /**
20
- * Find all entities that reference a given media ID.
21
- *
22
- * @param mediaId - UUID of the media item to check
23
- * @param db - Database connection
24
- * @returns Array of usage records (empty if unreferenced)
25
- */
26
- declare function findMediaUsages(mediaId: string, db: PostgresJsDatabase): Promise<MediaUsage[]>;
27
-
28
- export { type MediaUsage, findMediaUsages };