@murumets-ee/media 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/admin.d.mts +19 -16
  2. package/dist/admin.d.mts.map +1 -1
  3. package/dist/admin.mjs +1 -1
  4. package/dist/{client-CHGSsUxS.mjs → client-BcA8CDBk.mjs} +2 -2
  5. package/dist/{client-CHGSsUxS.mjs.map → client-BcA8CDBk.mjs.map} +1 -1
  6. package/dist/client.d.mts +1 -1
  7. package/dist/image-styles-settings.d.mts +4 -5
  8. package/dist/image-styles-settings.d.mts.map +1 -1
  9. package/dist/image-styles.d.mts +2 -3
  10. package/dist/image-styles.d.mts.map +1 -1
  11. package/dist/index.d.mts +1 -1
  12. package/dist/picker.d.mts +6 -7
  13. package/dist/picker.d.mts.map +1 -1
  14. package/dist/plugin-BPabn1pf.mjs +2 -0
  15. package/dist/plugin-BPabn1pf.mjs.map +1 -0
  16. package/dist/plugin.d.mts +9 -3
  17. package/dist/plugin.d.mts.map +1 -1
  18. package/dist/plugin.mjs +1 -1
  19. package/dist/plugin.mjs.map +1 -1
  20. package/dist/processing.d.mts +1 -1
  21. package/dist/query-client.d.mts +1 -1
  22. package/dist/regenerate-variants-DJIFSrdC.mjs +2 -0
  23. package/dist/regenerate-variants-DJIFSrdC.mjs.map +1 -0
  24. package/dist/regenerate-variants-DtA5pKjj.mjs +2 -0
  25. package/dist/regenerate-variants-DtA5pKjj.mjs.map +1 -0
  26. package/dist/{resolve-image-styles-2sa0O7wU.mjs → resolve-image-styles-Dqaa5QsX.mjs} +2 -2
  27. package/dist/{resolve-image-styles-2sa0O7wU.mjs.map → resolve-image-styles-Dqaa5QsX.mjs.map} +1 -1
  28. package/dist/routes-tHvb9cgv.mjs +2 -0
  29. package/dist/routes-tHvb9cgv.mjs.map +1 -0
  30. package/dist/{types-BMW3aeEB.d.mts → types-CqjShq2U.d.mts} +15 -16
  31. package/dist/types-CqjShq2U.d.mts.map +1 -0
  32. package/dist/usage-BwUdYWkA.mjs +2 -0
  33. package/dist/usage-BwUdYWkA.mjs.map +1 -0
  34. package/dist/usage.d.mts +3 -3
  35. package/dist/usage.d.mts.map +1 -1
  36. package/dist/usage.mjs +1 -1
  37. package/dist/usage.mjs.map +1 -1
  38. package/package.json +10 -9
  39. package/dist/plugin-DZaF1BHS.mjs +0 -2
  40. package/dist/plugin-DZaF1BHS.mjs.map +0 -1
  41. package/dist/regenerate-variants-CuHnSFS5.mjs +0 -2
  42. package/dist/regenerate-variants-CuHnSFS5.mjs.map +0 -1
  43. package/dist/regenerate-variants-rNGOuUJc.mjs +0 -2
  44. package/dist/regenerate-variants-rNGOuUJc.mjs.map +0 -1
  45. package/dist/routes-BFRoQhZB.mjs +0 -2
  46. package/dist/routes-BFRoQhZB.mjs.map +0 -1
  47. package/dist/types-BMW3aeEB.d.mts.map +0 -1
  48. package/dist/usage-aqV6Z9d_.mjs +0 -2
  49. package/dist/usage-aqV6Z9d_.mjs.map +0 -1
package/dist/admin.d.mts CHANGED
@@ -2,27 +2,30 @@ import { AdminRoute } from "@murumets-ee/core";
2
2
 
3
3
  //#region src/admin/routes.d.ts
4
4
  /**
5
- * Create admin API routes for media-specific operations.
5
+ * Build admin API routes for media management.
6
6
  *
7
- * Standard entity CRUD (PATCH with translations) is handled by the generic entity
8
- * handler — add `Media` to the `entities` array in your handler config.
7
+ * Returns `AdminRoute[]` from `combineAdminRoutes` — one entry per
8
+ * `(prefix, method)` after grouping. Callers spread the result into
9
+ * their top-level routes list: `routes: [...mediaRoutes()]`.
9
10
  *
10
- * Routes handled by this plugin:
11
- * - `GET /api/admin/media` — List media with search/filter + URLs
12
- * - `GET /api/admin/media/:id` — Get single media item with URL
13
- * - `GET /api/admin/media/:id/usage` — Find entities referencing this media
14
- * - `POST /api/admin/media` — Upload file (FormData with `file` field)
15
- * - `POST /api/admin/media/regenerate-variants` — Regenerate all image variants (admin only)
16
- * - `DELETE /api/admin/media/:id` — Delete media + storage file (blocked if in use)
11
+ * Four `defineAdminRoute` entries:
17
12
  *
18
- * Image style settings live under the generic settings API:
19
- * - `GET /api/admin/settings/media.imageStyles` — Read current styles
20
- * - `PATCH /api/admin/settings/media.imageStyles` — Update styles (Zod-validated)
13
+ * - GET matchAnyPath with `media:view` defaultRoles: VIEW_DEFAULT_ROLES
14
+ * - POST path='' `media:create` defaultRoles: WRITE_DEFAULT_ROLES
15
+ * - POST path='regenerate-variants' `media:create` defaultRoles: WRITE_DEFAULT_ROLES
16
+ * - DELETE matchAnyPath with `media:delete` defaultRoles: WRITE_DEFAULT_ROLES
21
17
  *
22
- * Routes handled by generic entity handler (via fallthrough):
23
- * - `PATCH /api/admin/media/:id` — Update metadata with translation support
18
+ * Permissions are auto-registered by the entity catalog
19
+ * (`buildResourceCatalog` in `@murumets-ee/auth`) AND by
20
+ * `registerPermission` inside `defineAdminRoute`. Idempotent —
21
+ * `registerPermission` unions `defaultRoles` on repeat registration,
22
+ * so the framework's two pathways agree on the final grant set.
23
+ *
24
+ * Standard entity CRUD (PATCH with translations) is NOT registered
25
+ * here — it falls through to the generic entity handler. Add `Media`
26
+ * to the `entities` array in your handler config to enable it.
24
27
  */
