@murumets-ee/media 0.1.4 → 0.1.5
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.ts → admin.d.mts} +4 -25
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +2 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/client-sF8mf4Fg.mjs +2 -0
- package/dist/client-sF8mf4Fg.mjs.map +1 -0
- package/dist/client.d.mts +95 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +2 -0
- package/dist/client.mjs.map +1 -0
- package/dist/entity-D5P2l05s.mjs +2 -0
- package/dist/entity-D5P2l05s.mjs.map +1 -0
- package/dist/entity-DZFku8b7.mjs +2 -0
- package/dist/entity-DZFku8b7.mjs.map +1 -0
- package/dist/image-styles-settings-7K_jPNIA.mjs +2 -0
- package/dist/image-styles-settings-7K_jPNIA.mjs.map +1 -0
- package/dist/image-styles-settings.d.mts +10 -0
- package/dist/image-styles-settings.d.mts.map +1 -0
- package/dist/image-styles-settings.mjs +2 -0
- package/dist/image-styles-settings.mjs.map +1 -0
- package/dist/image-styles.d.mts +71 -0
- package/dist/image-styles.d.mts.map +1 -0
- package/dist/image-styles.mjs +3 -0
- package/dist/image-styles.mjs.map +1 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/picker.d.mts +228 -0
- package/dist/picker.d.mts.map +1 -0
- package/dist/picker.mjs +3 -0
- package/dist/picker.mjs.map +1 -0
- package/dist/plugin-DV7lvImm.mjs +2 -0
- package/dist/plugin-DV7lvImm.mjs.map +1 -0
- package/dist/plugin.d.mts +20 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/query-client.d.mts +28 -0
- package/dist/query-client.d.mts.map +1 -0
- package/dist/query-client.mjs +2 -0
- package/dist/query-client.mjs.map +1 -0
- package/dist/{ref.d.ts → ref.d.mts} +26 -24
- package/dist/ref.d.mts.map +1 -0
- package/dist/ref.mjs +2 -0
- package/dist/ref.mjs.map +1 -0
- package/dist/regenerate-variants-DY7D4Ky3.mjs +2 -0
- package/dist/regenerate-variants-DY7D4Ky3.mjs.map +1 -0
- package/dist/types-BV_pOm23.d.mts +103 -0
- package/dist/types-BV_pOm23.d.mts.map +1 -0
- package/dist/usage-D7Bn7Vvv.mjs +2 -0
- package/dist/usage-D7Bn7Vvv.mjs.map +1 -0
- package/dist/usage.d.mts +21 -0
- package/dist/usage.d.mts.map +1 -0
- package/dist/usage.mjs +2 -0
- package/dist/usage.mjs.map +1 -0
- package/dist/variant-key-DZUYURS5.mjs +2 -0
- package/dist/variant-key-DZUYURS5.mjs.map +1 -0
- package/package.json +29 -29
- package/dist/admin.js +0 -1
- package/dist/chunk-JGE5BDIT.js +0 -1
- package/dist/chunk-L6BDKI76.js +0 -1
- package/dist/chunk-QTUXM53A.js +0 -1
- package/dist/chunk-YAHM4C5J.js +0 -1
- package/dist/client-37WG2Y6P.js +0 -1
- package/dist/client.d.ts +0 -102
- package/dist/client.js +0 -1
- package/dist/entity-QOBW3TFU.js +0 -1
- package/dist/image-styles-settings-AB5WFEFF.js +0 -1
- package/dist/image-styles-settings.d.ts +0 -9
- package/dist/image-styles-settings.js +0 -1
- package/dist/image-styles.d.ts +0 -62
- package/dist/image-styles.js +0 -2
- package/dist/index.d.ts +0 -131
- package/dist/index.js +0 -1
- package/dist/picker.d.ts +0 -183
- package/dist/picker.js +0 -2
- package/dist/plugin-CAV5BZMF.js +0 -1
- package/dist/plugin.d.ts +0 -35
- package/dist/plugin.js +0 -1
- package/dist/query-client.d.ts +0 -36
- package/dist/query-client.js +0 -1
- package/dist/ref.js +0 -1
- package/dist/regenerate-variants-IUDIIWXU.js +0 -1
- package/dist/types-ChlTxvlq.d.ts +0 -101
- package/dist/usage-E5RZMWE4.js +0 -1
- package/dist/usage.d.ts +0 -28
- package/dist/usage.js +0 -1
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
import { AdminRoute } from
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Media admin routes for the centralized admin API handler.
|
|
5
|
-
*
|
|
6
|
-
* Provides media-specific operations: upload, URL resolution, delete with storage cleanup,
|
|
7
|
-
* image style settings, and variant regeneration.
|
|
8
|
-
*
|
|
9
|
-
* Standard entity CRUD (PATCH with translations) falls through to the generic entity
|
|
10
|
-
* handler — add `Media` to the `entities` array in your handler config.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
|
|
15
|
-
* import { mediaRoutes } from '@murumets-ee/media/admin'
|
|
16
|
-
* import { Media } from '@murumets-ee/media'
|
|
17
|
-
*
|
|
18
|
-
* const handler = createAdminApiHandler({
|
|
19
|
-
* authenticate: async (req) => { ... },
|
|
20
|
-
* entities: [Article, Media],
|
|
21
|
-
* routes: [mediaRoutes()],
|
|
22
|
-
* })
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
1
|
+
import { AdminRoute } from "@murumets-ee/core";
|
|
25
2
|
|
|
3
|
+
//#region src/admin/routes.d.ts
|
|
26
4
|
/**
|
|
27
5
|
* Create admin API routes for media-specific operations.
|
|
28
6
|
*
|
|
@@ -43,5 +21,6 @@ import { AdminRoute } from '@murumets-ee/core';
|
|
|
43
21
|
* - `PATCH /api/admin/media/:id` — Update metadata with translation support
|
|
44
22
|
*/
|
|
45
23
|
declare function mediaRoutes(): AdminRoute;
|
|
46
|
-
|
|
24
|
+
//#endregion
|
|
47
25
|
export { mediaRoutes };
|
|
26
|
+
//# sourceMappingURL=admin.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAwbgB,WAAA,CAAA,GAAe,UAAA"}
|
package/dist/admin.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=[`cover`,`contain`,`inside`,`outside`,`fill`],t=[`webp`,`jpeg`,`png`,`avif`];function n(n){for(let[r,i]of Object.entries(n)){if(typeof r!=`string`||r.length===0||!/^[a-z][a-z0-9_-]*$/.test(r))return{valid:!1,error:`Invalid style name "${r}": must be lowercase alphanumeric (a-z, 0-9, -, _)`};if(!i||typeof i!=`object`)return{valid:!1,error:`Style "${r}" must be an object`};let n=i;if(n.width!==void 0&&(typeof n.width!=`number`||n.width<=0))return{valid:!1,error:`Invalid width for style "${r}": must be a positive number`};if(n.height!==void 0&&(typeof n.height!=`number`||n.height<=0))return{valid:!1,error:`Invalid height for style "${r}": must be a positive number`};if(n.width===void 0&&n.height===void 0)return{valid:!1,error:`Style "${r}" must have at least width or height`};if(n.quality!==void 0&&(typeof n.quality!=`number`||n.quality<1||n.quality>100))return{valid:!1,error:`Invalid quality for style "${r}": must be 1-100`};if(n.fit!==void 0&&!e.includes(n.fit))return{valid:!1,error:`Invalid fit for style "${r}": must be one of ${e.join(`, `)}`};if(n.format!==void 0&&!t.includes(n.format))return{valid:!1,error:`Invalid format for style "${r}": must be one of ${t.join(`, `)}`}}return{valid:!0,styles:n}}const r=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function i(e){return r.test(e)}let a=null;function o(){return a||=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),{MediaClient:r}=await import(`./client-sF8mf4Fg.mjs`),i=e(),a=t(n(),{app:i});return new r({db:i.db.readWrite,storage:a,logger:i.logger.child({media:!0})})})(),a}function s(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function c(e,t){return s({error:e},t)}async function l(e,{segments:t}){let n=await o();if(t.length===1&&t[0]===`settings`){let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:n}=await import(`@murumets-ee/core`);return s({imageStyles:await e(t,{app:n()}).get(`imageStyles`)??{}})}if(t.length===2&&t[1]===`usage`){let e=t[0];if(!i(e))return c(`Invalid media ID format`,400);let{findMediaUsages:n}=await import(`./usage-D7Bn7Vvv.mjs`),{getApp:r}=await import(`@murumets-ee/core`);return s({usages:await n(e,r().db.readWrite)})}if(t.length>0){let e=t[0],r=await n.findById(e);if(!r)return c(`Media not found`,404);let i=await n.getUrl(e);return s({...r,url:i})}let r=new URL(e.url),a=r.searchParams.get(`search`)??void 0,l=r.searchParams.get(`mediaType`)??void 0,u=Math.min(Math.max(Number(r.searchParams.get(`limit`))||24,1),100),d=Math.max(Number(r.searchParams.get(`offset`))||0,0),f=await n.findMany({search:a,mediaType:l,limit:u,offset:d}),p=f.items.map(e=>e.id),[m,h]=await Promise.all([n.getUrls(p),n.getVariantUrls(p,`thumbnail`)]);return s({items:f.items.map(e=>({id:e.id,title:e.title,alt:e.alt,filename:e.filename,mimeType:e.mimeType,size:e.size,mediaType:e.mediaType,url:m.get(e.id)??``,thumbnailUrl:h.get(e.id),width:e.width,height:e.height})),total:f.total})}async function u(e,{segments:t,user:r,audit:i,checkPermission:a}){if(t.length===1&&t[0]===`regenerate-variants`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for variant regeneration`,403);let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{regenerateAllVariants:n}=await import(`./regenerate-variants-DY7D4Ky3.mjs`),{getApp:o}=await import(`@murumets-ee/core`),{createStorageClient:l}=await import(`@murumets-ee/storage`),{getStorageConfig:u}=await import(`@murumets-ee/storage/plugin`),d=o(),f=await e(t,{app:d}).get(`imageStyles`);if(!f||Object.keys(f).length===0)return c(`No image styles configured`,400);let p=l(u(),{app:d}),m=await n({db:d.db.readWrite,storage:p,logger:d.logger.child({media:!0}),styles:f});return i?.({action:`media.regenerate_variants`,userId:r.id,userName:r.name,metadata:{total:m.total,processed:m.processed,errors:m.errors}}),s(m)}if(t.length===1&&t[0]===`settings`){if(!a(`media`,`update`))return c(`Forbidden: media update permission required for image style management`,403);let t=await e.json();if(!t.imageStyles||typeof t.imageStyles!=`object`)return c(`Body must contain "imageStyles" object`,400);let l=n(t.imageStyles);if(!l.valid)return c(l.error,400);let{createSettingsClient:u}=await import(`@murumets-ee/settings`),{imageStylesSettings:d}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:f}=await import(`@murumets-ee/core`);return await u(d,{app:f()}).set(`imageStyles`,l.styles),(await o()).invalidateImageStylesCache(),i?.({action:`media.settings.update`,entityType:`settings`,userId:r.id,userName:r.name,changes:{imageStyles:l.styles}}),s({imageStyles:l.styles})}let l=await o(),u=(await e.formData()).get(`file`);if(!u||u.size===0)return c(`No file provided`,400);if(u.size>50*1024*1024)return c(`File too large: ${(u.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let d=Buffer.from(await u.arrayBuffer()),{detectMimeType:f}=await import(`@murumets-ee/storage`),{mimeType:p,mismatch:m}=await f(d,u.type||`application/octet-stream`);if(m)return c(`File content doesn't match declared type: claimed ${u.type}, detected ${p}`,400);let h=await l.upload(d,{filename:u.name,mimeType:p,size:u.size,uploadedBy:r.id}),g={id:h.media.id,title:h.media.title,alt:h.media.alt,filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType,url:h.url,width:h.media.width,height:h.media.height};return i?.({action:`media.upload`,entityType:`media`,entityId:h.media.id,userId:r.id,userName:r.name,changes:{filename:h.media.filename,mimeType:h.media.mimeType,size:h.media.size,mediaType:h.media.mediaType}}),s(g,201)}async function d(e,{segments:t,user:n,audit:r}){if(t.length===0)return c(`Media ID required`,400);let a=t[0];return i(a)?(await(await o()).delete(a),r?.({action:`media.delete`,entityType:`media`,entityId:a,userId:n.id,userName:n.name}),s({deleted:1})):c(`Invalid media ID format`,400)}function f(){return{prefix:`media`,resource:`media`,actions:[`view`,`create`,`update`,`delete`],handlers:{GET:l,POST:u,DELETE:d}}}export{f as mediaRoutes};
|
|
2
|
+
//# sourceMappingURL=admin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.mjs","names":[],"sources":["../src/admin/routes.ts"],"sourcesContent":["/**\n * Media admin routes for the centralized admin API handler.\n *\n * Provides media-specific operations: upload, URL resolution, delete with storage cleanup,\n * image style settings, and variant regeneration.\n *\n * Standard entity CRUD (PATCH with translations) falls through to the generic entity\n * handler — add `Media` to the `entities` array in your handler config.\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { mediaRoutes } from '@murumets-ee/media/admin'\n * import { Media } from '@murumets-ee/media'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [Article, Media],\n * routes: [mediaRoutes()],\n * })\n * ```\n */\n\nimport type { AdminRoute, AuditLogFn, AuthUser } from '@murumets-ee/core'\nimport type { MediaClient } from '../client.js'\nimport type { MediaPickerItem, MediaPickerListResult } from '../picker/types.js'\nimport type { ImageStyle } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Image style validation\n// ---------------------------------------------------------------------------\n\nconst VALID_FIT = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst VALID_FORMAT = ['webp', 'jpeg', 'png', 'avif'] as const\n\nfunction validateImageStyles(\n styles: Record<string, unknown>,\n): { valid: true; styles: Record<string, ImageStyle> } | { valid: false; error: string } {\n for (const [name, raw] of Object.entries(styles)) {\n if (typeof name !== 'string' || name.length === 0 || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n return {\n valid: false,\n error: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n }\n }\n if (!raw || typeof raw !== 'object') {\n return { valid: false, error: `Style \"${name}\" must be an object` }\n }\n const style = raw as Record<string, unknown>\n if (style.width !== undefined && (typeof style.width !== 'number' || style.width <= 0)) {\n return { valid: false, error: `Invalid width for style \"${name}\": must be a positive number` }\n }\n if (style.height !== undefined && (typeof style.height !== 'number' || style.height <= 0)) {\n return {\n valid: false,\n error: `Invalid height for style \"${name}\": must be a positive number`,\n }\n }\n if (style.width === undefined && style.height === undefined) {\n return { valid: false, error: `Style \"${name}\" must have at least width or height` }\n }\n if (\n style.quality !== undefined &&\n (typeof style.quality !== 'number' || style.quality < 1 || style.quality > 100)\n ) {\n return { valid: false, error: `Invalid quality for style \"${name}\": must be 1-100` }\n }\n if (\n style.fit !== undefined &&\n !(VALID_FIT as readonly string[]).includes(style.fit as string)\n ) {\n return {\n valid: false,\n error: `Invalid fit for style \"${name}\": must be one of ${VALID_FIT.join(', ')}`,\n }\n }\n if (\n style.format !== undefined &&\n !(VALID_FORMAT as readonly string[]).includes(style.format as string)\n ) {\n return {\n valid: false,\n error: `Invalid format for style \"${name}\": must be one of ${VALID_FORMAT.join(', ')}`,\n }\n }\n }\n return { valid: true, styles: styles as Record<string, ImageStyle> }\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nfunction isValidUuid(value: string): boolean {\n return UUID_RE.test(value)\n}\n\n// ---------------------------------------------------------------------------\n// Lazy client initialization\n// ---------------------------------------------------------------------------\n\nlet clientPromise: Promise<MediaClient> | null = null\n\nfunction getClient(): Promise<MediaClient> {\n if (!clientPromise) {\n clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const { MediaClient } = await import('../client.js')\n\n const app = getApp()\n const storageConfig = getStorageConfig()\n const storage = createStorageClient(storageConfig, { app })\n\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n })()\n }\n return clientPromise\n}\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfunction json(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction errorJson(message: string, status: number) {\n return json({ error: message }, status)\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\nasync function handleGet(\n req: Request,\n { segments }: { segments: string[]; user: AuthUser; audit?: AuditLogFn },\n): Promise<Response> {\n const client = await getClient()\n\n // GET /media/settings — return current image styles\n if (segments.length === 1 && segments[0] === 'settings') {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('../image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const styles = await settingsClient.get('imageStyles')\n return json({ imageStyles: styles ?? {} })\n }\n\n // GET /media/:id/usage — find all entities referencing this media\n if (segments.length === 2 && segments[1] === 'usage') {\n const id = segments[0]\n if (!isValidUuid(id)) return errorJson('Invalid media ID format', 400)\n\n const { findMediaUsages } = await import('../usage.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const usages = await findMediaUsages(id, app.db.readWrite)\n return json({ usages })\n }\n\n // GET /media/:id — single item with URL (full record for EntityForm + picker)\n if (segments.length > 0) {\n const id = segments[0]\n const record = await client.findById(id)\n if (!record) return errorJson('Media not found', 404)\n\n const url = await client.getUrl(id)\n return json({ ...record, url })\n }\n\n // GET /media — list with search/filter + batch URL resolution\n const url = new URL(req.url)\n const search = url.searchParams.get('search') ?? undefined\n const mediaType = url.searchParams.get('mediaType') ?? undefined\n const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 24, 1), 100)\n const offset = Math.max(Number(url.searchParams.get('offset')) || 0, 0)\n\n const result = await client.findMany({\n search,\n mediaType: mediaType as 'image' | 'video' | 'audio' | 'document' | 'other' | undefined,\n limit,\n offset,\n })\n\n // Resolve original URLs + thumbnail variant URLs for all items\n const ids = result.items.map((item) => item.id)\n const [urlMap, thumbMap] = await Promise.all([\n client.getUrls(ids),\n client.getVariantUrls(ids, 'thumbnail'),\n ])\n\n const items: (MediaPickerItem & { thumbnailUrl?: string })[] = result.items.map((item) => ({\n id: item.id,\n title: item.title,\n alt: item.alt,\n filename: item.filename,\n mimeType: item.mimeType,\n size: item.size,\n mediaType: item.mediaType,\n url: urlMap.get(item.id) ?? '',\n thumbnailUrl: thumbMap.get(item.id),\n width: item.width,\n height: item.height,\n }))\n\n const response: MediaPickerListResult = { items, total: result.total }\n return json(response)\n}\n\nasync function handlePost(\n req: Request,\n {\n segments,\n user,\n audit,\n checkPermission,\n }: {\n segments: string[]\n user: AuthUser\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n): Promise<Response> {\n // POST /media/regenerate-variants — requires media update permission\n if (segments.length === 1 && segments[0] === 'regenerate-variants') {\n if (!checkPermission('media', 'update')) {\n return errorJson('Forbidden: media update permission required for variant regeneration', 403)\n }\n\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('../image-styles-settings.js')\n const { regenerateAllVariants } = await import('../regenerate-variants.js')\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const styles = await settingsClient.get('imageStyles')\n if (!styles || Object.keys(styles).length === 0) {\n return errorJson('No image styles configured', 400)\n }\n\n const storageConfig = getStorageConfig()\n const storage = createStorageClient(storageConfig, { app })\n\n const result = await regenerateAllVariants({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n styles,\n })\n\n audit?.({\n action: 'media.regenerate_variants',\n userId: user.id,\n userName: user.name,\n metadata: { total: result.total, processed: result.processed, errors: result.errors },\n })\n\n return json(result)\n }\n\n // POST /media/settings — requires media update permission\n if (segments.length === 1 && segments[0] === 'settings') {\n if (!checkPermission('media', 'update')) {\n return errorJson(\n 'Forbidden: media update permission required for image style management',\n 403,\n )\n }\n\n const body = (await req.json()) as { imageStyles?: Record<string, unknown> }\n if (!body.imageStyles || typeof body.imageStyles !== 'object') {\n return errorJson('Body must contain \"imageStyles\" object', 400)\n }\n\n const validation = validateImageStyles(body.imageStyles)\n if (!validation.valid) {\n return errorJson(validation.error, 400)\n }\n\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('../image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n await settingsClient.set('imageStyles', validation.styles)\n\n // Invalidate cached styles on the MediaClient singleton\n const client = await getClient()\n client.invalidateImageStylesCache()\n\n audit?.({\n action: 'media.settings.update',\n entityType: 'settings',\n userId: user.id,\n userName: user.name,\n changes: { imageStyles: validation.styles },\n })\n\n return json({ imageStyles: validation.styles })\n }\n\n // POST /media — upload file\n const client = await getClient()\n\n const formData = await req.formData()\n const file = formData.get('file') as File | null\n if (!file || file.size === 0) {\n return errorJson('No file provided', 400)\n }\n\n // Guard against oversized uploads (50 MB limit)\n const MAX_UPLOAD_SIZE = 50 * 1024 * 1024\n if (file.size > MAX_UPLOAD_SIZE) {\n return errorJson(\n `File too large: ${(file.size / 1024 / 1024).toFixed(1)} MB exceeds 50 MB limit`,\n 400,\n )\n }\n\n const buffer = Buffer.from(await file.arrayBuffer())\n\n // Detect actual MIME type from file content (prevents spoofing)\n const { detectMimeType } = await import('@murumets-ee/storage')\n const { mimeType, mismatch } = await detectMimeType(\n buffer,\n file.type || 'application/octet-stream',\n )\n if (mismatch) {\n return errorJson(\n `File content doesn't match declared type: claimed ${file.type}, detected ${mimeType}`,\n 400,\n )\n }\n\n const result = await client.upload(buffer, {\n filename: file.name,\n mimeType,\n size: file.size,\n uploadedBy: user.id,\n })\n\n const item: MediaPickerItem = {\n id: result.media.id,\n title: result.media.title,\n alt: result.media.alt,\n filename: result.media.filename,\n mimeType: result.media.mimeType,\n size: result.media.size,\n mediaType: result.media.mediaType,\n url: result.url,\n width: result.media.width,\n height: result.media.height,\n }\n\n audit?.({\n action: 'media.upload',\n entityType: 'media',\n entityId: result.media.id,\n userId: user.id,\n userName: user.name,\n changes: {\n filename: result.media.filename,\n mimeType: result.media.mimeType,\n size: result.media.size,\n mediaType: result.media.mediaType,\n },\n })\n\n return json(item, 201)\n}\n\nasync function handleDelete(\n _req: Request,\n { segments, user, audit }: { segments: string[]; user: AuthUser; audit?: AuditLogFn },\n): Promise<Response> {\n if (segments.length === 0) {\n return errorJson('Media ID required', 400)\n }\n\n const id = segments[0]\n if (!isValidUuid(id)) return errorJson('Invalid media ID format', 400)\n\n // AdminClient.delete() checks entity_refs and throws ReferencedEntityError\n // if this media is still referenced. The error bubbles to the caller's\n // error handler which returns 409 with usage details.\n const client = await getClient()\n await client.delete(id)\n\n audit?.({\n action: 'media.delete',\n entityType: 'media',\n entityId: id,\n userId: user.id,\n userName: user.name,\n })\n\n return json({ deleted: 1 })\n}\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create admin API routes for media-specific operations.\n *\n * Standard entity CRUD (PATCH with translations) is handled by the generic entity\n * handler — add `Media` to the `entities` array in your handler config.\n *\n * Routes handled by this plugin:\n * - `GET /api/admin/media` — List media with search/filter + URLs\n * - `GET /api/admin/media/settings` — Get current image styles\n * - `GET /api/admin/media/:id` — Get single media item with URL\n * - `GET /api/admin/media/:id/usage` — Find entities referencing this media\n * - `POST /api/admin/media` — Upload file (FormData with `file` field)\n * - `POST /api/admin/media/settings` — Save image styles (admin only)\n * - `POST /api/admin/media/regenerate-variants` — Regenerate all image variants (admin only)\n * - `DELETE /api/admin/media/:id` — Delete media + storage file (blocked if in use)\n *\n * Routes handled by generic entity handler (via fallthrough):\n * - `PATCH /api/admin/media/:id` — Update metadata with translation support\n */\nexport function mediaRoutes(): AdminRoute {\n return {\n prefix: 'media',\n resource: 'media',\n actions: ['view', 'create', 'update', 'delete'],\n handlers: {\n GET: handleGet,\n POST: handlePost,\n DELETE: handleDelete,\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Media as e}from"./entity-DZFku8b7.mjs";import{n as t,r as n,t as r}from"./variant-key-DZUYURS5.mjs";import"server-only";var i=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:t}=await import(`@murumets-ee/entity/admin`);this.admin=new t({entity:e,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings-7K_jPNIA.mjs`),{getApp:n}=await import(`@murumets-ee/core`),r=await e(t,{app:n()}).get(`imageStyles`);if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import(`./plugin-DV7lvImm.mjs`);return this.imageStyles=e().imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,i){let s=await this.storage.upload(e,{filename:i.filename,mimeType:i.mimeType,size:i.size,visibility:i.visibility,uploadedBy:i.uploadedBy}),c=i.width??null,l=i.height??null,u={};if(e instanceof Buffer&&t(i.mimeType))try{let t=await n(e,await this.resolveImageStyles());c=t.width,l=t.height;let a=s.visibility;await Promise.all([...t.variants.entries()].map(async([e,t])=>{let n=r(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:n,filename:`${e}_${i.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:a,metadata:{variantOf:s.key,style:e},uploadedBy:i.uploadedBy}),u[e]=n,this.logger?.debug({style:e,key:n,width:t.width,height:t.height},`Variant uploaded`)}catch(t){this.logger?.warn({style:e,key:n,error:t},`Failed to upload variant (non-fatal)`)}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(e=>{this.logger?.warn({key:s.key,error:e},`Failed to update original file metadata with variant keys (non-fatal)`)}),this.logger?.info({width:c,height:l,variants:Object.keys(u)},`Image processed`)}catch(e){this.logger?.warn({filename:i.filename,error:e},`Image processing failed (non-fatal, original saved)`)}let d=a(i.mimeType),f=await this.getAdmin();try{let e=await f.create({title:i.title??o(i.filename),alt:i.alt??null,description:i.description??null,fileKey:s.key,filename:i.filename,mimeType:i.mimeType,size:i.size,width:c,height:l,mediaType:d}),t=await this.storage.getUrl(s.key);return this.logger?.info({id:e.id,fileKey:s.key,mediaType:d},`Media uploaded`),{media:e,url:t}}catch(e){this.logger?.error({fileKey:s.key,error:e},`Media entity creation failed, rolling back storage uploads`);for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(e=>{this.logger?.error({fileKey:s.key,error:e},`Storage rollback also failed`)}),e}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{and:r,asc:i,desc:a,eq:o,ilike:s,sql:c}=await import(`drizzle-orm`),l=n.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(o(l.mediaType,e.mediaType)),e?.mimeTypePrefix&&u.push(s(l.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let t=`%${e.search}%`;u.push(c`(${s(l.filename,t)} OR ${l.fields} ->> 'title' ILIKE ${t})`)}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?r(...u):void 0,[m]=await this.db.select({count:c`count(*)::int`}).from(l).where(p),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?i:a;return{items:await t.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m?.count??0,limit:d,offset:f}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),n=await t.findById(e);if(!n)throw Error(`Media not found: ${e}`);let r=n.fileKey;await t.delete(e);let i=(await this.storage.getMetadata(r))?.metadata?.variants;if(i)for(let e of Object.values(i))await this.storage.delete(e).catch(t=>{this.logger?.warn({variantKey:e,error:t},`Failed to delete variant file`)});await this.storage.delete(r).catch(t=>{this.logger?.error({id:e,fileKey:r,error:t},`Failed to delete file from storage after entity deletion`)}),this.logger?.info({id:e,fileKey:r,deletedVariants:i?Object.keys(i).length:0},`Media deleted`)}async getUrl(e){let t=await(await this.getAdmin()).findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:r}=await import(`drizzle-orm`),i=n.get(`media`);if(!i)return new Map;let a=await t.findMany({where:r(i.id,e),limit:e.length}),o=new Map;return await Promise.all(a.map(async e=>{let t=await this.storage.getUrl(e.fileKey);o.set(e.id,t)})),o}async getVariantUrl(e,t){let n=await(await this.getAdmin()).findById(e);if(!n)return null;let i=n.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=r(i,t,a.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(i)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let n=await this.getAdmin(),{schemaRegistry:i}=await import(`@murumets-ee/db`),{inArray:a}=await import(`drizzle-orm`),o=i.get(`media`);if(!o)return new Map;let s=await n.findMany({where:a(o.id,e),limit:e.length}),c=(await this.resolveImageStyles())[t],l=new Map;return await Promise.all(s.map(async e=>{if(c){let n=r(e.fileKey,t,c.format??`webp`);try{let t=await this.storage.getUrl(n);l.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);l.set(e.id,t)}catch{}})),l}};function a(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}function o(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{i as MediaClient};
|
|
2
|
+
//# sourceMappingURL=client-sF8mf4Fg.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-sF8mf4Fg.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface MediaClientConfig {\n db: PostgresJsDatabase\n storage: StorageClient\n logger?: Logger\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private db: PostgresJsDatabase\n private storage: StorageClient\n private logger?: Logger\n private admin: AdminClient | null = null\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.db = config.db\n this.storage = config.storage\n this.logger = config.logger\n this.imageStyles = config.imageStyles ?? null\n }\n\n private async getAdmin(): Promise<AdminClient> {\n if (!this.admin) {\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n this.admin = new AdminClient({\n entity: Media,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.admin\n }\n\n /**\n * Resolve image styles: settings DB → plugin config → hardcoded defaults.\n * Result is cached; call `invalidateImageStylesCache()` after settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n\n // 1. Try settings DB (source of truth after first boot)\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const stored = await settingsClient.get('imageStyles')\n if (stored && Object.keys(stored).length > 0) {\n this.imageStyles = stored\n return stored\n }\n } catch {\n // Settings not available — fall through to plugin config\n }\n\n // 2. Fall back to plugin config singleton\n try {\n const { getMediaConfig } = await import('./plugin.js')\n const config = getMediaConfig()\n this.imageStyles = config.imageStyles\n return this.imageStyles\n } catch {\n // Plugin not initialized — hardcoded defaults\n const defaults: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n }\n this.imageStyles = defaults\n return defaults\n }\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n this.logger?.debug(\n { style: styleName, key: vKey, width: variant.width, height: variant.height },\n 'Variant uploaded',\n )\n } catch (variantErr: unknown) {\n this.logger?.warn(\n { style: styleName, key: vKey, error: variantErr },\n 'Failed to upload variant (non-fatal)',\n )\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch((metaErr: unknown) => {\n this.logger?.warn(\n { key: fileRecord.key, error: metaErr },\n 'Failed to update original file metadata with variant keys (non-fatal)',\n )\n })\n }\n\n this.logger?.info({ width, height, variants: Object.keys(variantKeys) }, 'Image processed')\n } catch (processErr: unknown) {\n this.logger?.warn(\n { filename: options.filename, error: processErr },\n 'Image processing failed (non-fatal, original saved)',\n )\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n const admin = await this.getAdmin()\n\n try {\n const entityRecord = await admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n } as Record<string, EntityFieldValue>)\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n this.logger?.info(\n { id: entityRecord.id, fileKey: fileRecord.key, mediaType },\n 'Media uploaded',\n )\n\n return {\n media: entityRecord as unknown as MediaRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n this.logger?.error(\n { fileKey: fileRecord.key, error },\n 'Media entity creation failed, rolling back storage uploads',\n )\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch((rollbackErr: unknown) => {\n this.logger?.error(\n { fileKey: fileRecord.key, error: rollbackErr },\n 'Storage rollback also failed',\n )\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n const admin = await this.getAdmin()\n const result = await admin.findById(id, options)\n return result as unknown as MediaRecord | null\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n conditions.push(ilike(table.mimeType, `${options.mimeTypePrefix}%`))\n }\n if (options?.search) {\n const pattern = `%${options.search}%`\n conditions.push(\n sql`(${ilike(table.filename, pattern)} OR ${table.fields} ->> 'title' ILIKE ${pattern})`,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total\n const [countResult] = await this.db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(whereClause)\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items: items as unknown as MediaRecord[],\n total: countResult?.count ?? 0,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n const admin = await this.getAdmin()\n const result = await admin.update(id, data as Record<string, EntityFieldValue>)\n return result as unknown as MediaRecord\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch((err: unknown) => {\n this.logger?.warn({ variantKey: vKey, error: err }, 'Failed to delete variant file')\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch((err: unknown) => {\n this.logger?.error(\n { id, fileKey, error: err },\n 'Failed to delete file from storage after entity deletion',\n )\n })\n\n this.logger?.info(\n { id, fileKey, deletedVariants: variants ? Object.keys(variants).length : 0 },\n 'Media deleted',\n )\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl((record as unknown as MediaRecord).fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) return null\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Lazy singleton — auto-discovers storage config\n// ---------------------------------------------------------------------------\n\nlet _clientPromise: Promise<MediaClient> | null = null\n\n/**\n * Lazy singleton — returns a shared MediaClient instance.\n * Auto-discovers storage configuration from the storage plugin.\n * Must be called after createApp().\n */\nexport function getMediaClient(): Promise<MediaClient> {\n if (!_clientPromise) {\n _clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n\n const app = getApp()\n const config = getStorageConfig()\n const storage = createStorageClient(config, { app })\n\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n })()\n }\n return _clientPromise\n}\n"],"mappings":"+HAwCA,IAAa,EAAb,KAAyB,CACvB,GACA,QACA,OACA,MAAoC,KACpC,YAEA,YAAY,EAA2B,CACrC,KAAK,GAAK,EAAO,GACjB,KAAK,QAAU,EAAO,QACtB,KAAK,OAAS,EAAO,OACrB,KAAK,YAAc,EAAO,aAAe,KAG3C,MAAc,UAAiC,CAC7C,GAAI,CAAC,KAAK,MAAO,CACf,GAAM,CAAE,eAAgB,MAAM,OAAO,6BACrC,KAAK,MAAQ,IAAI,EAAY,CAC3B,OAAQ,EACR,GAAI,KAAK,GACT,OAAQ,KAAK,OACd,CAAC,CAEJ,OAAO,KAAK,MAOd,MAAc,oBAA0D,CACtE,GAAI,KAAK,YAAa,OAAO,KAAK,YAGlC,GAAI,CACF,GAAM,CAAE,wBAAyB,MAAM,OAAO,yBACxC,CAAE,uBAAwB,MAAM,OAAO,wCACvC,CAAE,UAAW,MAAM,OAAO,qBAG1B,EAAS,MADQ,EAAqB,EAAqB,CAAE,IADvD,GAAQ,CACoD,CAAC,CACrC,IAAI,cAAc,CACtD,GAAI,GAAU,OAAO,KAAK,EAAO,CAAC,OAAS,EAEzC,MADA,MAAK,YAAc,EACZ,OAEH,EAKR,GAAI,CACF,GAAM,CAAE,kBAAmB,MAAM,OAAO,yBAGxC,MADA,MAAK,YADU,GAAgB,CACL,YACnB,KAAK,iBACN,CAEN,IAAM,EAAuC,CAC3C,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CAED,MADA,MAAK,YAAc,EACZ,GAKX,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,EADtB,MAAM,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,EACzB,KAAK,QAAQ,MACX,CAAE,MAAO,EAAW,IAAK,EAAM,MAAO,EAAQ,MAAO,OAAQ,EAAQ,OAAQ,CAC7E,mBACD,OACM,EAAqB,CAC5B,KAAK,QAAQ,KACX,CAAE,MAAO,EAAW,IAAK,EAAM,MAAO,EAAY,CAClD,uCACD,GAEH,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,MAAO,GAAqB,CAC3B,KAAK,QAAQ,KACX,CAAE,IAAK,EAAW,IAAK,MAAO,EAAS,CACvC,wEACD,EACD,CAGN,KAAK,QAAQ,KAAK,CAAE,QAAO,SAAQ,SAAU,OAAO,KAAK,EAAY,CAAE,CAAE,kBAAkB,OACpF,EAAqB,CAC5B,KAAK,QAAQ,KACX,CAAE,SAAU,EAAQ,SAAU,MAAO,EAAY,CACjD,sDACD,CAKL,IAAM,EAAY,EAAgB,EAAQ,SAAS,CAC7C,EAAQ,MAAM,KAAK,UAAU,CAEnC,GAAI,CACF,IAAM,EAAe,MAAM,EAAM,OAAO,CACtC,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,CAAqC,CAGhC,EAAM,MAAM,KAAK,QAAQ,OAAO,EAAW,IAAI,CAOrD,OALA,KAAK,QAAQ,KACX,CAAE,GAAI,EAAa,GAAI,QAAS,EAAW,IAAK,YAAW,CAC3D,iBACD,CAEM,CACL,MAAO,EACP,MACD,OACM,EAAO,CAEd,KAAK,QAAQ,MACX,CAAE,QAAS,EAAW,IAAK,QAAO,CAClC,6DACD,CAGD,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAY,CAC3C,MAAM,KAAK,QAAQ,OAAO,EAAK,CAAC,UAAY,GAAG,CAUjD,MANA,MAAM,KAAK,QAAQ,OAAO,EAAW,IAAI,CAAC,MAAO,GAAyB,CACxE,KAAK,QAAQ,MACX,CAAE,QAAS,EAAW,IAAK,MAAO,EAAa,CAC/C,+BACD,EACD,CACI,GAQV,MAAM,SAAS,EAAY,EAA4D,CAGrF,OADe,MADD,MAAM,KAAK,UAAU,EACR,SAAS,EAAI,EAAQ,CAIlD,MAAM,SAAS,EAAsD,CACnE,IAAM,EAAQ,MAAM,KAAK,UAAU,CAC7B,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAK,MAAK,OAAM,KAAI,QAAO,OAAQ,MAAM,OAAO,eAElD,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,MAAU,MAAM,6DAA6D,CAGzF,IAAM,EAAa,EAAE,CAQrB,GANI,GAAS,WACX,EAAW,KAAK,EAAG,EAAM,UAAW,EAAQ,UAAU,CAAC,CAErD,GAAS,gBACX,EAAW,KAAK,EAAM,EAAM,SAAU,GAAG,EAAQ,eAAe,GAAG,CAAC,CAElE,GAAS,OAAQ,CACnB,IAAM,EAAU,IAAI,EAAQ,OAAO,GACnC,EAAW,KACT,CAAG,IAAI,EAAM,EAAM,SAAU,EAAQ,CAAC,MAAM,EAAM,OAAO,qBAAqB,EAAQ,GACvF,CAGH,IAAM,EAAQ,GAAS,OAAS,GAC1B,EAAS,GAAS,QAAU,EAC5B,EAAc,EAAW,OAAS,EAAI,EAAI,GAAG,EAAW,CAAG,IAAA,GAG3D,CAAC,GAAe,MAAM,KAAK,GAC9B,OAAO,CAAE,MAAO,CAAW,gBAAiB,CAAC,CAC7C,KAAK,EAAM,CACX,MAAM,EAAY,CAGf,EAAa,GAAS,UAAY,WAAa,EAAM,SAAW,EAAM,UACtE,GAAW,GAAS,gBAAkB,UAAY,MAAQ,EAAM,EAStE,MAAO,CACL,MARY,MAAM,EAAM,SAAS,CACjC,MAAO,EACP,QACA,SACA,QAAS,EAAQ,EAAW,CAC7B,CAAC,CAIA,MAAO,GAAa,OAAS,EAC7B,QACA,SACD,CAGH,MAAM,OACJ,EACA,EACsB,CAGtB,OADe,MADD,MAAM,KAAK,UAAU,EACR,OAAO,EAAI,EAAyC,CAOjF,MAAM,OAAO,EAA2B,CACtC,IAAM,EAAQ,MAAM,KAAK,UAAU,CAC7B,EAAS,MAAM,EAAM,SAAS,EAAG,CACvC,GAAI,CAAC,EAAQ,MAAU,MAAM,oBAAoB,IAAK,CAEtD,IAAM,EAAW,EAAkC,QAGnD,MAAM,EAAM,OAAO,EAAG,CAItB,IAAM,GADa,MAAM,KAAK,QAAQ,YAAY,EAAQ,GAC5B,UAA6C,SAK3E,GAAI,EACF,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAS,CACxC,MAAM,KAAK,QAAQ,OAAO,EAAK,CAAC,MAAO,GAAiB,CACtD,KAAK,QAAQ,KAAK,CAAE,WAAY,EAAM,MAAO,EAAK,CAAE,gCAAgC,EACpF,CAKN,MAAM,KAAK,QAAQ,OAAO,EAAQ,CAAC,MAAO,GAAiB,CACzD,KAAK,QAAQ,MACX,CAAE,KAAI,UAAS,MAAO,EAAK,CAC3B,2DACD,EACD,CAEF,KAAK,QAAQ,KACX,CAAE,KAAI,UAAS,gBAAiB,EAAW,OAAO,KAAK,EAAS,CAAC,OAAS,EAAG,CAC7E,gBACD,CAWH,MAAM,OAAO,EAA6B,CAExC,IAAM,EAAS,MADD,MAAM,KAAK,UAAU,EACR,SAAS,EAAG,CACvC,GAAI,CAAC,EAAQ,MAAU,MAAM,oBAAoB,IAAK,CAEtD,OAAO,KAAK,QAAQ,OAAQ,EAAkC,QAAQ,CAOxE,MAAM,QAAQ,EAA6C,CACzD,GAAI,EAAI,SAAW,EAAG,OAAO,IAAI,IAEjC,IAAM,EAAQ,MAAM,KAAK,UAAU,CAC7B,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,WAAY,MAAM,OAAO,eAE3B,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,OAAO,IAAI,IAEvB,IAAM,EAAU,MAAM,EAAM,SAAS,CACnC,MAAO,EAAQ,EAAM,GAAI,EAAI,CAC7B,MAAO,EAAI,OACZ,CAAC,CAEI,EAAS,IAAI,IASnB,OAPA,MAAM,QAAQ,IACX,EAAqC,IAAI,KAAO,IAAW,CAC1D,IAAM,EAAM,MAAM,KAAK,QAAQ,OAAO,EAAO,QAAQ,CACrD,EAAO,IAAI,EAAO,GAAI,EAAI,EAC1B,CACH,CAEM,EAWT,MAAM,cAAc,EAAY,EAA2C,CAEzE,IAAM,EAAS,MADD,MAAM,KAAK,UAAU,EACR,SAAS,EAAG,CACvC,GAAI,CAAC,EAAQ,OAAO,KAEpB,IAAM,EAAW,EAAkC,QAI7C,GADS,MAAM,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,IAAM,EAAQ,MAAM,KAAK,UAAU,CAC7B,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,WAAY,MAAM,OAAO,eAE3B,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,OAAO,IAAI,IAEvB,IAAM,EAAU,MAAM,EAAM,SAAS,CACnC,MAAO,EAAQ,EAAM,GAAI,EAAI,CAC7B,MAAO,EAAI,OACZ,CAAC,CAGI,GADS,MAAM,KAAK,oBAAoB,EACzB,GACf,EAAS,IAAI,IA0BnB,OAxBA,MAAM,QAAQ,IACX,EAAqC,IAAI,KAAO,IAAW,CAE1D,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,GAAG,CACjC,QAAQ,QAAS,IAAI"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { a as MediaRecord, c as MediaUploadResult, n as MediaListOptions, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BV_pOm23.mjs";
|
|
2
|
+
import { Logger } from "@murumets-ee/core";
|
|
3
|
+
import { StorageClient } from "@murumets-ee/storage";
|
|
4
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
5
|
+
|
|
6
|
+
//#region src/client.d.ts
|
|
7
|
+
interface MediaClientConfig {
|
|
8
|
+
db: PostgresJsDatabase;
|
|
9
|
+
storage: StorageClient;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
/** Image styles to generate on upload. Loaded from plugin config if not provided. */
|
|
12
|
+
imageStyles?: Record<string, ImageStyle>;
|
|
13
|
+
}
|
|
14
|
+
declare class MediaClient {
|
|
15
|
+
private db;
|
|
16
|
+
private storage;
|
|
17
|
+
private logger?;
|
|
18
|
+
private admin;
|
|
19
|
+
private imageStyles;
|
|
20
|
+
constructor(config: MediaClientConfig);
|
|
21
|
+
private getAdmin;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve image styles: settings DB → plugin config → hardcoded defaults.
|
|
24
|
+
* Result is cached; call `invalidateImageStylesCache()` after settings update.
|
|
25
|
+
*/
|
|
26
|
+
private resolveImageStyles;
|
|
27
|
+
/** Clear cached styles so next access re-reads from settings DB. */
|
|
28
|
+
invalidateImageStylesCache(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Upload a file and create a media entity record in one step.
|
|
31
|
+
* 1. Uploads original to storage (StorageClient)
|
|
32
|
+
* 2. If image: extracts dimensions via Sharp + generates variants
|
|
33
|
+
* 3. Creates media entity record (AdminClient)
|
|
34
|
+
* 4. Returns the media record + URL
|
|
35
|
+
*
|
|
36
|
+
* Rolls back storage upload if entity creation fails.
|
|
37
|
+
* Variant generation failures are logged but don't fail the upload.
|
|
38
|
+
*/
|
|
39
|
+
upload(data: Buffer | ReadableStream<Uint8Array>, options: MediaUploadOptions): Promise<MediaUploadResult>;
|
|
40
|
+
findById(id: string, options?: {
|
|
41
|
+
locale?: string;
|
|
42
|
+
}): Promise<MediaRecord | null>;
|
|
43
|
+
findMany(options?: MediaListOptions): Promise<MediaListResult>;
|
|
44
|
+
update(id: string, data: {
|
|
45
|
+
title?: string;
|
|
46
|
+
alt?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
}): Promise<MediaRecord>;
|
|
49
|
+
/**
|
|
50
|
+
* Delete a media entity, its variants, and its original file in storage.
|
|
51
|
+
*/
|
|
52
|
+
delete(id: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Get URL for a media entity by its ID.
|
|
55
|
+
* Resolves entity -> fileKey -> storage URL.
|
|
56
|
+
*/
|
|
57
|
+
getUrl(id: string): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Get URLs for multiple media entities (batch).
|
|
60
|
+
* Returns a Map of mediaId -> url.
|
|
61
|
+
*/
|
|
62
|
+
getUrls(ids: string[]): Promise<Map<string, string>>;
|
|
63
|
+
/**
|
|
64
|
+
* Get variant URL for a specific image style.
|
|
65
|
+
* Falls back to original URL if the variant doesn't exist.
|
|
66
|
+
*
|
|
67
|
+
* @param id - Media entity ID
|
|
68
|
+
* @param styleName - Image style name (e.g., 'thumbnail')
|
|
69
|
+
* @returns The variant URL, or original URL as fallback, or null if media not found
|
|
70
|
+
*/
|
|
71
|
+
getVariantUrl(id: string, styleName: string): Promise<string | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Get variant URLs for multiple media entities (batch).
|
|
74
|
+
* Falls back to original URL per item if the variant doesn't exist.
|
|
75
|
+
*
|
|
76
|
+
* @param ids - Media entity IDs
|
|
77
|
+
* @param styleName - Image style name (e.g., 'thumbnail')
|
|
78
|
+
* @returns Map of mediaId -> variant URL (or original URL as fallback)
|
|
79
|
+
*/
|
|
80
|
+
getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Factory — creates a MediaClient with an explicit StorageClient.
|
|
84
|
+
* Must be called after createApp().
|
|
85
|
+
*/
|
|
86
|
+
declare function createMediaClient(storage: StorageClient): Promise<MediaClient>;
|
|
87
|
+
/**
|
|
88
|
+
* Lazy singleton — returns a shared MediaClient instance.
|
|
89
|
+
* Auto-discovers storage configuration from the storage plugin.
|
|
90
|
+
* Must be called after createApp().
|
|
91
|
+
*/
|
|
92
|
+
declare function getMediaClient(): Promise<MediaClient>;
|
|
93
|
+
//#endregion
|
|
94
|
+
export { MediaClient, MediaClientConfig, createMediaClient, getMediaClient };
|
|
95
|
+
//# sourceMappingURL=client.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;UAgCiB,iBAAA;EACf,EAAA,EAAI,kBAAA;EACJ,OAAA,EAAS,aAAA;EACT,MAAA,GAAS,MAAA;EAFT;EAIA,WAAA,GAAc,MAAA,SAAe,UAAA;AAAA;AAAA,cAGlB,WAAA;EAAA,QACH,EAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,WAAA;cAEI,MAAA,EAAQ,iBAAA;EAAA,QAON,QAAA;EAdH;;;;EAAA,QA8BG,kBAAA;EAuDkB;EAnBhC,0BAAA,CAAA;EAoBW;;;;;;;;;;EAFL,MAAA,CACJ,IAAA,EAAM,MAAA,GAAS,cAAA,CAAe,UAAA,GAC9B,OAAA,EAAS,kBAAA,GACR,OAAA,CAAQ,iBAAA;EAwIL,QAAA,CAAS,EAAA,UAAY,OAAA;IAAY,MAAA;EAAA,IAAoB,OAAA,CAAQ,WAAA;EAM7D,QAAA,CAAS,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAqD9C,MAAA,CACJ,EAAA,UACA,IAAA;IAAQ,KAAA;IAAgB,GAAA;IAAc,WAAA;EAAA,IACrC,OAAA,CAAQ,WAAA;EA3RH;;;EAoSF,MAAA,CAAO,EAAA,WAAa,OAAA;;;;;EA+CpB,MAAA,CAAO,EAAA,WAAa,OAAA;EAnR1B;;;;EA+RM,OAAA,CAAQ,GAAA,aAAgB,OAAA,CAAQ,GAAA;EA5QpC;;;;;;;;EA+SI,aAAA,CAAc,EAAA,UAAY,SAAA,WAAoB,OAAA;EArKO;;;;;;;;EAwMrD,cAAA,CAAe,GAAA,YAAe,SAAA,WAAoB,OAAA,CAAQ,GAAA;AAAA;;;;;iBA0E5C,iBAAA,CAAkB,OAAA,EAAS,aAAA,GAAgB,OAAA,CAAQ,WAAA;;;;;;iBAqBzD,cAAA,CAAA,GAAkB,OAAA,CAAQ,WAAA"}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./entity-D5P2l05s.mjs";import"server-only";import t from"sharp";const n=new Set([`image/svg+xml`,`image/gif`]);function r(e){return e.startsWith(`image/`)&&!n.has(e)}async function i(e,n){let r=await t(e).metadata(),i=r.width??0,a=r.height??0,o=new Map,s=Object.entries(n);return await Promise.all(s.map(async([n,r])=>{let i=r.format??`webp`,a=r.quality??80,s=r.fit??`cover`,{data:c,info:l}=await t(e).resize({width:r.width,height:r.height,fit:s,withoutEnlargement:!0})[i]({quality:a}).toBuffer({resolveWithObject:!0});o.set(n,{buffer:c,format:i,mimeType:`image/${i}`,width:l.width,height:l.height})})),{width:i,height:a,variants:o}}function a(e,t,n=`webp`){let r=e.lastIndexOf(`/`);return`${e.substring(0,r)}/${t}_${e.substring(r+1).replace(/\.[^.]+$/,``)}.${n}`}var o=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:t}=await import(`@murumets-ee/entity/admin`);this.admin=new t({entity:e,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import(`@murumets-ee/settings`),{imageStylesSettings:t}=await import(`./image-styles-settings.mjs`),{getApp:n}=await import(`@murumets-ee/core`),r=await e(t,{app:n()}).get(`imageStyles`);if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import(`./plugin.mjs`);return this.imageStyles=e().imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let n=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),o=t.width??null,l=t.height??null,u={};if(e instanceof Buffer&&r(t.mimeType))try{let r=await i(e,await this.resolveImageStyles());o=r.width,l=r.height;let s=n.visibility;await Promise.all([...r.variants.entries()].map(async([e,r])=>{let i=a(n.key,e,r.format);try{await this.storage.upload(r.buffer,{key:i,filename:`${e}_${t.filename}`,mimeType:r.mimeType,size:r.buffer.byteLength,visibility:s,metadata:{variantOf:n.key,style:e},uploadedBy:t.uploadedBy}),u[e]=i,this.logger?.debug({style:e,key:i,width:r.width,height:r.height},`Variant uploaded`)}catch(t){this.logger?.warn({style:e,key:i,error:t},`Failed to upload variant (non-fatal)`)}})),Object.keys(u).length>0&&await this.storage.updateMetadata(n.key,{metadata:{...n.metadata??{},variants:u}}).catch(e=>{this.logger?.warn({key:n.key,error:e},`Failed to update original file metadata with variant keys (non-fatal)`)}),this.logger?.info({width:o,height:l,variants:Object.keys(u)},`Image processed`)}catch(e){this.logger?.warn({filename:t.filename,error:e},`Image processing failed (non-fatal, original saved)`)}let d=s(t.mimeType),f=await this.getAdmin();try{let e=await f.create({title:t.title??c(t.filename),alt:t.alt??null,description:t.description??null,fileKey:n.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:o,height:l,mediaType:d}),r=await this.storage.getUrl(n.key);return this.logger?.info({id:e.id,fileKey:n.key,mediaType:d},`Media uploaded`),{media:e,url:r}}catch(e){this.logger?.error({fileKey:n.key,error:e},`Media entity creation failed, rolling back storage uploads`);for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(n.key).catch(e=>{this.logger?.error({fileKey:n.key,error:e},`Storage rollback also failed`)}),e}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{and:r,asc:i,desc:a,eq:o,ilike:s,sql:c}=await import(`drizzle-orm`),l=n.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(o(l.mediaType,e.mediaType)),e?.mimeTypePrefix&&u.push(s(l.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let t=`%${e.search}%`;u.push(c`(${s(l.filename,t)} OR ${l.fields} ->> 'title' ILIKE ${t})`)}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?r(...u):void 0,[m]=await this.db.select({count:c`count(*)::int`}).from(l).where(p),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?i:a;return{items:await t.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m?.count??0,limit:d,offset:f}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),n=await t.findById(e);if(!n)throw Error(`Media not found: ${e}`);let r=n.fileKey;await t.delete(e);let i=(await this.storage.getMetadata(r))?.metadata?.variants;if(i)for(let e of Object.values(i))await this.storage.delete(e).catch(t=>{this.logger?.warn({variantKey:e,error:t},`Failed to delete variant file`)});await this.storage.delete(r).catch(t=>{this.logger?.error({id:e,fileKey:r,error:t},`Failed to delete file from storage after entity deletion`)}),this.logger?.info({id:e,fileKey:r,deletedVariants:i?Object.keys(i).length:0},`Media deleted`)}async getUrl(e){let t=await(await this.getAdmin()).findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:r}=await import(`drizzle-orm`),i=n.get(`media`);if(!i)return new Map;let a=await t.findMany({where:r(i.id,e),limit:e.length}),o=new Map;return await Promise.all(a.map(async e=>{let t=await this.storage.getUrl(e.fileKey);o.set(e.id,t)})),o}async getVariantUrl(e,t){let n=await(await this.getAdmin()).findById(e);if(!n)return null;let r=n.fileKey,i=(await this.resolveImageStyles())[t];if(i){let e=a(r,t,i.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(r)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let n=await this.getAdmin(),{schemaRegistry:r}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),o=r.get(`media`);if(!o)return new Map;let s=await n.findMany({where:i(o.id,e),limit:e.length}),c=(await this.resolveImageStyles())[t],l=new Map;return await Promise.all(s.map(async e=>{if(c){let n=a(e.fileKey,t,c.format??`webp`);try{let t=await this.storage.getUrl(n);l.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);l.set(e.id,t)}catch{}})),l}};function s(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 c(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}async function l(e){let{getApp:t}=await import(`@murumets-ee/core`),n=t();return new o({db:n.db.readWrite,storage:e,logger:n.logger.child({media:!0})})}let u=null;function d(){return u||=(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(),i=t(n(),{app:r});return new o({db:r.db.readWrite,storage:i,logger:r.logger.child({media:!0})})})(),u}export{o as MediaClient,l as createMediaClient,d as getMediaClient};
|
|
2
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/process-image.ts","../src/variant-key.ts","../src/client.ts"],"sourcesContent":["/**\n * Server-side image processing via Sharp.\n *\n * Pure processing module — no DB or storage dependencies.\n * Takes a buffer and image styles, returns metadata + variant buffers.\n */\n\nimport sharp from 'sharp'\nimport type { ImageStyle } from './types.js'\n\n/** Result of processing an image through Sharp */\nexport interface ProcessedImage {\n /** Original image width in pixels */\n width: number\n /** Original image height in pixels */\n height: number\n /** Generated variant buffers keyed by style name */\n variants: Map<string, ProcessedVariant>\n}\n\n/** A single processed variant */\nexport interface ProcessedVariant {\n buffer: Buffer\n format: string\n mimeType: string\n width: number\n height: number\n}\n\n/** MIME types that should NOT be processed (vectors, animations) */\nconst SKIP_MIME_TYPES = new Set(['image/svg+xml', 'image/gif'])\n\n/**\n * Check if a MIME type is eligible for Sharp processing.\n * Returns false for SVG, GIF, and non-image types.\n */\nexport function isProcessableImage(mimeType: string): boolean {\n return mimeType.startsWith('image/') && !SKIP_MIME_TYPES.has(mimeType)\n}\n\n/**\n * Extract image dimensions and generate resized variants.\n *\n * @param buffer - Original image file as a Buffer\n * @param styles - Named image style presets to generate\n * @returns Metadata (width/height) and variant buffers\n */\nexport async function processImage(\n buffer: Buffer,\n styles: Record<string, ImageStyle>,\n): Promise<ProcessedImage> {\n const meta = await sharp(buffer).metadata()\n const width = meta.width ?? 0\n const height = meta.height ?? 0\n\n const variants = new Map<string, ProcessedVariant>()\n\n const entries = Object.entries(styles)\n await Promise.all(\n entries.map(async ([name, style]) => {\n const fmt = style.format ?? 'webp'\n const quality = style.quality ?? 80\n const fit = style.fit ?? 'cover'\n\n const resized = sharp(buffer).resize({\n width: style.width,\n height: style.height,\n fit,\n withoutEnlargement: true,\n })\n\n const { data: variantBuffer, info } = await resized[fmt]({ quality }).toBuffer({\n resolveWithObject: true,\n })\n\n variants.set(name, {\n buffer: variantBuffer,\n format: fmt,\n mimeType: `image/${fmt}`,\n width: info.width,\n height: info.height,\n })\n }),\n )\n\n return { width, height, variants }\n}\n\n/**\n * Extract only image dimensions (no variant generation).\n * Useful for getting width/height when styles are empty.\n */\nexport async function getImageDimensions(\n buffer: Buffer,\n): Promise<{ width: number; height: number }> {\n const meta = await sharp(buffer).metadata()\n return { width: meta.width ?? 0, height: meta.height ?? 0 }\n}\n","/**\n * Variant key derivation convention.\n *\n * Given an original storage key and a style name, produces a deterministic\n * variant key in the same directory.\n *\n * Convention:\n * Original: uploads/2026/02/{uuid}/photo.jpg\n * Variant: uploads/2026/02/{uuid}/thumbnail_photo.webp\n */\n\n/**\n * Derive a variant storage key from the original key + style name.\n *\n * @param originalKey - The original file's storage key\n * @param styleName - The image style name (e.g., 'thumbnail', 'medium')\n * @param format - The variant output format (default: 'webp')\n * @returns The derived variant key\n */\nexport function deriveVariantKey(originalKey: string, styleName: string, format = 'webp'): string {\n const lastSlash = originalKey.lastIndexOf('/')\n const dir = originalKey.substring(0, lastSlash)\n const filename = originalKey.substring(lastSlash + 1)\n const baseName = filename.replace(/\\.[^.]+$/, '')\n return `${dir}/${styleName}_${baseName}.${format}`\n}\n","/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface MediaClientConfig {\n db: PostgresJsDatabase\n storage: StorageClient\n logger?: Logger\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private db: PostgresJsDatabase\n private storage: StorageClient\n private logger?: Logger\n private admin: AdminClient | null = null\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.db = config.db\n this.storage = config.storage\n this.logger = config.logger\n this.imageStyles = config.imageStyles ?? null\n }\n\n private async getAdmin(): Promise<AdminClient> {\n if (!this.admin) {\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n this.admin = new AdminClient({\n entity: Media,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.admin\n }\n\n /**\n * Resolve image styles: settings DB → plugin config → hardcoded defaults.\n * Result is cached; call `invalidateImageStylesCache()` after settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n\n // 1. Try settings DB (source of truth after first boot)\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n const settingsClient = createSettingsClient(imageStylesSettings, { app })\n const stored = await settingsClient.get('imageStyles')\n if (stored && Object.keys(stored).length > 0) {\n this.imageStyles = stored\n return stored\n }\n } catch {\n // Settings not available — fall through to plugin config\n }\n\n // 2. Fall back to plugin config singleton\n try {\n const { getMediaConfig } = await import('./plugin.js')\n const config = getMediaConfig()\n this.imageStyles = config.imageStyles\n return this.imageStyles\n } catch {\n // Plugin not initialized — hardcoded defaults\n const defaults: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n }\n this.imageStyles = defaults\n return defaults\n }\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n this.logger?.debug(\n { style: styleName, key: vKey, width: variant.width, height: variant.height },\n 'Variant uploaded',\n )\n } catch (variantErr: unknown) {\n this.logger?.warn(\n { style: styleName, key: vKey, error: variantErr },\n 'Failed to upload variant (non-fatal)',\n )\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch((metaErr: unknown) => {\n this.logger?.warn(\n { key: fileRecord.key, error: metaErr },\n 'Failed to update original file metadata with variant keys (non-fatal)',\n )\n })\n }\n\n this.logger?.info({ width, height, variants: Object.keys(variantKeys) }, 'Image processed')\n } catch (processErr: unknown) {\n this.logger?.warn(\n { filename: options.filename, error: processErr },\n 'Image processing failed (non-fatal, original saved)',\n )\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n const admin = await this.getAdmin()\n\n try {\n const entityRecord = await admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n } as Record<string, EntityFieldValue>)\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n this.logger?.info(\n { id: entityRecord.id, fileKey: fileRecord.key, mediaType },\n 'Media uploaded',\n )\n\n return {\n media: entityRecord as unknown as MediaRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n this.logger?.error(\n { fileKey: fileRecord.key, error },\n 'Media entity creation failed, rolling back storage uploads',\n )\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch((rollbackErr: unknown) => {\n this.logger?.error(\n { fileKey: fileRecord.key, error: rollbackErr },\n 'Storage rollback also failed',\n )\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n const admin = await this.getAdmin()\n const result = await admin.findById(id, options)\n return result as unknown as MediaRecord | null\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n conditions.push(ilike(table.mimeType, `${options.mimeTypePrefix}%`))\n }\n if (options?.search) {\n const pattern = `%${options.search}%`\n conditions.push(\n sql`(${ilike(table.filename, pattern)} OR ${table.fields} ->> 'title' ILIKE ${pattern})`,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total\n const [countResult] = await this.db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(whereClause)\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items: items as unknown as MediaRecord[],\n total: countResult?.count ?? 0,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n const admin = await this.getAdmin()\n const result = await admin.update(id, data as Record<string, EntityFieldValue>)\n return result as unknown as MediaRecord\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch((err: unknown) => {\n this.logger?.warn({ variantKey: vKey, error: err }, 'Failed to delete variant file')\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch((err: unknown) => {\n this.logger?.error(\n { id, fileKey, error: err },\n 'Failed to delete file from storage after entity deletion',\n )\n })\n\n this.logger?.info(\n { id, fileKey, deletedVariants: variants ? Object.keys(variants).length : 0 },\n 'Media deleted',\n )\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl((record as unknown as MediaRecord).fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const admin = await this.getAdmin()\n const record = await admin.findById(id)\n if (!record) return null\n\n const fileKey = (record as unknown as MediaRecord).fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const admin = await this.getAdmin()\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n (records as unknown as MediaRecord[]).map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Lazy singleton — auto-discovers storage config\n// ---------------------------------------------------------------------------\n\nlet _clientPromise: Promise<MediaClient> | null = null\n\n/**\n * Lazy singleton — returns a shared MediaClient instance.\n * Auto-discovers storage configuration from the storage plugin.\n * Must be called after createApp().\n */\nexport function getMediaClient(): Promise<MediaClient> {\n if (!_clientPromise) {\n _clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n\n const app = getApp()\n const config = getStorageConfig()\n const storage = createStorageClient(config, { app })\n\n return new MediaClient({\n db: app.db.readWrite,\n storage,\n logger: app.logger.child({ media: true }),\n })\n })()\n }\n return _clientPromise\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{behavior as e,defineEntity as t,field as n}from"@murumets-ee/entity";const r=t({name:`media`,fields:{title:n.text({translatable:!0}),alt:n.text({translatable:!0}),description:n.text({translatable:!0}),fileKey:n.text({required:!0,indexed:!0}),filename:n.text({required:!0}),mimeType:n.text({required:!0,indexed:!0}),size:n.number({required:!0,integer:!0}),width:n.number({integer:!0}),height:n.number({integer:!0}),mediaType:n.select({options:[`image`,`video`,`audio`,`document`,`other`],required:!0,indexed:!0})},behaviors:[e.auditable()],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{r as t};
|
|
2
|
+
//# sourceMappingURL=entity-D5P2l05s.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-D5P2l05s.mjs","names":[],"sources":["../src/entity.ts"],"sourcesContent":["/**\n * Pre-defined Media entity.\n *\n * Auto-registered by the media() plugin — users do NOT add this to their entities array.\n * Connected to toolkit_files via the fileKey field (stores the StorageClient file key).\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineConfig({\n * plugins: [storage(), media()],\n * // Media entity is auto-added — no need to list it here\n * })\n * ```\n */\n\nimport { behavior, defineEntity, field } from '@murumets-ee/entity'\n\nexport const Media = defineEntity({\n name: 'media',\n fields: {\n // --- Display metadata (editable, translatable) ---\n title: field.text({ translatable: true }),\n alt: field.text({ translatable: true }),\n description: field.text({ translatable: true }),\n\n // --- File linkage (set on upload, immutable in practice) ---\n /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */\n fileKey: field.text({ required: true, indexed: true }),\n\n // --- File metadata (set on upload) ---\n filename: field.text({ required: true }),\n mimeType: field.text({ required: true, indexed: true }),\n size: field.number({ required: true, integer: true }),\n\n // --- Image-specific metadata (client-measured on upload) ---\n width: field.number({ integer: true }),\n height: field.number({ integer: true }),\n\n // --- Classification (derived from mimeType on upload) ---\n mediaType: field.select({\n options: ['image', 'video', 'audio', 'document', 'other'] as const,\n required: true,\n indexed: true,\n }),\n },\n behaviors: [behavior.auditable()],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"4EAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CACzC,IAAK,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CACvC,YAAa,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CAI/C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CAGtD,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,CAAC,CACxC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CACvD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CAGrD,MAAO,EAAM,OAAO,CAAE,QAAS,GAAM,CAAC,CACtC,OAAQ,EAAM,OAAO,CAAE,QAAS,GAAM,CAAC,CAGvC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,QAAQ,CACzD,SAAU,GACV,QAAS,GACV,CAAC,CACH,CACD,UAAW,CAAC,EAAS,WAAW,CAAC,CACjC,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,cACT,CACF,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{behavior as e,defineEntity as t,field as n}from"@murumets-ee/entity";const r=t({name:`media`,fields:{title:n.text({translatable:!0}),alt:n.text({translatable:!0}),description:n.text({translatable:!0}),fileKey:n.text({required:!0,indexed:!0}),filename:n.text({required:!0}),mimeType:n.text({required:!0,indexed:!0}),size:n.number({required:!0,integer:!0}),width:n.number({integer:!0}),height:n.number({integer:!0}),mediaType:n.select({options:[`image`,`video`,`audio`,`document`,`other`],required:!0,indexed:!0})},behaviors:[e.auditable()],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{r as Media};
|
|
2
|
+
//# sourceMappingURL=entity-DZFku8b7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-DZFku8b7.mjs","names":[],"sources":["../src/entity.ts"],"sourcesContent":["/**\n * Pre-defined Media entity.\n *\n * Auto-registered by the media() plugin — users do NOT add this to their entities array.\n * Connected to toolkit_files via the fileKey field (stores the StorageClient file key).\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineConfig({\n * plugins: [storage(), media()],\n * // Media entity is auto-added — no need to list it here\n * })\n * ```\n */\n\nimport { behavior, defineEntity, field } from '@murumets-ee/entity'\n\nexport const Media = defineEntity({\n name: 'media',\n fields: {\n // --- Display metadata (editable, translatable) ---\n title: field.text({ translatable: true }),\n alt: field.text({ translatable: true }),\n description: field.text({ translatable: true }),\n\n // --- File linkage (set on upload, immutable in practice) ---\n /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */\n fileKey: field.text({ required: true, indexed: true }),\n\n // --- File metadata (set on upload) ---\n filename: field.text({ required: true }),\n mimeType: field.text({ required: true, indexed: true }),\n size: field.number({ required: true, integer: true }),\n\n // --- Image-specific metadata (client-measured on upload) ---\n width: field.number({ integer: true }),\n height: field.number({ integer: true }),\n\n // --- Classification (derived from mimeType on upload) ---\n mediaType: field.select({\n options: ['image', 'video', 'audio', 'document', 'other'] as const,\n required: true,\n indexed: true,\n }),\n },\n behaviors: [behavior.auditable()],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"4EAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CACzC,IAAK,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CACvC,YAAa,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CAI/C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CAGtD,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,CAAC,CACxC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CACvD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,GAAM,CAAC,CAGrD,MAAO,EAAM,OAAO,CAAE,QAAS,GAAM,CAAC,CACtC,OAAQ,EAAM,OAAO,CAAE,QAAS,GAAM,CAAC,CAGvC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,QAAQ,CACzD,SAAU,GACV,QAAS,GACV,CAAC,CACH,CACD,UAAW,CAAC,EAAS,WAAW,CAAC,CACjC,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,cACT,CACF,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}},label:`Image processing presets`})}});export{n as imageStylesSettings};
|
|
2
|
+
//# sourceMappingURL=image-styles-settings-7K_jPNIA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-styles-settings-7K_jPNIA.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEAaA,MAAa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as ImageStyle } from "./types-BV_pOm23.mjs";
|
|
2
|
+
import * as _$_murumets_ee_settings0 from "@murumets-ee/settings";
|
|
3
|
+
|
|
4
|
+
//#region src/image-styles-settings.d.ts
|
|
5
|
+
declare const imageStylesSettings: _$_murumets_ee_settings0.SettingsDefinition<{
|
|
6
|
+
readonly imageStyles: _$_murumets_ee_settings0.JsonSettingConfig<Record<string, ImageStyle>>;
|
|
7
|
+
}>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { imageStylesSettings };
|
|
10
|
+
//# sourceMappingURL=image-styles-settings.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;cAaa,mBAAA,2BAAmB,kBAAA;EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{defineSettings as e,setting as t}from"@murumets-ee/settings";const n=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,schema:{imageStyles:t.json({default:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}},label:`Image processing presets`})}});export{n as imageStylesSettings};
|
|
2
|
+
//# sourceMappingURL=image-styles-settings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings'\nimport type { ImageStyle } from './types.js'\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n label: 'Image processing presets',\n }),\n },\n})\n"],"mappings":"oEAaA,MAAa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CACD,MAAO,2BACR,CAAC,CACH,CACF,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/** Named image processing preset — defines how to resize/convert an image variant */
|
|
5
|
+
interface ImageStyle {
|
|
6
|
+
/** Max width in pixels (omit for height-only constraint) */
|
|
7
|
+
width?: number;
|
|
8
|
+
/** Max height in pixels (omit for height-only constraint) */
|
|
9
|
+
height?: number;
|
|
10
|
+
/** Resize strategy (default: 'cover') */
|
|
11
|
+
fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
|
|
12
|
+
/** Output format (default: 'webp') */
|
|
13
|
+
format?: 'webp' | 'jpeg' | 'png' | 'avif';
|
|
14
|
+
/** Output quality 1–100 (default: 80) */
|
|
15
|
+
quality?: number;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/image-styles/types.d.ts
|
|
19
|
+
interface ImageStylesManagerLabels {
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
addStyle?: string;
|
|
23
|
+
editStyle?: string;
|
|
24
|
+
styleName?: string;
|
|
25
|
+
width?: string;
|
|
26
|
+
height?: string;
|
|
27
|
+
fit?: string;
|
|
28
|
+
format?: string;
|
|
29
|
+
quality?: string;
|
|
30
|
+
save?: string;
|
|
31
|
+
cancel?: string;
|
|
32
|
+
delete?: string;
|
|
33
|
+
deleteConfirmTitle?: string;
|
|
34
|
+
deleteConfirmDescription?: string;
|
|
35
|
+
systemBadge?: string;
|
|
36
|
+
noStyles?: string;
|
|
37
|
+
regenerate?: string;
|
|
38
|
+
regenerateDescription?: string;
|
|
39
|
+
regenerating?: string;
|
|
40
|
+
saving?: string;
|
|
41
|
+
px?: string;
|
|
42
|
+
}
|
|
43
|
+
interface ImageStylesManagerClassNames {
|
|
44
|
+
root?: string;
|
|
45
|
+
header?: string;
|
|
46
|
+
table?: string;
|
|
47
|
+
dialog?: string;
|
|
48
|
+
actions?: string;
|
|
49
|
+
regenerateSection?: string;
|
|
50
|
+
}
|
|
51
|
+
interface ImageStylesManagerProps {
|
|
52
|
+
/** Current image styles from server (required, server-first pattern) */
|
|
53
|
+
initialStyles: Record<string, ImageStyle>;
|
|
54
|
+
/** API base path for media admin routes (default: '/api/admin/media') */
|
|
55
|
+
apiBasePath?: string;
|
|
56
|
+
/** i18n labels with English defaults */
|
|
57
|
+
labels?: ImageStylesManagerLabels;
|
|
58
|
+
/** Per-element class overrides */
|
|
59
|
+
classNames?: ImageStylesManagerClassNames;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/image-styles/image-styles-manager.d.ts
|
|
63
|
+
declare function ImageStylesManager({
|
|
64
|
+
initialStyles,
|
|
65
|
+
apiBasePath,
|
|
66
|
+
labels: userLabels,
|
|
67
|
+
classNames
|
|
68
|
+
}: ImageStylesManagerProps): _$react_jsx_runtime0.JSX.Element;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps };
|
|
71
|
+
//# sourceMappingURL=image-styles.d.mts.map
|
|
@@ -0,0 +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"],"mappings":";;;;UAkFiB,UAAA;ECtC0B;EDwCzC,KAAA;;EAEA,MAAA;EEvBc;EFyBd,GAAA;;EAEA,MAAA;EEzBA;EF2BA,OAAA;AAAA;;;UC1Fe,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,UAAA;EACA,qBAAA;EACA,YAAA;EACA,MAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;EACA,iBAAA;AAAA;AAAA,UAGe,uBAAA;EArBf;EAuBA,aAAA,EAAe,MAAA,SAAe,UAAA;EArB9B;EAuBA,WAAA;EArBA;EAuBA,MAAA,GAAS,wBAAA;EArBT;EAuBA,UAAA,GAAa,4BAAA;AAAA;;;iBCmBC,kBAAA,CAAA;EACd,aAAA;EACA,WAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,GAAuB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{Badge as e,Button as t,Dialog as n,DialogContent as r,DialogDescription as i,DialogFooter as a,DialogHeader as o,DialogTitle as ee,Input as s,Label as c,Select as l,Table as te,TableBody as ne,TableCell as u,TableHead as d,TableHeader as re,TableRow as f,cn as p}from"@murumets-ee/ui";import{Loader2 as m,Pencil as ie,Plus as ae,RefreshCw as oe,Trash2 as se}from"lucide-react";import{useCallback as h,useState as g}from"react";import{Fragment as _,jsx as v,jsxs as y}from"react/jsx-runtime";const ce=[`cover`,`contain`,`inside`,`outside`,`fill`],le=[`webp`,`jpeg`,`png`,`avif`],ue={title:`Image Styles`,description:`Configure image processing presets. Variants are generated on upload for each style. Use "Regenerate" to update existing images.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,regenerate:`Regenerate All Variants`,regenerateDescription:`Reprocess all images with the current styles. This may take a while for large libraries.`,regenerating:`Regenerating...`,saving:`Saving...`,px:`px`};function b({initialStyles:b,apiBasePath:x=`/api/admin/media`,labels:de,classNames:S}){let C={...ue,...de},[w,fe]=g(b),[T,E]=g(!1),[D,O]=g(null),[pe,k]=g(!1),[A,j]=g(null),[M,N]=g(``),[P,F]=g(``),[I,L]=g(``),[R,z]=g(`cover`),[B,V]=g(`webp`),[H,U]=g(`80`),[W,G]=g(null),[me,K]=g(!1),[q,J]=g(null),[Y,X]=g(!1),[Z,he]=g(null),Q=h(async e=>{E(!0),O(null);try{let t=await fetch(`${x}/settings`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({imageStyles:e})});if(!t.ok){let e=await t.json();throw Error(e.error??`Save failed (${t.status})`)}fe(e)}catch(e){throw O(e instanceof Error?e.message:`Save failed`),e}finally{E(!1)}},[x]),ge=h(()=>{j(null),N(``),F(``),L(``),z(`cover`),V(`webp`),U(`80`),G(null),k(!0)},[]),_e=h(e=>{let t=w[e];t&&(j(e),N(e),F(t.width?.toString()??``),L(t.height?.toString()??``),z(t.fit??`cover`),V(t.format??`webp`),U((t.quality??80).toString()),G(null),k(!0))},[w]),ve=h(async()=>{G(null);let e=M.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){G(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==A&&w[e]){G(`A style named "${e}" already exists`);return}let t=P?Number.parseInt(P,10):void 0,n=I?Number.parseInt(I,10):void 0;if(!t&&!n){G(`At least width or height is required`);return}if(t!==void 0&&(Number.isNaN(t)||t<=0)){G(`Width must be a positive number`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){G(`Height must be a positive number`);return}let r=Number.parseInt(H,10);if(Number.isNaN(r)||r<1||r>100){G(`Quality must be 1-100`);return}let i={...t?{width:t}:{},...n?{height:n}:{},fit:R,format:B,quality:r},a={...w};A&&A!==e&&delete a[A],a[e]=i;try{await Q(a),k(!1)}catch{}},[w,A,M,P,I,R,B,H,Q]),ye=h(e=>{J(e),K(!0)},[]),be=h(async()=>{if(!q)return;let e={...w};delete e[q];try{await Q(e),K(!1),J(null)}catch{}},[q,w,Q]),xe=h(async()=>{X(!0),he(null);try{let e=await fetch(`${x}/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!e.ok){let t=await e.json();throw Error(t.error??`Regeneration failed (${e.status})`)}he(await e.json())}catch(e){O(e instanceof Error?e.message:`Regeneration failed`)}finally{X(!1)}},[x]),$=Object.entries(w).sort(([e],[t])=>e.localeCompare(t));return y(`div`,{className:p(`space-y-6`,S?.root),children:[y(`div`,{className:p(`flex items-start justify-between`,S?.header),children:[y(`div`,{children:[v(`h2`,{className:`text-2xl font-semibold tracking-tight`,children:C.title}),v(`p`,{className:`text-sm text-muted-foreground mt-1`,children:C.description})]}),y(t,{onClick:ge,size:`sm`,children:[v(ae,{className:`h-4 w-4 mr-1`}),C.addStyle]})]}),D&&v(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:D}),$.length===0?v(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:C.noStyles}):v(`div`,{className:p(`rounded-md border`,S?.table),children:y(te,{children:[v(re,{children:y(f,{children:[v(d,{children:C.styleName}),v(d,{className:`text-center`,children:C.width}),v(d,{className:`text-center`,children:C.height}),v(d,{className:`text-center`,children:C.fit}),v(d,{className:`text-center`,children:C.format}),v(d,{className:`text-center`,children:C.quality}),v(d,{className:`w-24`})]})}),v(ne,{children:$.map(([n,r])=>y(f,{children:[y(u,{className:`font-mono text-sm`,children:[n,n===`thumbnail`&&v(e,{variant:`secondary`,className:`ml-2 text-xs`,children:C.systemBadge})]}),v(u,{className:`text-center tabular-nums`,children:r.width?`${r.width}${C.px}`:`—`}),v(u,{className:`text-center tabular-nums`,children:r.height?`${r.height}${C.px}`:`—`}),v(u,{className:`text-center text-sm text-muted-foreground`,children:r.fit??`cover`}),v(u,{className:`text-center text-sm text-muted-foreground`,children:r.format??`webp`}),v(u,{className:`text-center tabular-nums`,children:r.quality??80}),v(u,{children:y(`div`,{className:p(`flex items-center gap-1 justify-end`,S?.actions),children:[v(t,{variant:`ghost`,size:`sm`,onClick:()=>_e(n),title:C.editStyle,children:v(ie,{className:`h-3.5 w-3.5`})}),v(t,{variant:`ghost`,size:`sm`,onClick:()=>ye(n),title:C.delete,className:`text-destructive hover:text-destructive`,children:v(se,{className:`h-3.5 w-3.5`})})]})})]},n))})]})}),y(`div`,{className:p(`rounded-md border p-4 space-y-3`,S?.regenerateSection),children:[y(`div`,{className:`flex items-center justify-between`,children:[y(`div`,{children:[v(`h3`,{className:`text-sm font-medium`,children:C.regenerate}),v(`p`,{className:`text-xs text-muted-foreground mt-0.5`,children:C.regenerateDescription})]}),v(t,{variant:`outline`,size:`sm`,onClick:xe,disabled:Y||$.length===0,children:Y?y(_,{children:[v(m,{className:`h-4 w-4 mr-1 animate-spin`}),C.regenerating]}):y(_,{children:[v(oe,{className:`h-4 w-4 mr-1`}),C.regenerate]})})]}),Z&&y(`div`,{className:`text-sm text-muted-foreground bg-muted/50 rounded px-3 py-2`,children:[`Processed `,Z.processed,` of `,Z.total,` images`,Z.skipped>0&&`, ${Z.skipped} skipped`,Z.errors>0&&y(`span`,{className:`text-destructive`,children:[`, `,Z.errors,` errors`]})]})]}),v(n,{open:pe,onOpenChange:k,children:y(r,{className:S?.dialog,children:[y(o,{children:[v(ee,{children:A?C.editStyle:C.addStyle}),v(i,{children:A?`Editing "${A}" image style.`:`Create a new image processing preset.`})]}),y(`div`,{className:`space-y-4 py-2`,children:[W&&v(`div`,{className:`text-sm text-destructive`,children:W}),y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-name`,children:C.styleName}),v(s,{id:`style-name`,value:M,onChange:e=>N(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!A})]}),y(`div`,{className:`grid grid-cols-2 gap-4`,children:[y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-width`,children:[C.width,` (`,C.px,`)`]}),v(s,{id:`style-width`,type:`number`,value:P,onChange:e=>F(e.target.value),placeholder:`e.g. 200`,min:1})]}),y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-height`,children:[C.height,` (`,C.px,`)`]}),v(s,{id:`style-height`,type:`number`,value:I,onChange:e=>L(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),y(`div`,{className:`grid grid-cols-2 gap-4`,children:[y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-fit`,children:C.fit}),v(l,{id:`style-fit`,value:R,onChange:e=>z(e.target.value),children:ce.map(e=>v(`option`,{value:e,children:e},e))})]}),y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-format`,children:C.format}),v(l,{id:`style-format`,value:B,onChange:e=>V(e.target.value),children:le.map(e=>v(`option`,{value:e,children:e},e))})]})]}),y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-quality`,children:[C.quality,` (1-100)`]}),v(s,{id:`style-quality`,type:`number`,value:H,onChange:e=>U(e.target.value),min:1,max:100})]})]}),y(a,{children:[v(t,{variant:`outline`,onClick:()=>k(!1),children:C.cancel}),v(t,{onClick:ve,disabled:T,children:T?y(_,{children:[v(m,{className:`h-4 w-4 mr-1 animate-spin`}),C.saving]}):C.save})]})]})}),v(n,{open:me,onOpenChange:K,children:y(r,{children:[y(o,{children:[v(ee,{children:C.deleteConfirmTitle}),y(i,{children:[C.deleteConfirmDescription,q&&v(`span`,{className:`block mt-2 font-mono text-foreground`,children:q})]})]}),y(a,{children:[v(t,{variant:`outline`,onClick:()=>K(!1),children:C.cancel}),y(t,{variant:`destructive`,onClick:be,disabled:T,children:[T?v(m,{className:`h-4 w-4 mr-1 animate-spin`}):null,C.delete]})]})]})})]})}export{b as ImageStylesManager};
|
|
3
|
+
//# sourceMappingURL=image-styles.mjs.map
|