@murumets-ee/media 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts +19 -16
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/{client-CHGSsUxS.mjs → client-BcA8CDBk.mjs} +2 -2
- package/dist/{client-CHGSsUxS.mjs.map → client-BcA8CDBk.mjs.map} +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/image-styles-settings.d.mts +4 -5
- package/dist/image-styles-settings.d.mts.map +1 -1
- package/dist/image-styles.d.mts +2 -3
- package/dist/image-styles.d.mts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/picker.d.mts +6 -7
- package/dist/picker.d.mts.map +1 -1
- package/dist/plugin-BPabn1pf.mjs +2 -0
- package/dist/plugin-BPabn1pf.mjs.map +1 -0
- package/dist/plugin.d.mts +9 -3
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/processing.d.mts +1 -1
- package/dist/query-client.d.mts +1 -1
- package/dist/regenerate-variants-DJIFSrdC.mjs +2 -0
- package/dist/regenerate-variants-DJIFSrdC.mjs.map +1 -0
- package/dist/regenerate-variants-DtA5pKjj.mjs +2 -0
- package/dist/regenerate-variants-DtA5pKjj.mjs.map +1 -0
- package/dist/{resolve-image-styles-2sa0O7wU.mjs → resolve-image-styles-Dqaa5QsX.mjs} +2 -2
- package/dist/{resolve-image-styles-2sa0O7wU.mjs.map → resolve-image-styles-Dqaa5QsX.mjs.map} +1 -1
- package/dist/routes-tHvb9cgv.mjs +2 -0
- package/dist/routes-tHvb9cgv.mjs.map +1 -0
- package/dist/{types-BMW3aeEB.d.mts → types-CqjShq2U.d.mts} +15 -16
- package/dist/types-CqjShq2U.d.mts.map +1 -0
- package/dist/usage-BwUdYWkA.mjs +2 -0
- package/dist/usage-BwUdYWkA.mjs.map +1 -0
- package/dist/usage.d.mts +3 -3
- package/dist/usage.d.mts.map +1 -1
- package/dist/usage.mjs +1 -1
- package/dist/usage.mjs.map +1 -1
- package/package.json +10 -9
- package/dist/plugin-DZaF1BHS.mjs +0 -2
- package/dist/plugin-DZaF1BHS.mjs.map +0 -1
- package/dist/regenerate-variants-CuHnSFS5.mjs +0 -2
- package/dist/regenerate-variants-CuHnSFS5.mjs.map +0 -1
- package/dist/regenerate-variants-rNGOuUJc.mjs +0 -2
- package/dist/regenerate-variants-rNGOuUJc.mjs.map +0 -1
- package/dist/routes-BFRoQhZB.mjs +0 -2
- package/dist/routes-BFRoQhZB.mjs.map +0 -1
- package/dist/types-BMW3aeEB.d.mts.map +0 -1
- package/dist/usage-aqV6Z9d_.mjs +0 -2
- 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
|
-
*
|
|
5
|
+
* Build admin API routes for media management.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - `
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
package/dist/admin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":"
|
|
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-
|
|
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-
|
|
2
|
-
//# sourceMappingURL=client-
|
|
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":"+GAmCA,IAAa,EAAb,KAAyB,CACvB,MACA,QACA,YAEA,YAAY,EAA2B,CACrC,KAAK,MAAQ,EAAO,MACpB,KAAK,QAAU,EAAO,QACtB,KAAK,YAAc,EAAO,aAAe,KAQ3C,MAAc,oBAA0D,CACtE,GAAI,KAAK,YAAa,OAAO,KAAK,YAClC,GAAM,CAAE,UAAW,MAAM,OAAO,qBAC1B,CAAE,sBAAuB,MAAM,OAAO,uCACtC,EAAM,GAAQ,CAEpB,MADA,MAAK,YAAc,MAAM,EAAmB,EAAK,EAAI,OAAO,CACrD,KAAK,YAId,4BAAmC,CACjC,KAAK,YAAc,KAiBrB,MAAM,OACJ,EACA,EAC4B,CAE5B,IAAM,EAAa,MAAM,KAAK,QAAQ,OAAO,EAAM,CACjD,SAAU,EAAQ,SAClB,SAAU,EAAQ,SAClB,KAAM,EAAQ,KACd,WAAY,EAAQ,WACpB,WAAY,EAAQ,WACrB,CAAC,CAGE,EAAQ,EAAQ,OAAS,KACzB,EAAS,EAAQ,QAAU,KACzB,EAAsC,EAAE,CAE9C,GAAI,aAAgB,QAAU,EAAmB,EAAQ,SAAS,CAChE,GAAI,CAEF,IAAM,EAAY,MAAM,EAAa,EAAM,MADtB,KAAK,oBAAoB,CACI,CAGlD,EAAQ,EAAU,MAClB,EAAS,EAAU,OAGnB,IAAM,EAAa,EAAW,WAC9B,MAAM,QAAQ,IACZ,CAAC,GAAG,EAAU,SAAS,SAAS,CAAC,CAAC,IAAI,MAAO,CAAC,EAAW,KAAa,CACpE,IAAM,EAAO,EAAiB,EAAW,IAAK,EAAW,EAAQ,OAAO,CACxE,GAAI,CACF,MAAM,KAAK,QAAQ,OAAO,EAAQ,OAAQ,CACxC,IAAK,EACL,SAAU,GAAG,EAAU,GAAG,EAAQ,WAClC,SAAU,EAAQ,SAClB,KAAM,EAAQ,OAAO,WACrB,aACA,SAAU,CAAE,UAAW,EAAW,IAAK,MAAO,EAAW,CACzD,WAAY,EAAQ,WACrB,CAAC,CACF,EAAY,GAAa,OACnB,IAGR,CACH,CAGG,OAAO,KAAK,EAAY,CAAC,OAAS,GACpC,MAAM,KAAK,QACR,eAAe,EAAW,IAAK,CAC9B,SAAU,CACR,GAAI,EAAW,UAAY,EAAE,CAC7B,SAAU,EACX,CACF,CAAC,CACD,UAAY,GAEX,MAEA,EAMV,IAAM,EAAY,EAAgB,EAAQ,SAAS,CAEnD,GAAI,CAiBF,MAAO,CACL,MAAO,MAjBkB,KAAK,MAAM,OAAO,CAC3C,MAAO,EAAQ,OAAS,EAAY,EAAQ,SAAS,CACrD,IAAK,EAAQ,KAAO,KACpB,YAAa,EAAQ,aAAe,KACpC,QAAS,EAAW,IACpB,SAAU,EAAQ,SAClB,SAAU,EAAQ,SAClB,KAAM,EAAQ,KACd,QACA,SACA,YACD,CAAC,CAOA,IAAA,MAJgB,KAAK,QAAQ,OAAO,EAAW,IAAI,CAKpD,OACM,EAAO,CAId,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAY,CAC3C,MAAM,KAAK,QAAQ,OAAO,EAAK,CAAC,UAAY,GAAG,CAOjD,MAHA,MAAM,KAAK,QAAQ,OAAO,EAAW,IAAI,CAAC,UAAY,GAEpD,CACI,GAQV,MAAM,SAAS,EAAY,EAA4D,CACrF,OAAO,KAAK,MAAM,SAAS,EAAI,EAAQ,CAGzC,MAAM,SAAS,EAAsD,CACnE,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAK,MAAK,OAAM,KAAI,QAAO,KAAI,OAAQ,MAAM,OAAO,eAEtD,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,MAAU,MAAM,6DAA6D,CAGzF,IAAM,EAAa,EAAE,CAKrB,GAHI,GAAS,WACX,EAAW,KAAK,EAAG,EAAM,UAAW,EAAQ,UAAU,CAAC,CAErD,GAAS,eAAgB,CAE3B,IAAM,EAAU,EAAQ,eAAe,QAAQ,UAAW,OAAO,CACjE,EAAW,KAAK,EAAM,EAAM,SAAU,GAAG,EAAQ,GAAG,CAAC,CAEvD,GAAI,GAAS,OAAQ,CAGnB,IAAM,EAAU,IADA,EAAQ,OAAO,QAAQ,UAAW,OACvB,CAAC,GAC5B,EAAW,KACT,EACE,EAAM,EAAM,SAAU,EAAQ,CAC9B,CAAG,GAAG,EAAM,OAAO,qBAAqB,IACzC,CACF,CAGH,IAAM,EAAQ,GAAS,OAAS,GAC1B,EAAS,GAAS,QAAU,EAC5B,EAAc,EAAW,OAAS,EAAI,EAAI,GAAG,EAAW,CAAG,IAAA,GAG3D,EAAQ,MAAM,KAAK,MAAM,MAAM,CAAE,MAAO,EAAa,CAAC,CAGtD,EAAa,GAAS,UAAY,WAAa,EAAM,SAAW,EAAM,UACtE,GAAW,GAAS,gBAAkB,UAAY,MAAQ,EAAM,EAStE,MAAO,CACL,MAAA,MARkB,KAAK,MAAM,SAAS,CACtC,MAAO,EACP,QACA,SACA,QAAS,EAAQ,EAAW,CAC7B,CAAC,CAIA,QACA,QACA,SACD,CAGH,MAAM,OACJ,EACA,EACsB,CACtB,OAAO,KAAK,MAAM,OAAO,EAAI,EAAK,CAMpC,MAAM,OAAO,EAA2B,CACtC,IAAM,EAAS,MAAM,KAAK,MAAM,SAAS,EAAG,CAC5C,GAAI,CAAC,EAAQ,MAAU,MAAM,oBAAoB,IAAK,CAEtD,IAAM,EAAU,EAAO,QAGvB,MAAM,KAAK,MAAM,OAAO,EAAG,CAI3B,IAAM,GAAY,MADO,KAAK,QAAQ,YAAY,EAAQ,GAC5B,UAA6C,SAK3E,GAAI,EACF,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAS,CACxC,MAAM,KAAK,QAAQ,OAAO,EAAK,CAAC,UAAY,GAE1C,CAKN,MAAM,KAAK,QAAQ,OAAO,EAAQ,CAAC,UAAY,GAE7C,CAWJ,MAAM,OAAO,EAA6B,CACxC,IAAM,EAAS,MAAM,KAAK,MAAM,SAAS,EAAG,CAC5C,GAAI,CAAC,EAAQ,MAAU,MAAM,oBAAoB,IAAK,CAEtD,OAAO,KAAK,QAAQ,OAAO,EAAO,QAAQ,CAO5C,MAAM,QAAQ,EAA6C,CACzD,GAAI,EAAI,SAAW,EAAG,OAAO,IAAI,IAEjC,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,WAAY,MAAM,OAAO,eAE3B,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,OAAO,IAAI,IAEvB,IAAM,EAAU,MAAM,KAAK,MAAM,SAAS,CACxC,MAAO,EAAQ,EAAM,GAAI,EAAI,CAC7B,MAAO,EAAI,OACZ,CAAC,CAEI,EAAS,IAAI,IASnB,OAPA,MAAM,QAAQ,IACZ,EAAQ,IAAI,KAAO,IAAW,CAC5B,IAAM,EAAM,MAAM,KAAK,QAAQ,OAAO,EAAO,QAAQ,CACrD,EAAO,IAAI,EAAO,GAAI,EAAI,EAC1B,CACH,CAEM,EAWT,MAAM,cAAc,EAAY,EAA2C,CACzE,IAAM,EAAS,MAAM,KAAK,MAAM,SAAS,EAAG,CAC5C,GAAI,CAAC,EAAQ,OAAO,KAEpB,IAAM,EAAU,EAAO,QAIjB,GAAQ,MADO,KAAK,oBAAoB,EACzB,GACrB,GAAI,EAAO,CACT,IAAM,EAAO,EAAiB,EAAS,EAAW,EAAM,QAAU,OAAO,CACzE,GAAI,CACF,OAAO,MAAM,KAAK,QAAQ,OAAO,EAAK,MAChC,GAMV,GAAI,CACF,OAAO,MAAM,KAAK,QAAQ,OAAO,EAAQ,MACnC,CACN,OAAO,MAYX,MAAM,eAAe,EAAe,EAAiD,CACnF,GAAI,EAAI,SAAW,EAAG,OAAO,IAAI,IAEjC,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,WAAY,MAAM,OAAO,eAE3B,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,OAAO,IAAI,IAEvB,IAAM,EAAU,MAAM,KAAK,MAAM,SAAS,CACxC,MAAO,EAAQ,EAAM,GAAI,EAAI,CAC7B,MAAO,EAAI,OACZ,CAAC,CAGI,GAAQ,MADO,KAAK,oBAAoB,EACzB,GACf,EAAS,IAAI,IA0BnB,OAxBA,MAAM,QAAQ,IACZ,EAAQ,IAAI,KAAO,IAAW,CAE5B,GAAI,EAAO,CACT,IAAM,EAAO,EAAiB,EAAO,QAAS,EAAW,EAAM,QAAU,OAAO,CAChF,GAAI,CACF,IAAM,EAAM,MAAM,KAAK,QAAQ,OAAO,EAAK,CAC3C,EAAO,IAAI,EAAO,GAAI,EAAI,CAC1B,YACM,GAMV,GAAI,CACF,IAAM,EAAM,MAAM,KAAK,QAAQ,OAAO,EAAO,QAAQ,CACrD,EAAO,IAAI,EAAO,GAAI,EAAI,MACpB,IAGR,CACH,CAEM,IAQX,SAAS,EAAgB,EAA6B,CAWpD,OAVI,EAAS,WAAW,SAAS,CAAS,QACtC,EAAS,WAAW,SAAS,CAAS,QACtC,EAAS,WAAW,SAAS,CAAS,QAExC,IAAa,mBACb,EAAS,WAAW,qBAAqB,EACzC,EAAS,WAAW,mBAAmB,CAEhC,WAEF,QAGT,SAAS,EAAY,EAA0B,CAE7C,OADmB,EAAS,QAAQ,WAAY,GAC/B,CAAC,QAAQ,QAAS,IAAI"}
|
|
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-
|
|
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-
|
|
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:
|
|
20
|
-
readonly imageStyles:
|
|
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":"
|
|
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"}
|
package/dist/image-styles.d.mts
CHANGED
|
@@ -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):
|
|
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):
|
|
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":"
|
|
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-
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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
|
package/dist/picker.d.mts.map
CHANGED
|
@@ -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":"
|
|
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":";;;;;UAGiB,eAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,GAAA;AAAA;;UAIe,qBAAA;EACf,KAAA,EAAO,eAAA;EACP,KAAA;EANG;AAIL;;;;EAQE,UAAA;EAPO;EASP,MAAA;AAAA;;UAIe,oBAAA;EAJT;EAMN,UAAA,GAAa,OAAA;IACX,MAAA;IACA,SAAA;IACA,KAAA;IACA,MAAA;EAAA,MACI,OAAA,CAAQ,qBAAA;EAGuB;EAArC,WAAA,GAAc,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,eAAA;AAAA;;KAI3B,eAAA;;UAGK,qBAAA;EACf,OAAA;EACA,OAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,WAAA;EACA,UAAA;EACA,IAAA;EACA,IAAA;EACA,YAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,gBAAA;EACA,MAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA;EACA,KAAA;AAAA;;UAIe,gBAAA;EArBf;EAuBA,IAAA;EArBA;EAuBA,YAAA,GAAe,IAAA;EArBf;EAuBA,QAAA,GAAW,KAAA,EAAO,eAAA;EArBlB;EAuBA,IAAA,GAAO,eAAA;EArBP;EAuBA,MAAA;EArBA;EAuBA,SAAA;EArBA;EAuBA,SAAA;EArBA;EAuBA,WAAA;EArBA;EAuBA,KAAA;EAtBK;EAwBL,WAAA;EApBe;EAsBf,UAAA,GAAa,qBAAA;;EAEb,SAAA;EAhBO;EAkBP,QAAA,GAAW,SAAA;AAAA;;;;KCpFD,mBAAA,GAAsB,oBAAA;EAChC,WAAA,GAAc,EAAA,aAAe,OAAA;AAAA;;;;;;ADG/B;;;;;;;;;;;AAcA;iBCGgB,yBAAA,CAA0B,WAAA,YAA6B,mBAAA;;;UC9B7D,cAAA;EACR,IAAA,EAAM,eAAA;EACN,UAAA;EACA,QAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAUC,SAAA,CAAA;EAAY,IAAA;EAAM,UAAA;EAAY,QAAA;EAAU;AAAA,GAAc,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCd1E,cAAA;EACR,KAAA,EAAO,eAAA;EACP,QAAA,EAAU,GAAA;EACV,QAAA,GAAW,IAAA,EAAM,eAAA;EACjB,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;EACf,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;iBCbD,WAAA,CAAA;EACd,IAAA;EACA,YAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,SAAA;EACA;AAAA,GACC,gBAAA,+BAAgB,GAAA,CAAA,OAAA;;;UCtBF,wBAAA,SAAiC,oBAAA;EAChD,QAAA,EAAU,SAAA;AAAA;;;;;;;;;;;;;;;iBAiBI,mBAAA,CAAA;EACd,QAAA;EACA,UAAA;EACA;AAAA,GACC,wBAAA,+BAAwB,GAAA,CAAA,OAAA;AAAA,iBASX,cAAA,CAAA,GAAkB,oBAAA;;;UChCxB,cAAA;EACR,KAAA;EACA,QAAA,GAAW,KAAA;EACX,SAAA;EACA,iBAAA,GAAoB,IAAA;ENLU;EMO9B,MAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAWC,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,SAAA;EACA,iBAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCxBP,eAAA;EACR,QAAA,GAAW,IAAA,EAAM,IAAA,KAAS,OAAA;EAC1B,WAAA;EACA,MAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,UAAA,CAAA;EAAa,QAAA;EAAU,WAAA;EAAa,MAAA;EAAQ;AAAA,GAAc,eAAA,+BAAe,GAAA,CAAA,OAAA"}
|
|
@@ -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-
|
|
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
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":"
|
|
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{
|
|
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
|