25
- declare function mediaRoutes(): AdminRoute;
28
+ declare function mediaRoutes(): AdminRoute[];
26
29
  //#endregion
27
30
  export { mediaRoutes };
28
31
  //# sourceMappingURL=admin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;iBAsagB,WAAA,CAAA,GAAe,UAAA"}
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAslBgB,WAAA,CAAA,GAAe,UAAA"}
package/dist/admin.mjs CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./routes-BFRoQhZB.mjs";export{e as mediaRoutes};
1
+ import{t as e}from"./routes-tHvb9cgv.mjs";export{e as mediaRoutes};
@@ -1,2 +1,2 @@
1
- import"./entity-B75A4L3p.mjs";import{n as e,r as t,t as n}from"./variant-key-CFr3fR-n.mjs";import"server-only";var r=class{admin;storage;imageStyles;constructor(e){this.admin=e.admin,this.storage=e.storage,this.imageStyles=e.imageStyles??null}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-2sa0O7wU.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}invalidateImageStylesCache(){this.imageStyles=null}async upload(r,o){let s=await this.storage.upload(r,{filename:o.filename,mimeType:o.mimeType,size:o.size,visibility:o.visibility,uploadedBy:o.uploadedBy}),c=o.width??null,l=o.height??null,u={};if(r instanceof Buffer&&e(o.mimeType))try{let e=await t(r,await this.resolveImageStyles());c=e.width,l=e.height;let i=s.visibility;await Promise.all([...e.variants.entries()].map(async([e,t])=>{let r=n(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:r,filename:`${e}_${o.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:i,metadata:{variantOf:s.key,style:e},uploadedBy:o.uploadedBy}),u[e]=r}catch{}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(()=>{})}catch{}let d=i(o.mimeType);try{return{media:await this.admin.create({title:o.title??a(o.filename),alt:o.alt??null,description:o.description??null,fileKey:s.key,filename:o.filename,mimeType:o.mimeType,size:o.size,width:c,height:l,mediaType:d}),url:await this.storage.getUrl(s.key)}}catch(e){for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(()=>{}),e}}async findById(e,t){return this.admin.findById(e,t)}async findMany(e){let{schemaRegistry:t}=await import(`@murumets-ee/db`),{and:n,asc:r,desc:i,eq:a,ilike:o,or:s,sql:c}=await import(`drizzle-orm`),l=t.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(a(l.mediaType,e.mediaType)),e?.mimeTypePrefix){let t=e.mimeTypePrefix.replace(/[\\%_]/g,`\\$&`);u.push(o(l.mimeType,`${t}%`))}if(e?.search){let t=`%${e.search.replace(/[\\%_]/g,`\\$&`)}%`;u.push(s(o(l.filename,t),c`${l.fields} ->> 'title' ILIKE ${t}`))}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?n(...u):void 0,m=await this.admin.count({where:p}),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?r:i;return{items:await this.admin.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m,limit:d,offset:f}}async update(e,t){return this.admin.update(e,t)}async delete(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);let n=t.fileKey;await this.admin.delete(e);let r=(await this.storage.getMetadata(n))?.metadata?.variants;if(r)for(let e of Object.values(r))await this.storage.delete(e).catch(()=>{});await this.storage.delete(n).catch(()=>{})}async getUrl(e){let t=await this.admin.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{schemaRegistry:t}=await import(`@murumets-ee/db`),{inArray:n}=await import(`drizzle-orm`),r=t.get(`media`);if(!r)return new Map;let i=await this.admin.findMany({where:n(r.id,e),limit:e.length}),a=new Map;return await Promise.all(i.map(async e=>{let t=await this.storage.getUrl(e.fileKey);a.set(e.id,t)})),a}async getVariantUrl(e,t){let r=await this.admin.findById(e);if(!r)return null;let i=r.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=n(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{schemaRegistry:r}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=r.get(`media`);if(!a)return new Map;let o=await this.admin.findMany({where:i(a.id,e),limit:e.length}),s=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(o.map(async e=>{if(s){let r=n(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(r);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};function i(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 a(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{r as MediaClient};
2
- //# sourceMappingURL=client-CHGSsUxS.mjs.map
1
+ import"./entity-B75A4L3p.mjs";import{n as e,r as t,t as n}from"./variant-key-CFr3fR-n.mjs";import"server-only";var r=class{admin;storage;imageStyles;constructor(e){this.admin=e.admin,this.storage=e.storage,this.imageStyles=e.imageStyles??null}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-Dqaa5QsX.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}invalidateImageStylesCache(){this.imageStyles=null}async upload(r,o){let s=await this.storage.upload(r,{filename:o.filename,mimeType:o.mimeType,size:o.size,visibility:o.visibility,uploadedBy:o.uploadedBy}),c=o.width??null,l=o.height??null,u={};if(r instanceof Buffer&&e(o.mimeType))try{let e=await t(r,await this.resolveImageStyles());c=e.width,l=e.height;let i=s.visibility;await Promise.all([...e.variants.entries()].map(async([e,t])=>{let r=n(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:r,filename:`${e}_${o.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:i,metadata:{variantOf:s.key,style:e},uploadedBy:o.uploadedBy}),u[e]=r}catch{}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(()=>{})}catch{}let d=i(o.mimeType);try{return{media:await this.admin.create({title:o.title??a(o.filename),alt:o.alt??null,description:o.description??null,fileKey:s.key,filename:o.filename,mimeType:o.mimeType,size:o.size,width:c,height:l,mediaType:d}),url:await this.storage.getUrl(s.key)}}catch(e){for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(()=>{}),e}}async findById(e,t){return this.admin.findById(e,t)}async findMany(e){let{schemaRegistry:t}=await import(`@murumets-ee/db`),{and:n,asc:r,desc:i,eq:a,ilike:o,or:s,sql:c}=await import(`drizzle-orm`),l=t.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(a(l.mediaType,e.mediaType)),e?.mimeTypePrefix){let t=e.mimeTypePrefix.replace(/[\\%_]/g,`\\$&`);u.push(o(l.mimeType,`${t}%`))}if(e?.search){let t=`%${e.search.replace(/[\\%_]/g,`\\$&`)}%`;u.push(s(o(l.filename,t),c`${l.fields} ->> 'title' ILIKE ${t}`))}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?n(...u):void 0,m=await this.admin.count({where:p}),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?r:i;return{items:await this.admin.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m,limit:d,offset:f}}async update(e,t){return this.admin.update(e,t)}async delete(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);let n=t.fileKey;await this.admin.delete(e);let r=(await this.storage.getMetadata(n))?.metadata?.variants;if(r)for(let e of Object.values(r))await this.storage.delete(e).catch(()=>{});await this.storage.delete(n).catch(()=>{})}async getUrl(e){let t=await this.admin.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{schemaRegistry:t}=await import(`@murumets-ee/db`),{inArray:n}=await import(`drizzle-orm`),r=t.get(`media`);if(!r)return new Map;let i=await this.admin.findMany({where:n(r.id,e),limit:e.length}),a=new Map;return await Promise.all(i.map(async e=>{let t=await this.storage.getUrl(e.fileKey);a.set(e.id,t)})),a}async getVariantUrl(e,t){let r=await this.admin.findById(e);if(!r)return null;let i=r.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=n(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{schemaRegistry:r}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=r.get(`media`);if(!a)return new Map;let o=await this.admin.findMany({where:i(a.id,e),limit:e.length}),s=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(o.map(async e=>{if(s){let r=n(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(r);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};function i(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 a(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{r as MediaClient};
2
+ //# sourceMappingURL=client-BcA8CDBk.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-CHGSsUxS.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 { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\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\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\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 admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\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 } catch {\n // Variant upload failure is non-fatal — original is saved\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(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.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 })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\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(() => {\n // Storage rollback failure is already being handled — propagate original error\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 return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, 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 // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\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 via AdminClient\n const total = await this.admin.count({ 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 this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\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 return this.admin.update(id, data)\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 record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.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(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already 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 record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.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 { 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 this.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.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 record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.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 { 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 this.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.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 { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (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 const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-BcA8CDBk.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 { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\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\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\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 admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\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 } catch {\n // Variant upload failure is non-fatal — original is saved\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(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.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 })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\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(() => {\n // Storage rollback failure is already being handled — propagate original error\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 return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, 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 // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\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 via AdminClient\n const total = await this.admin.count({ 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 this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\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 return this.admin.update(id, data)\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 record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.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(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already 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 record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.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 { 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 this.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.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 record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.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 { 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 this.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.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 { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (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 const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MediaRecord, c as MediaUploadResult, l as Media, n as MediaListOptions, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BMW3aeEB.mjs";
1
+ import { a as MediaRecord, c as MediaUploadResult, l as Media, n as MediaListOptions, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-CqjShq2U.mjs";
2
2
  import { AdminClient } from "@murumets-ee/entity/admin";
3
3
  import { StorageClient } from "@murumets-ee/storage";
4
4
 
@@ -1,5 +1,4 @@
1
- import { t as ImageStyle } from "./types-BMW3aeEB.mjs";
2
- import * as _$_murumets_ee_settings0 from "@murumets-ee/settings";
1
+ import { t as ImageStyle } from "./types-CqjShq2U.mjs";
3
2
 
4
3
  //#region src/image-styles-settings.d.ts
5
4
  /**
@@ -16,9 +15,9 @@ import * as _$_murumets_ee_settings0 from "@murumets-ee/settings";
16
15
  * that uses the plugin without opting in.
17
16
  */
18
17
  declare const defaultImageStyles: Record<string, ImageStyle>;
19
- declare const imageStylesSettings: _$_murumets_ee_settings0.SettingsDefinition<{
20
- readonly imageStyles: _$_murumets_ee_settings0.JsonSettingConfig<Record<string, ImageStyle>>;
21
- }>;
18
+ declare const imageStylesSettings: import("@murumets-ee/settings").SettingsDefinition<{
19
+ readonly imageStyles: import("@murumets-ee/settings").JsonSettingConfig<Record<string, ImageStyle>>;
20
+ }, "media.imageStyles">;
22
21
  //#endregion
23
22
  export { defaultImageStyles, imageStylesSettings };
24
23
  //# 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":";;;;;;;;;;;;;;;;;cA+Da,kBAAA,EAAoB,MAAA,SAAe,UAAA;AAAA,cAOnC,mBAAA,2BAAmB,kBAAA;EAAA"}
1
+ {"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA+Da,kBAAA,EAAoB,MAAA,SAAe,UAAA;AAAA,cAOnC,mBAAA,kCAAmB,kBAAA;EAAA,wEAe9B,MAAA,SAAA,UAAA;AAAA"}
@@ -1,5 +1,4 @@
1
1
 
2
- import * as _$react_jsx_runtime0 from "react/jsx-runtime";
3
2
  //#region src/types.d.ts
4
3
  /** Named image processing preset — defines how to resize/convert an image variant */
5
4
  interface ImageStyle {
@@ -85,7 +84,7 @@ declare function ImageStylesManager({
85
84
  error,
86
85
  labels: userLabels,
87
86
  classNames
88
- }: ImageStylesManagerProps): _$react_jsx_runtime0.JSX.Element;
87
+ }: ImageStylesManagerProps): import("react/jsx-runtime").JSX.Element;
89
88
  //#endregion
90
89
  //#region src/image-styles/regenerate-variants-action.d.ts
91
90
  /**
@@ -98,7 +97,7 @@ declare function ImageStylesManager({
98
97
  declare function RegenerateVariantsAction({
99
98
  apiBasePath,
100
99
  labels: userLabels
101
- }: RegenerateVariantsActionProps): _$react_jsx_runtime0.JSX.Element;
100
+ }: RegenerateVariantsActionProps): import("react/jsx-runtime").JSX.Element;
102
101
  //#endregion
103
102
  export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps, RegenerateVariantsAction, type RegenerateVariantsActionLabels, type RegenerateVariantsActionProps };
104
103
  //# sourceMappingURL=image-styles.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;;UA2EiB,UAAA;EClCG;EDoClB,KAAA;ECpCW;EDsCX,MAAA;EClCA;EDoCA,GAAA;EClCA;EDoCA,MAAA;ECpCyC;EDsCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EAtBf;EAwBA,KAAA,EAAO,MAAA,SAAe,UAAA;EAtBtB;EAwBA,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EAtBjC;EAwBA,KAAA;EAvBE;EAyBF,MAAA,GAAS,wBAAA;EAtBM;EAwBf,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,YAAA;AAAA;AAAA,UAGe,6BAAA;EA3Bf;EA6BA,WAAA;EA7BO;EA+BP,MAAA,GAAS,8BAAA;AAAA;;;;;;;ADgBX;;iBEjBgB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,GAAuB,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;AFW1B;;;iBG9CgB,wBAAA,CAAA;EACd,WAAA;EACA,MAAA,EAAQ;AAAA,GACP,6BAAA,GAA6B,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;UA2EiB,UAAA;EClCkB;EDoCjC,KAAA;EClCA;EDoCA,MAAA;EClCS;EDoCT,GAAA;EClCa;EDoCb,MAAA;ECpCyC;EDsCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EArBf;EAuBA,KAAA,EAAO,MAAA,SAAe,UAAA;EArBtB;EAuBA,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EArBjC;EAuBA,KAAA;EAvBE;EAyBF,MAAA,GAAS,wBAAA;EAtBkC;EAwB3C,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,YAAA;AAAA;AAAA,UAGe,6BAAA;EA3BR;EA6BP,WAAA;EApBe;EAsBf,MAAA,GAAS,8BAAA;AAAA;;;;;;ADgBX;;;iBEjBgB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,+BAAuB,GAAA,CAAA,OAAA;;;;;;AFW1B;;;;iBG9CgB,wBAAA,CAAA;EACd,WAAA;EACA,MAAA,EAAQ;AAAA,GACP,6BAAA,+BAA6B,GAAA,CAAA,OAAA"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, l as Media, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BMW3aeEB.mjs";
1
+ import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, l as Media, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-CqjShq2U.mjs";
2
2
  import { defaultImageStyles, imageStylesSettings } from "./image-styles-settings.mjs";
3
3
  //#region src/enrich.d.ts
4
4
  /**
package/dist/picker.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
 
2
- import * as _$react_jsx_runtime0 from "react/jsx-runtime";
3
2
  import { ReactNode } from "react";
4
3
 
5
4
  //#region src/picker/types.d.ts
@@ -131,7 +130,7 @@ declare function MediaCard({
131
130
  isSelected,
132
131
  onToggle,
133
132
  classNames
134
- }: MediaCardProps): _$react_jsx_runtime0.JSX.Element;
133
+ }: MediaCardProps): import("react/jsx-runtime").JSX.Element;
135
134
  //#endregion
136
135
  //#region src/picker/media-grid.d.ts
137
136
  interface MediaGridProps {
@@ -155,7 +154,7 @@ declare function MediaGrid({
155
154
  limit,
156
155
  onPageChange,
157
156
  classNames
158
- }: MediaGridProps): _$react_jsx_runtime0.JSX.Element;
157
+ }: MediaGridProps): import("react/jsx-runtime").JSX.Element;
159
158
  //#endregion
160
159
  //#region src/picker/media-picker.d.ts
161
160
  declare function MediaPicker({
@@ -172,7 +171,7 @@ declare function MediaPicker({
172
171
  classNames,
173
172
  className,
174
173
  children
175
- }: MediaPickerProps): _$react_jsx_runtime0.JSX.Element;
174
+ }: MediaPickerProps): import("react/jsx-runtime").JSX.Element;
176
175
  //#endregion
177
176
  //#region src/picker/provider.d.ts
178
177
  interface MediaPickerProviderProps extends MediaPickerCallbacks {
@@ -196,7 +195,7 @@ declare function MediaPickerProvider({
196
195
  children,
197
196
  fetchMedia,
198
197
  uploadMedia
199
- }: MediaPickerProviderProps): _$react_jsx_runtime0.JSX.Element;
198
+ }: MediaPickerProviderProps): import("react/jsx-runtime").JSX.Element;
200
199
  declare function useMediaPicker(): MediaPickerCallbacks;
201
200
  //#endregion
202
201
  //#region src/picker/search-bar.d.ts
@@ -216,7 +215,7 @@ declare function SearchBar({
216
215
  onMediaTypeChange,
217
216
  locked,
218
217
  classNames
219
- }: SearchBarProps): _$react_jsx_runtime0.JSX.Element;
218
+ }: SearchBarProps): import("react/jsx-runtime").JSX.Element;
220
219
  //#endregion
221
220
  //#region src/picker/upload-zone.d.ts
222
221
  interface UploadZoneProps {
@@ -230,7 +229,7 @@ declare function UploadZone({
230
229
  isUploading,
231
230
  accept,
232
231
  classNames
233
- }: UploadZoneProps): _$react_jsx_runtime0.JSX.Element;
232
+ }: UploadZoneProps): import("react/jsx-runtime").JSX.Element;
234
233
  //#endregion
235
234
  export { type AdminMediaCallbacks, MediaCard, MediaGrid, MediaPicker, type MediaPickerCallbacks, type MediaPickerClassNames, type MediaPickerItem, type MediaPickerListResult, type MediaPickerMode, type MediaPickerProps, MediaPickerProvider, SearchBar, UploadZone, createAdminMediaCallbacks, useMediaPicker };
236
235
  //# sourceMappingURL=picker.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/types.ts","../src/picker/admin-callbacks.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":"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"}
1
+ {"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/types.ts","../src/picker/admin-callbacks.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import"./routes-tHvb9cgv.mjs";import"./entity-B75A4L3p.mjs";import"./image-styles-settings-B4QOt7Ve.mjs";import"@murumets-ee/core";import"@murumets-ee/media/image-styles";function e(){throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`)}export{e as getMediaConfig};
2
+ //# sourceMappingURL=plugin-BPabn1pf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-BPabn1pf.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Media plugin — auto-registers the Media entity, mounts admin routes, and\n * contributes sidebar + default-route scaffolding for the admin shell.\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [\n * storage(),\n * media({ maxUploadSize: 10 * 1024 * 1024 }),\n * ],\n * })\n * ```\n */\n\nimport { definePlugin } from '@murumets-ee/core'\n// Self-reference the `./image-styles` subpath (not the internal source\n// files) so the plugin bundle externalizes the React components instead\n// of inlining them — the subpath is built separately with a `'use client'`\n// banner, and Next.js needs to see that boundary preserved.\nimport { ImageStylesManager, RegenerateVariantsAction } from '@murumets-ee/media/image-styles'\nimport { mediaRoutes } from './admin/routes.js'\nimport { Media } from './entity.js'\nimport { imageStylesSettings } from './image-styles-settings.js'\nimport type { MediaPluginConfig } from './types.js'\n\nlet _mediaConfig: Required<MediaPluginConfig> | null = null\n\n/**\n * Get the resolved media plugin configuration.\n * Throws if plugin not initialized.\n */\nexport function getMediaConfig(): Required<MediaPluginConfig> {\n if (!_mediaConfig) {\n throw new Error('@murumets-ee/media plugin not initialized. Add media() to your plugins array.')\n }\n return _mediaConfig\n}\n\n/**\n * Media plugin factory.\n *\n * - Registers the Media entity\n * - Mounts the media admin API routes\n * - Contributes sidebar + default-route metadata\n * - Validates that @murumets-ee/storage + @murumets-ee/settings are registered at init\n * - Captures resolved configuration for `getMediaConfig()`\n */\nexport function media(config?: MediaPluginConfig) {\n const resolvedConfig: Required<MediaPluginConfig> = {\n acceptedTypes: config?.acceptedTypes ?? ['image/*', 'video/*', 'audio/*', 'application/pdf'],\n maxUploadSize: config?.maxUploadSize ?? 50 * 1024 * 1024,\n defaultVisibility: config?.defaultVisibility ?? 'public',\n imageStyles: config?.imageStyles ?? {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n }\n\n return definePlugin({\n name: '@murumets-ee/media',\n shared: {\n // Self-contributes the media.imageStyles namespace. The merge engine\n // auto-derives the permission resource (`settings_media.imageStyles`)\n // and the sidebar entry under the \"Settings\" group. The app's\n // admin-api-handler call site aggregates plugin-contributed\n // namespaces into `settingsRoutes(...)` (see admin-playground's\n // `getAllSettings()` for the canonical pattern); apps that follow\n // the scaffold pattern get this for free.\n // Drop the `as PluginSettingsDefinition` cast — it would widen the\n // namespace literal (e.g. `'media.imageStyles'`) to `string`,\n // leaking `settings_${string}:view|update` into the resolved\n // permission union via `<const N>` capture loss.\n // `imageStylesSettings` is structurally a superset of\n // `PluginSettingsDefinition` (full vs. narrow `SettingsDefinition`),\n // which TS accepts directly. The `readonly` outer tuple comes from\n // the parent definePlugin's `<const S>` capture.\n settings: [imageStylesSettings],\n },\n server: {\n entities: [Media],\n routes: [...mediaRoutes()],\n init: async (app) => {\n if (!app.plugins.has('@murumets-ee/storage')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/storage plugin. ' +\n 'Add storage() before media() in your plugins array.',\n )\n }\n\n if (!app.plugins.has('@murumets-ee/settings')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/settings plugin. ' +\n 'Add settings() before media() in your plugins array.',\n )\n }\n\n _mediaConfig = resolvedConfig\n\n // Image style defaults live in the plugin config and are exposed via\n // `getMediaConfig()`. Persistent overrides go to the settings DB\n // through the generic settings API at `/api/admin/settings/media.imageStyles`\n // — see `image-styles-settings.ts`. `resolveImageStyles` reads\n // DB-first, config-fallback.\n //\n // We deliberately do NOT seed the DB here. Init must stay lightweight:\n // it runs in every context (Next.js server, Next.js build, CLI commands).\n // The prior eager-seed path pulled in `@murumets-ee/settings`'s main entry —\n // which carries `import 'server-only'` — and crashed non-RSC Node bootstrap.\n\n app.logger.info(\n {\n acceptedTypes: resolvedConfig.acceptedTypes,\n maxUploadSize: resolvedConfig.maxUploadSize,\n defaultVisibility: resolvedConfig.defaultVisibility,\n },\n 'Media plugin initialized',\n )\n },\n },\n adminUi: {\n sidebar: [\n {\n id: 'media',\n group: 'Library',\n label: 'Media',\n href: '/admin/media',\n iconName: 'image',\n },\n ],\n defaultRoutes: [\n {\n path: 'media',\n factory: 'MediaListPage',\n nav: { label: 'Media', iconName: 'image', group: 'Library' },\n },\n {\n path: 'media/[id]',\n factory: 'MediaEditPage',\n },\n ],\n // Self-contributes the rich image-styles editor and the\n // \"Regenerate All Variants\" action. Apps don't have to wire either\n // — they appear automatically on /admin/settings/media.imageStyles\n // (route + sidebar entry auto-derived from `shared.settings`).\n settingRenderers: {\n 'media.imageStyles': ImageStylesManager,\n },\n settingsActions: {\n 'media.imageStyles': RegenerateVariantsAction,\n },\n },\n })\n}\n"],"mappings":"2KAkCA,SAAgB,GAA8C,CAE1D,MAAU,MAAM,gFAAgF"}
package/dist/plugin.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { i as MediaPluginConfig } from "./types-BMW3aeEB.mjs";
2
- import { Plugin } from "@murumets-ee/core";
1
+ import { i as MediaPluginConfig, t as ImageStyle } from "./types-CqjShq2U.mjs";
3
2
 
4
3
  //#region src/plugin.d.ts
5
4
  /**
@@ -16,7 +15,14 @@ declare function getMediaConfig(): Required<MediaPluginConfig>;
16
15
  * - Validates that @murumets-ee/storage + @murumets-ee/settings are registered at init
17
16
  * - Captures resolved configuration for `getMediaConfig()`
18
17
  */
19
- declare function media(config?: MediaPluginConfig): Plugin;
18
+ declare function media(config?: MediaPluginConfig): Pick<import("@murumets-ee/core").Plugin, "server" | "adminUi" | "postInstall"> & {
19
+ readonly name: "@murumets-ee/media";
20
+ readonly shared?: {
21
+ readonly settings: readonly [import("@murumets-ee/settings").SettingsDefinition<{
22
+ readonly imageStyles: import("@murumets-ee/settings").JsonSettingConfig<Record<string, ImageStyle>>;
23
+ }, "media.imageStyles">];
24
+ };
25
+ };
20
26
  //#endregion
21
27
  export { getMediaConfig, media };
22
28
  //# sourceMappingURL=plugin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;;iBAkCgB,cAAA,CAAA,GAAkB,QAAA,CAAS,iBAAA;;;;;;;;;;iBAgB3B,KAAA,CAAM,MAAA,GAAS,iBAAA,GAAoB,MAAA"}
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;iBAkCgB,cAAA,CAAA,GAAkB,QAAA,CAAS,iBAAA;;;;;;;;;;iBAgB3B,KAAA,CAAM,MAAA,GAAS,iBAAA,GAAiB,IAAA,6BAAA,MAAA;EAAA;;;6FAAA,UAAA;IAAA;EAAA;AAAA"}
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./entity-D1YaFEh8.mjs";import{imageStylesSettings as t}from"./image-styles-settings.mjs";import{ImageStylesManager as n,RegenerateVariantsAction as r}from"@murumets-ee/media/image-styles";const i=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function a(e){return e!==void 0&&i.test(e)}let o=null;async function s(){return o||=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),r=e();return t(n(),{app:r})})(),o}async function c(){let{createAdminClient:e}=await import(`@murumets-ee/core/clients`),{MediaClient:t}=await import(`./client.mjs`),{Media:n}=await import(`./entity-D1YaFEh8.mjs`).then(e=>e.n),r=await s();return new t({admin:e(n),storage:r})}function l(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function u(e,t){return l({error:e},t)}async function d(e,{segments:t}){let{isStorageConfigured:n,getStorageConfigReason:r}=await import(`@murumets-ee/storage`);if(t.length===2&&t[1]===`usage`){let e=t[0];if(!a(e))return u(`Invalid media ID format`,400);let{findMediaUsages:n}=await import(`./usage.mjs`),{getApp:r}=await import(`@murumets-ee/core`);return l({usages:await n(e,r().db.readWrite)})}let i=t.length>0?t[0]:void 0;if(i!==void 0&&!a(i))return u(`Invalid media ID format`,400);if(!n()){let e=r()??`Storage not configured`;return t.length>0?l({error:e,configured:!1,reason:e},503):l({items:[],total:0,configured:!1,reason:e})}let o=await c();if(i!==void 0){let e=await o.findById(i);if(!e)return u(`Media not found`,404);let t=await o.getUrl(i);return l({...e,url:t})}let s=new URL(e.url),d=s.searchParams.get(`search`)??void 0,f=s.searchParams.get(`mediaType`)??void 0,p=Math.min(Math.max(Number(s.searchParams.get(`limit`))||24,1),100),m=Math.max(Number(s.searchParams.get(`offset`))||0,0),h=await o.findMany({...d!==void 0&&{search:d},...f!==void 0&&{mediaType:f},limit:p,offset:m}),g=h.items.map(e=>e.id),[_,v]=await Promise.all([o.getUrls(g),o.getVariantUrls(g,`thumbnail`)]);return l({items:h.items.map(e=>{let t=v.get(e.id);return{id:e.id,title:e.title??null,alt:e.alt??null,filename:e.filename,mimeType:e.mimeType,size:e.size,mediaType:e.mediaType,url:_.get(e.id)??``,...t!==void 0&&{thumbnailUrl:t},width:e.width??null,height:e.height??null}}),total:h.total})}async function f(e,{segments:t,user:n,audit:r,checkPermission:i}){let{isStorageConfigured:a,getStorageConfigReason:o}=await import(`@murumets-ee/storage`);if(t.length===1&&t[0]===`regenerate-variants`){if(!i(`media`,`create`))return u(`Forbidden: media create permission required for variant regeneration`,403);if(!a())return u(o()??`Storage not configured`,503);let{regenerateAllVariants:e}=await import(`./regenerate-variants-rNGOuUJc.mjs`),{getApp:t,getContext:s}=await import(`@murumets-ee/core`),{resolveImageStyles:c}=await import(`./resolve-image-styles-BI3pvJBZ.mjs`),{createStorageClient:d}=await import(`@murumets-ee/storage`),{getStorageConfig:f}=await import(`@murumets-ee/storage/plugin`),p=t(),m=await c(p,p.logger);if(!m||Object.keys(m).length===0)return u(`No image styles configured`,400);let h=d(f(),{app:p}),g=await e({db:p.db.readWrite,storage:h,logger:p.logger.child({media:!0}),styles:m,contextResolver:()=>{let e=s();if(!(!e?.user||!e?.checker))return{user:e.user,checker:e.checker,...e.scope!==void 0&&{scope:e.scope}}}});return r?.({action:`media.regenerate_variants`,userId:n.id,...n.name!==void 0&&{userName:n.name},metadata:{total:g.total,processed:g.processed,errors:g.errors}}),l(g)}if(!i(`media`,`create`))return u(`Forbidden: media create permission required for upload`,403);if(!a())return u(o()??`Storage not configured`,503);let s=await c(),d=(await e.formData()).get(`file`);if(!d||d.size===0)return u(`No file provided`,400);if(d.size>50*1024*1024)return u(`File too large: ${(d.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let f=Buffer.from(await d.arrayBuffer()),{detectMimeType:p}=await import(`@murumets-ee/storage`),{mimeType:m,mismatch:h}=await p(f,d.type||`application/octet-stream`);if(h)return u(`File content doesn't match declared type: claimed ${d.type}, detected ${m}`,400);let g=await s.upload(f,{filename:d.name,mimeType:m,size:d.size,uploadedBy:n.id}),_={id:g.media.id,title:g.media.title??null,alt:g.media.alt??null,filename:g.media.filename,mimeType:g.media.mimeType,size:g.media.size,mediaType:g.media.mediaType,url:g.url,width:g.media.width??null,height:g.media.height??null};return r?.({action:`media.upload`,entityType:`media`,entityId:g.media.id,userId:n.id,...n.name!==void 0&&{userName:n.name},changes:{filename:g.media.filename,mimeType:g.media.mimeType,size:g.media.size,mediaType:g.media.mediaType}}),l(_,201)}async function p(e,{segments:t,user:n,audit:r,checkPermission:i}){if(!i(`media`,`delete`))return u(`Forbidden: media delete permission required`,403);if(t.length===0)return u(`Media ID required`,400);let o=t[0];if(!a(o))return u(`Invalid media ID format`,400);let{isStorageConfigured:s,getStorageConfigReason:d}=await import(`@murumets-ee/storage`);return s()?(await(await c()).delete(o),r?.({action:`media.delete`,entityType:`media`,entityId:o,userId:n.id,...n.name!==void 0&&{userName:n.name}}),l({deleted:1})):u(d()??`Storage not configured`,503)}function m(){return{prefix:`media`,resource:`media`,actions:[`view`,`create`,`update`,`delete`],handlers:{GET:d,POST:f,DELETE:p}}}let h=null;function g(){if(!h)throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`);return h}function _(i){let a={acceptedTypes:i?.acceptedTypes??[`image/*`,`video/*`,`audio/*`,`application/pdf`],maxUploadSize:i?.maxUploadSize??50*1024*1024,defaultVisibility:i?.defaultVisibility??`public`,imageStyles:i?.imageStyles??{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}}};return{name:`@murumets-ee/media`,shared:{settings:[t]},server:{entities:[e],routes:[m()],init:async e=>{if(!e.plugins.has(`@murumets-ee/storage`))throw Error(`@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.`);if(!e.plugins.has(`@murumets-ee/settings`))throw Error(`@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.`);h=a,e.logger.info({acceptedTypes:a.acceptedTypes,maxUploadSize:a.maxUploadSize,defaultVisibility:a.defaultVisibility},`Media plugin initialized`)}},adminUi:{sidebar:[{id:`media`,group:`Library`,label:`Media`,href:`/admin/media`,iconName:`image`}],defaultRoutes:[{path:`media`,factory:`MediaListPage`,nav:{label:`Media`,iconName:`image`,group:`Library`}},{path:`media/[id]`,factory:`MediaEditPage`}],settingRenderers:{"media.imageStyles":n},settingsActions:{"media.imageStyles":r}}}}export{g as getMediaConfig,_ as media};
1
+ import{t as e}from"./entity-D1YaFEh8.mjs";import{imageStylesSettings as t}from"./image-styles-settings.mjs";import{combineAdminRoutes as n,defineAdminRoute as r,definePlugin as i,safeAudit as a}from"@murumets-ee/core";import{ImageStylesManager as o,RegenerateVariantsAction as s}from"@murumets-ee/media/image-styles";const c=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function l(e){return e!==void 0&&c.test(e)}const u=[`image`,`video`,`audio`,`document`,`other`];function d(e){return u.includes(e)}function f(e,t){if(e===null||!/^\d+$/.test(e))return t;let n=Number(e);return Number.isSafeInteger(n)?n:t}function p(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function m(e,t){return p({error:e},t)}const h={clientPromise:null};async function g(){if(!h.clientPromise){let e=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),r=e();return t(n(),{app:r})})();e.catch(()=>{h.clientPromise===e&&(h.clientPromise=null)}),h.clientPromise=e}return h.clientPromise}async function _(){let{createAdminClient:e}=await import(`@murumets-ee/core/clients`),{MediaClient:t}=await import(`./client.mjs`),{Media:n}=await import(`./entity-D1YaFEh8.mjs`).then(e=>e.n),r=await g();return new t({admin:e(n),storage:r})}const v=async(e,t)=>{let{isStorageConfigured:n,getStorageConfigReason:r}=await import(`@murumets-ee/storage`),i=t.segments;if(i.length===2&&i[1]===`usage`){let e=i[0];if(!l(e))return m(`Invalid media ID format`,400);let{findMediaUsages:t}=await import(`./usage.mjs`),{getApp:n}=await import(`@murumets-ee/core`);return p({usages:await t(e,n())})}if(i.length>=2)return m(`Not found`,404);let a=i.length>0?i[0]:void 0;if(a!==void 0&&!l(a))return m(`Invalid media ID format`,400);if(!n()){let e=r()??`Storage not configured`;return i.length>0?p({error:e,configured:!1,reason:e},503):p({items:[],total:0,configured:!1,reason:e})}let o=await _();if(a!==void 0){let e=await o.findById(a);if(!e)return m(`Media not found`,404);let t=await o.getUrl(a);return p({...e,url:t})}let s=new URL(e.url),c=s.searchParams.get(`search`)??void 0,u=s.searchParams.get(`mediaType`),h=u!==null&&d(u)?u:void 0,g=Math.min(Math.max(f(s.searchParams.get(`limit`),24),1),100),v=Math.min(f(s.searchParams.get(`offset`),0),1e5),y=await o.findMany({...c!==void 0&&{search:c},...h!==void 0&&{mediaType:h},limit:g,offset:v}),b=y.items.map(e=>e.id),[x,S]=await Promise.all([o.getUrls(b),o.getVariantUrls(b,`thumbnail`)]);return p({items:y.items.map(e=>{let t=S.get(e.id);return{id:e.id,title:e.title??null,alt:e.alt??null,filename:e.filename,mimeType:e.mimeType,size:e.size,mediaType:e.mediaType,url:x.get(e.id)??``,...t!==void 0&&{thumbnailUrl:t},width:e.width??null,height:e.height??null}}),total:y.total})},y=async(e,t)=>{let{isStorageConfigured:n,getStorageConfigReason:r,detectMimeType:i}=await import(`@murumets-ee/storage`);if(t.segments.length>0)return m(`Not found`,404);if(!n())return m(r()??`Storage not configured`,503);let o=await _(),s=(await e.formData()).get(`file`);if(!(s instanceof File)||s.size===0)return m(`No file provided`,400);if(s.size>50*1024*1024)return m(`File too large: ${(s.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let c=Buffer.from(await s.arrayBuffer()),{mimeType:l,mismatch:u}=await i(c,s.type||`application/octet-stream`);if(u)return m(`File content doesn't match declared type: claimed ${s.type}, detected ${l}`,400);let d=await o.upload(c,{filename:s.name,mimeType:l,size:s.size,uploadedBy:t.user.id}),f={id:d.media.id,title:d.media.title??null,alt:d.media.alt??null,filename:d.media.filename,mimeType:d.media.mimeType,size:d.media.size,mediaType:d.media.mediaType,url:d.url,width:d.media.width??null,height:d.media.height??null};return a(t,{action:`media.upload`,entityType:`media`,entityId:d.media.id,userId:t.user.id,...t.user.name!==void 0&&{userName:t.user.name},changes:{filename:d.media.filename,mimeType:d.media.mimeType,size:d.media.size,mediaType:d.media.mediaType}}),p(f,201)},b=async(e,t)=>{let{isStorageConfigured:n,getStorageConfigReason:r}=await import(`@murumets-ee/storage`);if(t.segments.length!==1)return m(`Not found`,404);if(!n())return m(r()??`Storage not configured`,503);let{regenerateAllVariants:i}=await import(`./regenerate-variants-DJIFSrdC.mjs`),{getApp:o,getContext:s}=await import(`@murumets-ee/core`),{resolveImageStyles:c}=await import(`./resolve-image-styles-BI3pvJBZ.mjs`),{createStorageClient:l}=await import(`@murumets-ee/storage`),{getStorageConfig:u}=await import(`@murumets-ee/storage/plugin`),d=o(),f=await c(d,d.logger);if(!f||Object.keys(f).length===0)return m(`No image styles configured`,400);let h=await i({app:d,storage:l(u(),{app:d}),logger:d.logger.child({media:!0}),styles:f,contextResolver:()=>{let e=s();if(!(!e?.user||!e?.checker))return{user:e.user,checker:e.checker,...e.scope!==void 0&&{scope:e.scope}}}});return a(t,{action:`media.regenerate_variants`,userId:t.user.id,...t.user.name!==void 0&&{userName:t.user.name},metadata:{total:h.total,processed:h.processed,errors:h.errors}}),p(h)},x=async(e,t)=>{let n=t.segments;if(n.length===0)return m(`Media ID required`,400);if(n.length>1)return m(`Not found`,404);let r=n[0];if(!l(r))return m(`Invalid media ID format`,400);let{isStorageConfigured:i,getStorageConfigReason:o}=await import(`@murumets-ee/storage`);return i()?(await(await _()).delete(r),a(t,{action:`media.delete`,entityType:`media`,entityId:r,userId:t.user.id,...t.user.name!==void 0&&{userName:t.user.name}}),p({deleted:1})):m(o()??`Storage not configured`,503)},S=[`admin`,`editor`,`agent`,`viewer`],C=[`admin`,`editor`];function w(){return n([r({prefix:`media`,path:``,method:`GET`,permission:`media:view`,defaultRoles:S,matchAnyPath:!0,description:`Read media — list, single item, or referencing-entity usage report.`,handler:v}),r({prefix:`media`,path:``,method:`POST`,permission:`media:create`,defaultRoles:C,description:"Upload a media file (multipart FormData with `file` field).",handler:y}),r({prefix:`media`,path:`regenerate-variants`,method:`POST`,permission:`media:create`,defaultRoles:C,description:`Regenerate every image-style variant for every media row.`,handler:b}),r({prefix:`media`,path:``,method:`DELETE`,permission:`media:delete`,defaultRoles:C,matchAnyPath:!0,description:`Delete a media record + its storage object.`,handler:x})])}let T=null;function E(){if(!T)throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`);return T}function D(n){let r={acceptedTypes:n?.acceptedTypes??[`image/*`,`video/*`,`audio/*`,`application/pdf`],maxUploadSize:n?.maxUploadSize??50*1024*1024,defaultVisibility:n?.defaultVisibility??`public`,imageStyles:n?.imageStyles??{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}}};return i({name:`@murumets-ee/media`,shared:{settings:[t]},server:{entities:[e],routes:[...w()],init:async e=>{if(!e.plugins.has(`@murumets-ee/storage`))throw Error(`@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.`);if(!e.plugins.has(`@murumets-ee/settings`))throw Error(`@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.`);T=r,e.logger.info({acceptedTypes:r.acceptedTypes,maxUploadSize:r.maxUploadSize,defaultVisibility:r.defaultVisibility},`Media plugin initialized`)}},adminUi:{sidebar:[{id:`media`,group:`Library`,label:`Media`,href:`/admin/media`,iconName:`image`}],defaultRoutes:[{path:`media`,factory:`MediaListPage`,nav:{label:`Media`,iconName:`image`,group:`Library`}},{path:`media/[id]`,factory:`MediaEditPage`}],settingRenderers:{"media.imageStyles":o},settingsActions:{"media.imageStyles":s}}})}export{E as getMediaConfig,D as media};
2
2
  //# sourceMappingURL=plugin.mjs.map