@murumets-ee/media 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=[`cover`,`contain`,`inside`,`outside`,`fill`],t=[`webp`,`jpeg`,`png`,`avif`];function n(n){for(let[r,i]of Object.entries(n)){if(typeof r!=`string`||r.length===0||!/^[a-z][a-z0-9_-]*$/.test(r))return{valid:!1,error:`Invalid style name "${r}": must be lowercase alphanumeric (a-z, 0-9, -, _)`};if(!i||typeof i!=`object`)return{valid:!1,error:`Style "${r}" must be an object`};let n=i;if(n.width!==void 0&&(typeof n.width!=`number`||n.width<=0))return{valid:!1,error:`Invalid width for style "${r}": must be a positive number`};if(n.height!==void 0&&(typeof n.height!=`number`||n.height<=0))return{valid:!1,error:`Invalid height for style "${r}": must be a positive number`};if(n.width===void 0&&n.height===void 0)return{valid:!1,error:`Style "${r}" must have at least width or height`};if(n.quality!==void 0&&(typeof n.quality!=`number`||n.quality<1||n.quality>100))return{valid:!1,error:`Invalid quality for style "${r}": must be 1-100`};if(n.fit!==void 0&&!e.includes(n.fit))return{valid:!1,error:`Invalid fit for style "${r}": must be one of ${e.join(`, `)}`};if(n.format!==void 0&&!t.includes(n.format))return{valid:!1,error:`Invalid format for style "${r}": must be one of ${t.join(`, `)}`}}return{valid:!0,styles:n}}const r=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function i(e){return r.test(e)}let a=null;function o(){return a||=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),{MediaClient:r}=await import(`./client-sF8mf4Fg.mjs`),i=e(),a=t(n(),{app:i});return new r({db:i.db.readWrite,storage:a,logger:i.logger.child({media:!0})})})(),a}function s(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function c(e,t){return s({error:e},t)}async function l(e,{segments:t}){let n=await o();if(t.length===1&&t[0]===`settings`){let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:n}=await import(`@murumets-ee/core`);return s({imageStyles:await e(t,{app:n()}).get(`imageStyles`)??{}})}if(t.length===2&&t[1]===`usage`){let e=t[0];if(!i(e))return c(`Invalid media ID format`,400);let{findMediaUsages:n}=await import(`./usage-D7Bn7Vvv.mjs`),{getApp:r}=await import(`@murumets-ee/core`);return s({usages:await n(e,r().db.readWrite)})}if(t.length>0){let e=t[0],r=await n.findById(e);if(!r)return c(`Media not found`,404);let i=await n.getUrl(e);return s({...r,url:i})}let r=new URL(e.url),a=r.searchParams.get(`search`)??void 0,l=r.searchParams.get(`mediaType`)??void 0,u=Math.min(Math.max(Number(r.searchParams.get(`limit`))||24,1),100),d=Math.max(Number(r.searchParams.get(`offset`))||0,0),f=await n.findMany({search:a,mediaType:l,limit:u,offset:d}),p=f.items.map(e=>e.id),[m,h]=await Promise.all([n.getUrls(p),n.getVariantUrls(p,`thumbnail`)]);return s({items:f.items.map(e=>({id:e.id,title:e.title,alt:e.alt,filename:e.filename,mimeType:e.mimeType,size:e.size,mediaType:e.mediaType,url:m.get(e.id)??``,thumbnailUrl:h.get(e.id),width:e.width,height:e.height})),total:f.total})}async function u(e,{segments:t,user:r,audit:i,checkPermission:a}){if(t.length===1&&t[0]===`regenerate-variants`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for variant regeneration`,403);let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{regenerateAllVariants:n}=await import(`./regenerate-variants-DY7D4Ky3.mjs`),{getApp:o}=await import(`@murumets-ee/core`),{createStorageClient:l}=await import(`@murumets-ee/storage`),{getStorageConfig:u}=await import(`@murumets-ee/storage/plugin`),d=o(),f=await e(t,{app:d}).get(`imageStyles`);if(!f||Object.keys(f).length===0)return c(`No image styles configured`,400);let p=l(u(),{app:d}),m=await n({db:d.db.readWrite,storage:p,logger:d.logger.child({media:!0}),styles:f});return i?.({action:`media.regenerate_variants`,userId:r.id,userName:r.name,metadata:{total:m.total,processed:m.processed,errors:m.errors}}),s(m)}if(t.length===1&&t[0]===`settings`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for image style management`,403);let t=await e.json();if(!t.imageStyles||typeof t.imageStyles!=`object`)return c(`Body must contain "imageStyles" object`,400);let l=n(t.imageStyles);if(!l.valid)return c(l.error,400);let{createSettingsClient:u}=await import(`@murumets-ee/settings`),{imageStylesSettings:d}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:f}=await import(`@murumets-ee/core`);return await u(d,{app:f()}).set(`imageStyles`,l.styles),(await o()).invalidateImageStylesCache(),i?.({action:`media.settings.update`,entityType:`settings`,userId:r.id,userName:r.name,changes:{imageStyles:l.styles}}),s({imageStyles:l.styles})}let l=await o(),u=(await e.formData()).get(`file`);if(!u||u.size===0)return c(`No file provided`,400);if(u.size>50*1024*1024)return c(`File too large: ${(u.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let d=Buffer.from(await u.arrayBuffer()),{detectMimeType:f}=await import(`@murumets-ee/storage`),{mimeType:p,mismatch:m}=await f(d,u.type||`application/octet-stream`);if(m)return c(`File content doesn't match declared type: claimed ${u.type}, detected ${p}`,400);let h=await l.upload(d,{filename:u.name,mimeType:p,size:u.size,uploadedBy:r.id}),g={id:h.media.id,title:h.media.title,alt:h.media.alt,filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType,url:h.url,width:h.media.width,height:h.media.height};return i?.({action:`media.upload`,entityType:`media`,entityId:h.media.id,userId:r.id,userName:r.name,changes:{filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType}}),s(g,201)}async function d(e,{segments:t,user:n,audit:r}){if(t.length===0)return c(`Media ID required`,400);let a=t[0];return i(a)?(await(await o()).delete(a),r?.({action:`media.delete`,entityType:`media`,entityId:a,userId:n.id,userName:n.name}),s({deleted:1})):c(`Invalid media ID format`,400)}function f(){return{prefix:`media`,resource:`media`,actions:[`view`,`create`,`update`,`delete`],handlers:{GET:l,POST:u,DELETE:d}}}export{f as mediaRoutes};
1
+ const e=[`cover`,`contain`,`inside`,`outside`,`fill`],t=[`webp`,`jpeg`,`png`,`avif`];function n(n){for(let[r,i]of Object.entries(n)){if(typeof r!=`string`||r.length===0||!/^[a-z][a-z0-9_-]*$/.test(r))return{valid:!1,error:`Invalid style name "${r}": must be lowercase alphanumeric (a-z, 0-9, -, _)`};if(!i||typeof i!=`object`)return{valid:!1,error:`Style "${r}" must be an object`};let n=i;if(n.width!==void 0&&(typeof n.width!=`number`||n.width<=0))return{valid:!1,error:`Invalid width for style "${r}": must be a positive number`};if(n.height!==void 0&&(typeof n.height!=`number`||n.height<=0))return{valid:!1,error:`Invalid height for style "${r}": must be a positive number`};if(n.width===void 0&&n.height===void 0)return{valid:!1,error:`Style "${r}" must have at least width or height`};if(n.quality!==void 0&&(typeof n.quality!=`number`||n.quality<1||n.quality>100))return{valid:!1,error:`Invalid quality for style "${r}": must be 1-100`};if(n.fit!==void 0&&!e.includes(n.fit))return{valid:!1,error:`Invalid fit for style "${r}": must be one of ${e.join(`, `)}`};if(n.format!==void 0&&!t.includes(n.format))return{valid:!1,error:`Invalid format for style "${r}": must be one of ${t.join(`, `)}`}}return{valid:!0,styles:n}}const r=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function i(e){return r.test(e)}let a=null;function o(){return a||=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),{MediaClient:r}=await import(`./client-DNTvmvYS.mjs`),i=e(),a=t(n(),{app:i});return new r({db:i.db.readWrite,storage:a,logger:i.logger.child({media:!0})})})(),a}function s(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function c(e,t){return s({error:e},t)}async function l(e,{segments:t}){let n=await o();if(t.length===1&&t[0]===`settings`){let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-5zpGEfKG.mjs`),{getApp:n}=await import(`@murumets-ee/core`);return s({imageStyles:await e(t,{app:n()}).get(`imageStyles`)??{}})}if(t.length===2&&t[1]===`usage`){let e=t[0];if(!i(e))return c(`Invalid media ID format`,400);let{findMediaUsages:n}=await import(`./usage-D7Bn7Vvv.mjs`),{getApp:r}=await import(`@murumets-ee/core`);return s({usages:await n(e,r().db.readWrite)})}if(t.length>0){let e=t[0],r=await n.findById(e);if(!r)return c(`Media not found`,404);let i=await n.getUrl(e);return s({...r,url:i})}let r=new URL(e.url),a=r.searchParams.get(`search`)??void 0,l=r.searchParams.get(`mediaType`)??void 0,u=Math.min(Math.max(Number(r.searchParams.get(`limit`))||24,1),100),d=Math.max(Number(r.searchParams.get(`offset`))||0,0),f=await n.findMany({search:a,mediaType:l,limit:u,offset:d}),p=f.items.map(e=>e.id),[m,h]=await Promise.all([n.getUrls(p),n.getVariantUrls(p,`thumbnail`)]);return s({items:f.items.map(e=>({id:e.id,title:e.title,alt:e.alt,filename:e.filename,mimeType:e.mimeType,size:e.size,mediaType:e.mediaType,url:m.get(e.id)??``,thumbnailUrl:h.get(e.id),width:e.width,height:e.height})),total:f.total})}async function u(e,{segments:t,user:r,audit:i,checkPermission:a}){if(t.length===1&&t[0]===`regenerate-variants`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for variant regeneration`,403);let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-5zpGEfKG.mjs`),{regenerateAllVariants:n}=await import(`./regenerate-variants-DY7D4Ky3.mjs`),{getApp:o}=await import(`@murumets-ee/core`),{createStorageClient:l}=await import(`@murumets-ee/storage`),{getStorageConfig:u}=await import(`@murumets-ee/storage/plugin`),d=o(),f=await e(t,{app:d}).get(`imageStyles`);if(!f||Object.keys(f).length===0)return c(`No image styles configured`,400);let p=l(u(),{app:d}),m=await n({db:d.db.readWrite,storage:p,logger:d.logger.child({media:!0}),styles:f});return i?.({action:`media.regenerate_variants`,userId:r.id,userName:r.name,metadata:{total:m.total,processed:m.processed,errors:m.errors}}),s(m)}if(t.length===1&&t[0]===`settings`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for image style management`,403);let t=await e.json();if(!t.imageStyles||typeof t.imageStyles!=`object`)return c(`Body must contain "imageStyles" object`,400);let l=n(t.imageStyles);if(!l.valid)return c(l.error,400);let{createSettingsClient:u}=await import(`@murumets-ee/settings`),{imageStylesSettings:d}=await import(`./image-styles-settings-5zpGEfKG.mjs`),{getApp:f}=await import(`@murumets-ee/core`);return await u(d,{app:f()}).set(`imageStyles`,l.styles),(await o()).invalidateImageStylesCache(),i?.({action:`media.settings.update`,entityType:`settings`,userId:r.id,userName:r.name,changes:{imageStyles:l.styles}}),s({imageStyles:l.styles})}let l=await o(),u=(await e.formData()).get(`file`);if(!u||u.size===0)return c(`No file provided`,400);if(u.size>50*1024*1024)return c(`File too large: ${(u.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let d=Buffer.from(await u.arrayBuffer()),{detectMimeType:f}=await import(`@murumets-ee/storage`),{mimeType:p,mismatch:m}=await f(d,u.type||`application/octet-stream`);if(m)return c(`File content doesn't match declared type: claimed ${u.type}, detected ${p}`,400);let h=await l.upload(d,{filename:u.name,mimeType:p,size:u.size,uploadedBy:r.id}),g={id:h.media.id,title:h.media.title,alt:h.media.alt,filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType,url:h.url,width:h.media.width,height:h.media.height};return i?.({action:`media.upload`,entityType:`media`,entityId:h.media.id,userId:r.id,userName:r.name,changes:{filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType}}),s(g,201)}async function d(e,{segments:t,user:n,audit:r}){if(t.length===0)return c(`Media ID required`,400);let a=t[0];return i(a)?(await(await o()).delete(a),r?.({action:`media.delete`,entityType:`media`,entityId:a,userId:n.id,userName:n.name}),s({deleted:1})):c(`Invalid media ID format`,400)}function f(){return{prefix:`media`,resource:`media`,actions:[`view`,`create`,`update`,`delete`],handlers:{GET:l,POST:u,DELETE:d}}}export{f as mediaRoutes};
2
2
  //# sourceMappingURL=admin.mjs.map
@@ -1,2 +1,2 @@
1
- import{Media as e}from"./entity-DZFku8b7.mjs";import{n as t,r as n,t as r}from"./variant-key-DZUYURS5.mjs";import"server-only";var i=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:t}=await import(`@murumets-ee/entity/admin`);this.admin=new t({entity:e,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 import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:n}=await import(`@murumets-ee/core`),r=await e(t,{app:n()}).get(`imageStyles`);if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import(`./plugin-DV7lvImm.mjs`);return this.imageStyles=e().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,i){let s=await this.storage.upload(e,{filename:i.filename,mimeType:i.mimeType,size:i.size,visibility:i.visibility,uploadedBy:i.uploadedBy}),c=i.width??null,l=i.height??null,u={};if(e instanceof Buffer&&t(i.mimeType))try{let t=await n(e,await this.resolveImageStyles());c=t.width,l=t.height;let a=s.visibility;await Promise.all([...t.variants.entries()].map(async([e,t])=>{let n=r(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:n,filename:`${e}_${i.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:a,metadata:{variantOf:s.key,style:e},uploadedBy:i.uploadedBy}),u[e]=n,this.logger?.debug({style:e,key:n,width:t.width,height:t.height},`Variant uploaded`)}catch(t){this.logger?.warn({style:e,key:n,error:t},`Failed to upload variant (non-fatal)`)}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(e=>{this.logger?.warn({key:s.key,error:e},`Failed to update original file metadata with variant keys (non-fatal)`)}),this.logger?.info({width:c,height:l,variants:Object.keys(u)},`Image processed`)}catch(e){this.logger?.warn({filename:i.filename,error:e},`Image processing failed (non-fatal, original saved)`)}let d=a(i.mimeType),f=await this.getAdmin();try{let e=await f.create({title:i.title??o(i.filename),alt:i.alt??null,description:i.description??null,fileKey:s.key,filename:i.filename,mimeType:i.mimeType,size:i.size,width:c,height:l,mediaType:d}),t=await this.storage.getUrl(s.key);return this.logger?.info({id:e.id,fileKey:s.key,mediaType:d},`Media uploaded`),{media:e,url:t}}catch(e){this.logger?.error({fileKey:s.key,error:e},`Media entity creation failed, rolling back storage uploads`);for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(e=>{this.logger?.error({fileKey:s.key,error:e},`Storage rollback also failed`)}),e}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{and:r,asc:i,desc:a,eq:o,ilike:s,sql:c}=await import(`drizzle-orm`),l=n.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(o(l.mediaType,e.mediaType)),e?.mimeTypePrefix&&u.push(s(l.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let t=`%${e.search}%`;u.push(c`(${s(l.filename,t)} OR ${l.fields} ->> 'title' ILIKE ${t})`)}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?r(...u):void 0,[m]=await this.db.select({count:c`count(*)::int`}).from(l).where(p),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?i:a;return{items:await t.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m?.count??0,limit:d,offset:f}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),n=await t.findById(e);if(!n)throw Error(`Media not found: ${e}`);let r=n.fileKey;await t.delete(e);let i=(await this.storage.getMetadata(r))?.metadata?.variants;if(i)for(let e of Object.values(i))await this.storage.delete(e).catch(t=>{this.logger?.warn({variantKey:e,error:t},`Failed to delete variant file`)});await this.storage.delete(r).catch(t=>{this.logger?.error({id:e,fileKey:r,error:t},`Failed to delete file from storage after entity deletion`)}),this.logger?.info({id:e,fileKey:r,deletedVariants:i?Object.keys(i).length:0},`Media deleted`)}async getUrl(e){let t=await(await this.getAdmin()).findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:r}=await import(`drizzle-orm`),i=n.get(`media`);if(!i)return new Map;let a=await t.findMany({where:r(i.id,e),limit:e.length}),o=new Map;return await Promise.all(a.map(async e=>{let t=await this.storage.getUrl(e.fileKey);o.set(e.id,t)})),o}async getVariantUrl(e,t){let n=await(await this.getAdmin()).findById(e);if(!n)return null;let i=n.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=r(i,t,a.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(i)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let n=await this.getAdmin(),{schemaRegistry:i}=await import(`@murumets-ee/db`),{inArray:a}=await import(`drizzle-orm`),o=i.get(`media`);if(!o)return new Map;let s=await n.findMany({where:a(o.id,e),limit:e.length}),c=(await this.resolveImageStyles())[t],l=new Map;return await Promise.all(s.map(async e=>{if(c){let n=r(e.fileKey,t,c.format??`webp`);try{let t=await this.storage.getUrl(n);l.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);l.set(e.id,t)}catch{}})),l}};function a(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}function o(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{i as MediaClient};
2
- //# sourceMappingURL=client-sF8mf4Fg.mjs.map
1
+ import{Media as e}from"./entity-DZFku8b7.mjs";import{n as t,r as n,t as r}from"./variant-key-DZUYURS5.mjs";import"server-only";var i=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:t}=await import(`@murumets-ee/entity/admin`);this.admin=new t({entity:e,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 import(`./image-styles-settings-5zpGEfKG.mjs`),{getApp:n}=await import(`@murumets-ee/core`),r=await e(t,{app:n()}).get(`imageStyles`);if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import(`./plugin-DV7lvImm.mjs`);return this.imageStyles=e().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,i){let s=await this.storage.upload(e,{filename:i.filename,mimeType:i.mimeType,size:i.size,visibility:i.visibility,uploadedBy:i.uploadedBy}),c=i.width??null,l=i.height??null,u={};if(e instanceof Buffer&&t(i.mimeType))try{let t=await n(e,await this.resolveImageStyles());c=t.width,l=t.height;let a=s.visibility;await Promise.all([...t.variants.entries()].map(async([e,t])=>{let n=r(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:n,filename:`${e}_${i.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:a,metadata:{variantOf:s.key,style:e},uploadedBy:i.uploadedBy}),u[e]=n,this.logger?.debug({style:e,key:n,width:t.width,height:t.height},`Variant uploaded`)}catch(t){this.logger?.warn({style:e,key:n,error:t},`Failed to upload variant (non-fatal)`)}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(e=>{this.logger?.warn({key:s.key,error:e},`Failed to update original file metadata with variant keys (non-fatal)`)}),this.logger?.info({width:c,height:l,variants:Object.keys(u)},`Image processed`)}catch(e){this.logger?.warn({filename:i.filename,error:e},`Image processing failed (non-fatal, original saved)`)}let d=a(i.mimeType),f=await this.getAdmin();try{let e=await f.create({title:i.title??o(i.filename),alt:i.alt??null,description:i.description??null,fileKey:s.key,filename:i.filename,mimeType:i.mimeType,size:i.size,width:c,height:l,mediaType:d}),t=await this.storage.getUrl(s.key);return this.logger?.info({id:e.id,fileKey:s.key,mediaType:d},`Media uploaded`),{media:e,url:t}}catch(e){this.logger?.error({fileKey:s.key,error:e},`Media entity creation failed, rolling back storage uploads`);for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(e=>{this.logger?.error({fileKey:s.key,error:e},`Storage rollback also failed`)}),e}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{and:r,asc:i,desc:a,eq:o,ilike:s,sql:c}=await import(`drizzle-orm`),l=n.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(o(l.mediaType,e.mediaType)),e?.mimeTypePrefix&&u.push(s(l.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let t=`%${e.search}%`;u.push(c`(${s(l.filename,t)} OR ${l.fields} ->> 'title' ILIKE ${t})`)}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?r(...u):void 0,[m]=await this.db.select({count:c`count(*)::int`}).from(l).where(p),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?i:a;return{items:await t.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m?.count??0,limit:d,offset:f}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),n=await t.findById(e);if(!n)throw Error(`Media not found: ${e}`);let r=n.fileKey;await t.delete(e);let i=(await this.storage.getMetadata(r))?.metadata?.variants;if(i)for(let e of Object.values(i))await this.storage.delete(e).catch(t=>{this.logger?.warn({variantKey:e,error:t},`Failed to delete variant file`)});await this.storage.delete(r).catch(t=>{this.logger?.error({id:e,fileKey:r,error:t},`Failed to delete file from storage after entity deletion`)}),this.logger?.info({id:e,fileKey:r,deletedVariants:i?Object.keys(i).length:0},`Media deleted`)}async getUrl(e){let t=await(await this.getAdmin()).findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:r}=await import(`drizzle-orm`),i=n.get(`media`);if(!i)return new Map;let a=await t.findMany({where:r(i.id,e),limit:e.length}),o=new Map;return await Promise.all(a.map(async e=>{let t=await this.storage.getUrl(e.fileKey);o.set(e.id,t)})),o}async getVariantUrl(e,t){let n=await(await this.getAdmin()).findById(e);if(!n)return null;let i=n.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=r(i,t,a.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(i)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let n=await this.getAdmin(),{schemaRegistry:i}=await import(`@murumets-ee/db`),{inArray:a}=await import(`drizzle-orm`),o=i.get(`media`);if(!o)return new Map;let s=await n.findMany({where:a(o.id,e),limit:e.length}),c=(await this.resolveImageStyles())[t],l=new Map;return await Promise.all(s.map(async e=>{if(c){let n=r(e.fileKey,t,c.format??`webp`);try{let t=await this.storage.getUrl(n);l.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);l.set(e.id,t)}catch{}})),l}};function a(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}function o(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{i as MediaClient};
2
+ //# sourceMappingURL=client-DNTvmvYS.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-sF8mf4Fg.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface MediaClientConfig {\n db: PostgresJsDatabase\n storage: StorageClient\n logger?: Logger\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private db: PostgresJsDatabase\n private storage: StorageClient\n private logger?: Logger\n private admin: AdminClient | null = null\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.db = config.db\n this.storage = config.storage\n this.logger = config.logger\n this.imageStyles = config.imageStyles ?? null\n }\n\n private async getAdmin(): Promise<AdminClient> {\n if (!this.admin) {\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n this.admin = new AdminClient({\n entity: Media,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.admin\n }\n\n /**\n * Resolve image styles: settings DB → plugin config → hardcoded defaults.\n * Result is cached; call `invalidateImageStylesCache()` after settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n\n // 1. Try settings DB (source of truth after first boot)\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const stored = await settingsClient.get('imageStyles')\n if (stored && Object.keys(stored).length > 0) {\n this.imageStyles = stored\n return stored\n }\n } catch {\n // Settings not available — fall through to plugin config\n }\n\n // 2. Fall back to plugin config singleton\n try {\n const { getMediaConfig } = await import('./plugin.js')\n const config = getMediaConfig()\n this.imageStyles = config.imageStyles\n return this.imageStyles\n } catch {\n // Plugin not initialized — hardcoded defaults\n const defaults: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n }\n this.imageStyles = defaults\n return defaults\n }\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n this.logger?.debug(\n { style: styleName, key: vKey, width: variant.width, height: variant.height },\n 'Variant uploaded',\n )\n } catch (variantErr: unknown) {\n this.logger?.warn(\n { style: styleName, key: vKey, error: variantErr },\n 'Failed to upload variant (non-fatal)',\n )\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch((metaErr: unknown) => {\n this.logger?.warn(\n { key: fileRecord.key, error: metaErr },\n 'Failed to update original file metadata with variant keys (non-fatal)',\n )\n })\n }\n\n this.logger?.info({ width, height, variants: Object.keys(variantKeys) }, 'Image processed')\n } catch (processErr: unknown) {\n this.logger?.warn(\n { filename: options.filename, error: processErr },\n 'Image processing failed (non-fatal, original saved)',\n )\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n const admin = await this.getAdmin()\n\n try {\n const entityRecord = await admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n } as Record<string, EntityFieldValue>)\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n this.logger?.info(\n { id: entityRecord.id, fileKey: fileRecord.key, mediaType },\n 'Media uploaded',\n )\n\n return {\n media: entityRecord as unknown as MediaRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n this.logger?.error(\n { fileKey: fileRecord.key, error },\n 'Media entity creation failed, rolling back storage uploads',\n )\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch((rollbackErr: unknown) => {\n this.logger?.error(\n { fileKey: fileRecord.key, error: rollbackErr },\n 'Storage rollback also failed',\n )\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n const admin = await this.getAdmin()\n const result = await admin.findById(id, options)\n return result as unknown as MediaRecord | null\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n conditions.push(ilike(table.mimeType, `${options.mimeTypePrefix}%`))\n }\n if (options?.search) {\n const pattern = `%${options.search}%`\n conditions.push(\n sql`(${ilike(table.filename, pattern)} OR ${table.fields} ->> 'title' ILIKE ${pattern})`,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total\n const [countResult] = await this.db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(whereClause)\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items: items as unknown as MediaRecord[],\n total: countResult?.count ?? 0,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n const admin = await this.getAdmin()\n const result = await admin.update(id, data as Record<string, EntityFieldValue>)\n return result as unknown as MediaRecord\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch((err: unknown) => {\n this.logger?.warn({ variantKey: vKey, error: err }, 'Failed to delete variant file')\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch((err: unknown) => {\n this.logger?.error(\n { id, fileKey, error: err },\n 'Failed to delete file from storage after entity deletion',\n )\n })\n\n this.logger?.info(\n { id, fileKey, deletedVariants: variants ? Object.keys(variants).length : 0 },\n 'Media deleted',\n )\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl((record as unknown as MediaRecord).fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) return null\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Lazy singleton — auto-discovers storage config\n// ---------------------------------------------------------------------------\n\nlet _clientPromise: Promise<MediaClient> | null = null\n\n/**\n * Lazy singleton — returns a shared MediaClient instance.\n * Auto-discovers storage configuration from the storage plugin.\n * Must be called after createApp().\n */\nexport function getMediaClient(): Promise<MediaClient> {\n if (!_clientPromise) {\n _clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n\n const app = getApp()\n const config = getStorageConfig()\n const storage = createStorageClient(config, { app })\n\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n })()\n }\n return _clientPromise\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-DNTvmvYS.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface MediaClientConfig {\n db: PostgresJsDatabase\n storage: StorageClient\n logger?: Logger\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private db: PostgresJsDatabase\n private storage: StorageClient\n private logger?: Logger\n private admin: AdminClient | null = null\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.db = config.db\n this.storage = config.storage\n this.logger = config.logger\n this.imageStyles = config.imageStyles ?? null\n }\n\n private async getAdmin(): Promise<AdminClient> {\n if (!this.admin) {\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n this.admin = new AdminClient({\n entity: Media,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.admin\n }\n\n /**\n * Resolve image styles: settings DB → plugin config → hardcoded defaults.\n * Result is cached; call `invalidateImageStylesCache()` after settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n\n // 1. Try settings DB (source of truth after first boot)\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const stored = await settingsClient.get('imageStyles')\n if (stored && Object.keys(stored).length > 0) {\n this.imageStyles = stored\n return stored\n }\n } catch {\n // Settings not available — fall through to plugin config\n }\n\n // 2. Fall back to plugin config singleton\n try {\n const { getMediaConfig } = await import('./plugin.js')\n const config = getMediaConfig()\n this.imageStyles = config.imageStyles\n return this.imageStyles\n } catch {\n // Plugin not initialized — hardcoded defaults\n const defaults: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n }\n this.imageStyles = defaults\n return defaults\n }\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n this.logger?.debug(\n { style: styleName, key: vKey, width: variant.width, height: variant.height },\n 'Variant uploaded',\n )\n } catch (variantErr: unknown) {\n this.logger?.warn(\n { style: styleName, key: vKey, error: variantErr },\n 'Failed to upload variant (non-fatal)',\n )\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch((metaErr: unknown) => {\n this.logger?.warn(\n { key: fileRecord.key, error: metaErr },\n 'Failed to update original file metadata with variant keys (non-fatal)',\n )\n })\n }\n\n this.logger?.info({ width, height, variants: Object.keys(variantKeys) }, 'Image processed')\n } catch (processErr: unknown) {\n this.logger?.warn(\n { filename: options.filename, error: processErr },\n 'Image processing failed (non-fatal, original saved)',\n )\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n const admin = await this.getAdmin()\n\n try {\n const entityRecord = await admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n } as Record<string, EntityFieldValue>)\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n this.logger?.info(\n { id: entityRecord.id, fileKey: fileRecord.key, mediaType },\n 'Media uploaded',\n )\n\n return {\n media: entityRecord as unknown as MediaRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n this.logger?.error(\n { fileKey: fileRecord.key, error },\n 'Media entity creation failed, rolling back storage uploads',\n )\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch((rollbackErr: unknown) => {\n this.logger?.error(\n { fileKey: fileRecord.key, error: rollbackErr },\n 'Storage rollback also failed',\n )\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n const admin = await this.getAdmin()\n const result = await admin.findById(id, options)\n return result as unknown as MediaRecord | null\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n conditions.push(ilike(table.mimeType, `${options.mimeTypePrefix}%`))\n }\n if (options?.search) {\n const pattern = `%${options.search}%`\n conditions.push(\n sql`(${ilike(table.filename, pattern)} OR ${table.fields} ->> 'title' ILIKE ${pattern})`,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total\n const [countResult] = await this.db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(whereClause)\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items: items as unknown as MediaRecord[],\n total: countResult?.count ?? 0,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n const admin = await this.getAdmin()\n const result = await admin.update(id, data as Record<string, EntityFieldValue>)\n return result as unknown as MediaRecord\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch((err: unknown) => {\n this.logger?.warn({ variantKey: vKey, error: err }, 'Failed to delete variant file')\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch((err: unknown) => {\n this.logger?.error(\n { id, fileKey, error: err },\n 'Failed to delete file from storage after entity deletion',\n )\n })\n\n this.logger?.info(\n { id, fileKey, deletedVariants: variants ? Object.keys(variants).length : 0 },\n 'Media deleted',\n )\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl((record as unknown as MediaRecord).fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) return null\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Lazy singleton — auto-discovers storage config\n// ---------------------------------------------------------------------------\n\nlet _clientPromise: Promise<MediaClient> | null = null\n\n/**\n * Lazy singleton — returns a shared MediaClient instance.\n * Auto-discovers storage configuration from the storage plugin.\n * Must be called after createApp().\n */\nexport function getMediaClient(): Promise<MediaClient> {\n if (!_clientPromise) {\n _clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n\n const app = getApp()\n const config = getStorageConfig()\n const storage = createStorageClient(config, { app })\n\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n })()\n }\n return _clientPromise\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}}.thumbnail},label:`Image processing presets`})}});export{n as imageStylesSettings};
2
+ //# sourceMappingURL=image-styles-settings-5zpGEfKG.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-styles-settings-5zpGEfKG.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEAiCA,MAAa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAdsD,CAC5D,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CACjF,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAC5E,KAAM,CAAE,MAAO,KAAM,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAC7E,KAAM,CAAE,MAAO,KAAM,IAAK,SAAU,OAAQ,OAAQ,QAAS,GAAI,CAClE,CASqC,UAC/B,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}
@@ -2,9 +2,23 @@ import { t as ImageStyle } from "./types-BV_pOm23.mjs";
2
2
  import * as _$_murumets_ee_settings0 from "@murumets-ee/settings";
3
3
 
4
4
  //#region src/image-styles-settings.d.ts
5
+ /**
6
+ * Curated default image style presets for new projects.
7
+ *
8
+ * Consumers can register all of these via the media plugin config:
9
+ * media({ imageStyles: defaultImageStyles })
10
+ *
11
+ * Or pick a subset:
12
+ * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })
13
+ *
14
+ * The `imageStylesSettings` default below intentionally only includes
15
+ * `thumbnail` — adding more here would auto-create them for every project
16
+ * that uses the plugin without opting in.
17
+ */
18
+ declare const defaultImageStyles: Record<string, ImageStyle>;
5
19
  declare const imageStylesSettings: _$_murumets_ee_settings0.SettingsDefinition<{
6
20
  readonly imageStyles: _$_murumets_ee_settings0.JsonSettingConfig<Record<string, ImageStyle>>;
7
21
  }>;
8
22
  //#endregion
9
- export { imageStylesSettings };
23
+ export { defaultImageStyles, imageStylesSettings };
10
24
  //# sourceMappingURL=image-styles-settings.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;cAaa,mBAAA,2BAAmB,kBAAA;EAAA"}
1
+ {"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;;cA0Ba,kBAAA,EAAoB,MAAA,SAAe,UAAA;AAAA,cAOnC,mBAAA,2BAAmB,kBAAA;EAAA"}
@@ -1,2 +1,2 @@
1
- import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}},label:`Image processing presets`})}});export{n as imageStylesSettings};
1
+ import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}},r=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:n.thumbnail},label:`Image processing presets`})}});export{n as defaultImageStyles,r as imageStylesSettings};
2
2
  //# sourceMappingURL=image-styles-settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEAaA,MAAa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}
1
+ {"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEA0BA,MAAa,EAAiD,CAC5D,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CACjF,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAC5E,KAAM,CAAE,MAAO,KAAM,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAC7E,KAAM,CAAE,MAAO,KAAM,IAAK,SAAU,OAAQ,OAAQ,QAAS,GAAI,CAClE,CAEY,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,EAAmB,UAC/B,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BV_pOm23.mjs";
2
- import { imageStylesSettings } from "./image-styles-settings.mjs";
2
+ import { defaultImageStyles, imageStylesSettings } from "./image-styles-settings.mjs";
3
3
  import * as _$_murumets_ee_entity0 from "@murumets-ee/entity";
4
4
  //#region src/enrich.d.ts
5
5
  /**
@@ -108,5 +108,5 @@ interface MediaPickerCallbacks {
108
108
  uploadMedia: (file: File) => Promise<MediaPickerItem>;
109
109
  }
110
110
  //#endregion
111
- export { type ImageStyle, Media, type MediaListOptions, type MediaListResult, type MediaPickerCallbacks, type MediaPickerItem, type MediaPickerListResult, type MediaPluginConfig, type MediaRecord, type MediaType, type MediaUploadOptions, type MediaUploadResult, enrichWithMediaUrls, imageStylesSettings };
111
+ export { type ImageStyle, Media, type MediaListOptions, type MediaListResult, type MediaPickerCallbacks, type MediaPickerItem, type MediaPickerListResult, type MediaPluginConfig, type MediaRecord, type MediaType, type MediaUploadOptions, type MediaUploadResult, defaultImageStyles, enrichWithMediaUrls, imageStylesSettings };
112
112
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./entity-D5P2l05s.mjs";import{imageStylesSettings as t}from"./image-styles-settings.mjs";import"server-only";async function n(e,t,n=`thumbnail`){let r=Object.entries(e.allFields).filter(([,e])=>e.type===`media`).map(([e])=>e);if(r.length===0)return;let i=new Set;for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&n.length>0&&i.add(n)}if(i.size===0)return;let{getMediaClient:a}=await import(`./client.mjs`),o=await(await a()).getVariantUrls([...i],n);for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&o.has(n)&&(e[`${t}Url`]=o.get(n))}}export{e as Media,n as enrichWithMediaUrls,t as imageStylesSettings};
1
+ import{t as e}from"./entity-D5P2l05s.mjs";import{defaultImageStyles as t,imageStylesSettings as n}from"./image-styles-settings.mjs";import"server-only";async function r(e,t,n=`thumbnail`){let r=Object.entries(e.allFields).filter(([,e])=>e.type===`media`).map(([e])=>e);if(r.length===0)return;let i=new Set;for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&n.length>0&&i.add(n)}if(i.size===0)return;let{getMediaClient:a}=await import(`./client.mjs`),o=await(await a()).getVariantUrls([...i],n);for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&o.has(n)&&(e[`${t}Url`]=o.get(n))}}export{e as Media,t as defaultImageStyles,r as enrichWithMediaUrls,n as imageStylesSettings};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["/**\n * Server-side utility to enrich entity list items with resolved media URLs.\n *\n * For entities with `field.media()` columns, this scans items for media UUIDs,\n * batch-resolves them via MediaClient, and injects `${fieldName}Url` into each item.\n *\n * Convention: a media field named `coverImage` (UUID) gets enriched with\n * `coverImageUrl` (resolved URL string).\n *\n * @example\n * ```typescript\n * import { enrichWithMediaUrls } from '@murumets-ee/media'\n *\n * const items = await adminClient.findMany({ limit: 20 })\n * await enrichWithMediaUrls(Article, items)\n * // items[0].coverImageUrl → 'https://cdn.example.com/uploads/.../thumbnail_photo.webp'\n * ```\n */\n\nimport 'server-only'\n\n/**\n * Enrich entity list items by resolving media field UUIDs to variant URLs.\n *\n * Mutates items in place — injects `${fieldName}Url` for each media field.\n *\n * @param entity - Entity definition (or any object with allFields containing type info)\n * @param items - Array of entity records to enrich\n * @param styleName - Image style to resolve (default: 'thumbnail')\n */\nexport async function enrichWithMediaUrls(\n entity: { allFields: Record<string, { type: string }> },\n items: Record<string, unknown>[],\n styleName = 'thumbnail',\n): Promise<void> {\n // 1. Find media-type fields in entity definition\n const mediaFields = Object.entries(entity.allFields)\n .filter(([, config]) => config.type === 'media')\n .map(([name]) => name)\n\n if (mediaFields.length === 0) return\n\n // 2. Collect unique media UUIDs across all items\n const mediaIds = new Set<string>()\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && val.length > 0) {\n mediaIds.add(val)\n }\n }\n }\n\n if (mediaIds.size === 0) return\n\n // 3. Batch resolve via MediaClient\n const { getMediaClient } = await import('./client.js')\n const client = await getMediaClient()\n const urlMap = await client.getVariantUrls([...mediaIds], styleName)\n\n // 4. Inject ${fieldName}Url into each item\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && urlMap.has(val)) {\n item[`${f}Url`] = urlMap.get(val)\n }\n }\n }\n}\n"],"mappings":"gIA8BA,eAAsB,EACpB,EACA,EACA,EAAY,YACG,CAEf,IAAM,EAAc,OAAO,QAAQ,EAAO,UAAU,CACjD,QAAQ,EAAG,KAAY,EAAO,OAAS,QAAQ,CAC/C,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAY,SAAW,EAAG,OAG9B,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAI,OAAS,GAC1C,EAAS,IAAI,EAAI,CAKvB,GAAI,EAAS,OAAS,EAAG,OAGzB,GAAM,CAAE,kBAAmB,MAAM,OAAO,gBAElC,EAAS,MADA,MAAM,GAAgB,EACT,eAAe,CAAC,GAAG,EAAS,CAAE,EAAU,CAGpE,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAO,IAAI,EAAI,GAC5C,EAAK,GAAG,EAAE,MAAQ,EAAO,IAAI,EAAI"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["/**\n * Server-side utility to enrich entity list items with resolved media URLs.\n *\n * For entities with `field.media()` columns, this scans items for media UUIDs,\n * batch-resolves them via MediaClient, and injects `${fieldName}Url` into each item.\n *\n * Convention: a media field named `coverImage` (UUID) gets enriched with\n * `coverImageUrl` (resolved URL string).\n *\n * @example\n * ```typescript\n * import { enrichWithMediaUrls } from '@murumets-ee/media'\n *\n * const items = await adminClient.findMany({ limit: 20 })\n * await enrichWithMediaUrls(Article, items)\n * // items[0].coverImageUrl → 'https://cdn.example.com/uploads/.../thumbnail_photo.webp'\n * ```\n */\n\nimport 'server-only'\n\n/**\n * Enrich entity list items by resolving media field UUIDs to variant URLs.\n *\n * Mutates items in place — injects `${fieldName}Url` for each media field.\n *\n * @param entity - Entity definition (or any object with allFields containing type info)\n * @param items - Array of entity records to enrich\n * @param styleName - Image style to resolve (default: 'thumbnail')\n */\nexport async function enrichWithMediaUrls(\n entity: { allFields: Record<string, { type: string }> },\n items: Record<string, unknown>[],\n styleName = 'thumbnail',\n): Promise<void> {\n // 1. Find media-type fields in entity definition\n const mediaFields = Object.entries(entity.allFields)\n .filter(([, config]) => config.type === 'media')\n .map(([name]) => name)\n\n if (mediaFields.length === 0) return\n\n // 2. Collect unique media UUIDs across all items\n const mediaIds = new Set<string>()\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && val.length > 0) {\n mediaIds.add(val)\n }\n }\n }\n\n if (mediaIds.size === 0) return\n\n // 3. Batch resolve via MediaClient\n const { getMediaClient } = await import('./client.js')\n const client = await getMediaClient()\n const urlMap = await client.getVariantUrls([...mediaIds], styleName)\n\n // 4. Inject ${fieldName}Url into each item\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && urlMap.has(val)) {\n item[`${f}Url`] = urlMap.get(val)\n }\n }\n }\n}\n"],"mappings":"wJA8BA,eAAsB,EACpB,EACA,EACA,EAAY,YACG,CAEf,IAAM,EAAc,OAAO,QAAQ,EAAO,UAAU,CACjD,QAAQ,EAAG,KAAY,EAAO,OAAS,QAAQ,CAC/C,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAY,SAAW,EAAG,OAG9B,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAI,OAAS,GAC1C,EAAS,IAAI,EAAI,CAKvB,GAAI,EAAS,OAAS,EAAG,OAGzB,GAAM,CAAE,kBAAmB,MAAM,OAAO,gBAElC,EAAS,MADA,MAAM,GAAgB,EACT,eAAe,CAAC,GAAG,EAAS,CAAE,EAAU,CAGpE,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAO,IAAI,EAAI,GAC5C,EAAK,GAAG,EAAE,MAAQ,EAAO,IAAI,EAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/media",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -52,12 +52,12 @@
52
52
  "server-only": "^0.0.1",
53
53
  "sharp": "^0.34.5",
54
54
  "tailwind-merge": "^2.6.0",
55
- "@murumets-ee/logging": "0.1.6",
56
- "@murumets-ee/settings": "0.1.6",
57
- "@murumets-ee/core": "0.1.6",
58
- "@murumets-ee/storage": "0.1.6",
59
- "@murumets-ee/db": "0.1.5",
60
- "@murumets-ee/entity": "0.1.5"
55
+ "@murumets-ee/core": "0.2.0",
56
+ "@murumets-ee/entity": "0.2.1",
57
+ "@murumets-ee/db": "0.2.0",
58
+ "@murumets-ee/logging": "0.2.0",
59
+ "@murumets-ee/storage": "0.2.0",
60
+ "@murumets-ee/settings": "0.2.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "lucide-react": ">=0.400.0",
@@ -1,2 +0,0 @@
1
- import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}},label:`Image processing presets`})}});export{n as imageStylesSettings};
2
- //# sourceMappingURL=image-styles-settings-7K_jPNIA.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"image-styles-settings-7K_jPNIA.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEAaA,MAAa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}