@murumets-ee/media 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/{client-BZY26jaX.mjs → client-BL8RADZv.mjs} +2 -2
- package/dist/{client-BZY26jaX.mjs.map → client-BL8RADZv.mjs.map} +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/{entity-B75A4L3p.mjs → entity-Caba_NFw.mjs} +1 -1
- package/dist/{entity-B75A4L3p.mjs.map → entity-Caba_NFw.mjs.map} +1 -1
- package/dist/{entity-D1YaFEh8.mjs → entity-v0J9plyH.mjs} +1 -1
- package/dist/{entity-D1YaFEh8.mjs.map → entity-v0J9plyH.mjs.map} +1 -1
- package/dist/{image-styles-settings-B4QOt7Ve.mjs → image-styles-settings-ClIA2MJI.mjs} +1 -1
- package/dist/{image-styles-settings-B4QOt7Ve.mjs.map → image-styles-settings-ClIA2MJI.mjs.map} +1 -1
- package/dist/image-styles-settings.d.mts +1 -1
- package/dist/image-styles-settings.d.mts.map +1 -1
- package/dist/image-styles-settings.mjs.map +1 -1
- package/dist/image-styles.d.mts.map +1 -1
- package/dist/image-styles.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs.map +1 -1
- package/dist/{plugin-DyYikOz_.mjs → plugin-_fxTC79v.mjs} +2 -2
- package/dist/{plugin-DyYikOz_.mjs.map → plugin-_fxTC79v.mjs.map} +1 -1
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/processing.d.mts +1 -1
- package/dist/processing.d.mts.map +1 -1
- package/dist/processing.mjs +1 -1
- package/dist/query-client.d.mts +1 -1
- package/dist/query-client.d.mts.map +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/query-client.mjs.map +1 -1
- package/dist/ref.d.mts.map +1 -1
- package/dist/ref.mjs.map +1 -1
- package/dist/{regenerate-variants-DtA5pKjj.mjs → regenerate-variants-BysyeIoA.mjs} +2 -2
- package/dist/{regenerate-variants-DtA5pKjj.mjs.map → regenerate-variants-BysyeIoA.mjs.map} +1 -1
- package/dist/{regenerate-variants-DJIFSrdC.mjs → regenerate-variants-CtzCRkKd.mjs} +2 -2
- package/dist/{regenerate-variants-DJIFSrdC.mjs.map → regenerate-variants-CtzCRkKd.mjs.map} +1 -1
- package/dist/{resolve-image-styles-BI3pvJBZ.mjs → resolve-image-styles-iN9JbZYf.mjs} +1 -1
- package/dist/{resolve-image-styles-BI3pvJBZ.mjs.map → resolve-image-styles-iN9JbZYf.mjs.map} +1 -1
- package/dist/{resolve-image-styles-B2pbn_AH.mjs → resolve-image-styles-qxVgkNT_.mjs} +2 -2
- package/dist/{resolve-image-styles-B2pbn_AH.mjs.map → resolve-image-styles-qxVgkNT_.mjs.map} +1 -1
- package/dist/{routes-BbNpbNjb.mjs → routes-CT4-zLOt.mjs} +2 -2
- package/dist/{routes-BbNpbNjb.mjs.map → routes-CT4-zLOt.mjs.map} +1 -1
- package/dist/{types-CqjShq2U.d.mts → types-Bv5ATAgT.d.mts} +1 -1
- package/dist/types-Bv5ATAgT.d.mts.map +1 -0
- package/dist/{usage-DmdsFJpK.mjs → usage-CVAqkS6h.mjs} +1 -1
- package/dist/{usage-DmdsFJpK.mjs.map → usage-CVAqkS6h.mjs.map} +1 -1
- package/dist/usage.mjs.map +1 -1
- package/dist/{variant-key-CFr3fR-n.mjs → variant-key-CLlac3_1.mjs} +1 -1
- package/dist/{variant-key-CFr3fR-n.mjs.map → variant-key-CLlac3_1.mjs.map} +1 -1
- package/dist/{variant-key-gVMhzKyv.mjs → variant-key-CyI9Qq-f.mjs} +1 -1
- package/dist/{variant-key-gVMhzKyv.mjs.map → variant-key-CyI9Qq-f.mjs.map} +1 -1
- package/package.json +14 -14
- package/dist/types-CqjShq2U.d.mts.map +0 -1
package/dist/admin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAslBgB,WAAA,CAAA,GAAe,
|
|
1
|
+
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAslBgB,WAAA,CAAA,GAAe,UAAU"}
|
package/dist/admin.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./routes-
|
|
1
|
+
import{t as e}from"./routes-CT4-zLOt.mjs";export{e as mediaRoutes};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import"./entity-
|
|
2
|
-
//# sourceMappingURL=client-
|
|
1
|
+
import"./entity-Caba_NFw.mjs";import{n as e,r as t,t as n}from"./variant-key-CLlac3_1.mjs";import"server-only";var r=class{admin;storage;imageStyles;constructor(e){this.admin=e.admin,this.storage=e.storage,this.imageStyles=e.imageStyles??null}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-qxVgkNT_.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}invalidateImageStylesCache(){this.imageStyles=null}async upload(r,o){let s=await this.storage.upload(r,{filename:o.filename,mimeType:o.mimeType,size:o.size,visibility:o.visibility,uploadedBy:o.uploadedBy}),c=o.width??null,l=o.height??null,u={};if(r instanceof Buffer&&e(o.mimeType))try{let e=await t(r,await this.resolveImageStyles());c=e.width,l=e.height;let i=s.visibility;await Promise.all([...e.variants.entries()].map(async([e,t])=>{let r=n(s.key,e,t.format);try{await this.storage.upload(t.buffer,{key:r,filename:`${e}_${o.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:i,metadata:{variantOf:s.key,style:e},uploadedBy:o.uploadedBy}),u[e]=r}catch{}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(()=>{})}catch{}let d=i(o.mimeType);try{return{media:await this.admin.create({title:o.title??a(o.filename),alt:o.alt??null,description:o.description??null,fileKey:s.key,filename:o.filename,mimeType:o.mimeType,size:o.size,width:c,height:l,mediaType:d}),url:await this.storage.getUrl(s.key)}}catch(e){for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(()=>{}),e}}async findById(e,t){return this.admin.findById(e,t)}async findMany(e){let{schemaRegistry:t}=await import(`@murumets-ee/db`),{and:n,asc:r,desc:i,eq:a,ilike:o,or:s,sql:c}=await import(`drizzle-orm`),l=t.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(a(l.mediaType,e.mediaType)),e?.mimeTypePrefix){let t=e.mimeTypePrefix.replace(/[\\%_]/g,`\\$&`);u.push(o(l.mimeType,`${t}%`))}if(e?.search){let t=`%${e.search.replace(/[\\%_]/g,`\\$&`)}%`;u.push(s(o(l.filename,t),c`${l.fields} ->> 'title' ILIKE ${t}`))}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?n(...u):void 0,m=await this.admin.count({where:p}),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?r:i;return{items:await this.admin.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m,limit:d,offset:f}}async update(e,t){return this.admin.update(e,t)}async delete(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);let n=t.fileKey;await this.admin.delete(e);let r=(await this.storage.getMetadata(n))?.metadata?.variants;if(r)for(let e of Object.values(r))await this.storage.delete(e).catch(()=>{});await this.storage.delete(n).catch(()=>{})}async getUrl(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let{schemaRegistry:t}=await import(`@murumets-ee/db`),{inArray:n}=await import(`drizzle-orm`),r=t.get(`media`);if(!r)return new Map;let i=await this.admin.findMany({where:n(r.id,e),limit:e.length}),a=new Map;return await Promise.all(i.map(async e=>{let t=await this.storage.getUrl(e.fileKey);a.set(e.id,t)})),a}async getVariantUrl(e,t){let r=await this.admin.findById(e);if(!r)return null;let i=r.fileKey,a=(await this.resolveImageStyles())[t];if(a){let e=n(i,t,a.format??`webp`);try{return await this.storage.getUrl(e)}catch{}}try{return await this.storage.getUrl(i)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let{schemaRegistry:r}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=r.get(`media`);if(!a)return new Map;let o=await this.admin.findMany({where:i(a.id,e),limit:e.length}),s=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(o.map(async e=>{if(s){let r=n(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(r);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};function i(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}function a(e){return e.replace(/\.[^.]+$/,``).replace(/[-_]/g,` `)}export{r as MediaClient};
|
|
2
|
+
//# sourceMappingURL=client-BL8RADZv.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-BZY26jaX.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n } catch {\n // Variant upload failure is non-fatal — original is saved\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch(() => {\n // Storage rollback failure is already being handled — propagate original error\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total via AdminClient\n const total = await this.admin.count({ where: whereClause })\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n return this.admin.update(id, data)\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already deleted\n })\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-BL8RADZv.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n } catch {\n // Variant upload failure is non-fatal — original is saved\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch(() => {\n // Storage rollback failure is already being handled — propagate original error\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total via AdminClient\n const total = await this.admin.count({ where: whereClause })\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n return this.admin.update(id, data)\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already deleted\n })\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as MediaRecord, c as MediaUploadResult, l as Media, n as MediaListOptions, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-
|
|
1
|
+
import { a as MediaRecord, c as MediaUploadResult, l as Media, n as MediaListOptions, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-Bv5ATAgT.mjs";
|
|
2
2
|
import { AdminClient } from "@murumets-ee/entity/admin";
|
|
3
3
|
import { StorageClient } from "@murumets-ee/storage";
|
|
4
4
|
|
package/dist/client.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;KA0BK,WAAA,UAAqB,KAAA,CAAM,
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;KA0BK,WAAA,UAAqB,KAAA,CAAM,SAAS;AAAA,UAExB,iBAAA;EACf,KAAA,EAAO,WAAA,CAAY,WAAA;EACnB,OAAA,EAAS,aAAA;EAEoB;EAA7B,WAAA,GAAc,MAAA,SAAe,UAAA;AAAA;AAAA,cAGlB,WAAA;EAAA,QACH,KAAA;EAAA,QACA,OAAA;EAAA,QACA,WAAA;cAEI,MAAA,EAAQ,iBAAA;EAVX;;;;;EAAA,QAqBK,kBAAA;EAhBH;EA0BX,0BAAA,CAAA;;;;;;;;;;;EAkBM,MAAA,CACJ,IAAA,EAAM,MAAA,GAAS,cAAA,CAAe,UAAA,GAC9B,OAAA,EAAS,kBAAA,GACR,OAAA,CAAQ,iBAAA;EA4GL,QAAA,CAAS,EAAA,UAAY,OAAA;IAAY,MAAA;EAAA,IAAoB,OAAA,CAAQ,WAAA;EAI7D,QAAA,CAAS,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAwD9C,MAAA,CACJ,EAAA,UACA,IAAA;IAAQ,KAAA;IAAgB,GAAA;IAAc,WAAA;EAAA,IACrC,OAAA,CAAQ,WAAA;EA4HqD;;;EArH1D,MAAA,CAAO,EAAA,WAAa,OAAA;EAhOlB;;;;EAsQF,MAAA,CAAO,EAAA,WAAa,OAAA;EAlQd;;;;EA6QN,OAAA,CAAQ,GAAA,aAAgB,OAAA,CAAQ,GAAA;EArOrB;;;;;;;;EAuQX,aAAA,CAAc,EAAA,UAAY,SAAA,WAAoB,OAAA;EAzJb;;;;;;;;EA2LjC,cAAA,CAAe,GAAA,YAAe,SAAA,WAAoB,OAAA,CAAQ,GAAA;AAAA;;;;;iBAyE5C,iBAAA,CAAkB,OAAA,EAAS,aAAA,GAAgB,OAAA,CAAQ,WAAA;;;;;;;;;;iBAuCnD,cAAA,CAAA,GAAkB,OAAO,CAAC,WAAA"}
|
package/dist/client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./entity-
|
|
1
|
+
import{t as e}from"./entity-v0J9plyH.mjs";import{i as t,r as n,t as r}from"./variant-key-CyI9Qq-f.mjs";import"server-only";var i=class{admin;storage;imageStyles;constructor(e){this.admin=e.admin,this.storage=e.storage,this.imageStyles=e.imageStyles??null}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;let{getApp:e}=await import(`@murumets-ee/core`),{resolveImageStyles:t}=await import(`./resolve-image-styles-iN9JbZYf.mjs`),n=e();return this.imageStyles=await t(n,n.logger),this.imageStyles}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&&n(i.mimeType))try{let n=await t(e,await this.resolveImageStyles());c=n.width,l=n.height;let a=s.visibility;await Promise.all([...n.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}catch{}})),Object.keys(u).length>0&&await this.storage.updateMetadata(s.key,{metadata:{...s.metadata??{},variants:u}}).catch(()=>{})}catch{}let d=a(i.mimeType);try{return{media:await this.admin.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}),url:await this.storage.getUrl(s.key)}}catch(e){for(let e of Object.values(u))await this.storage.delete(e).catch(()=>{});throw await this.storage.delete(s.key).catch(()=>{}),e}}async findById(e,t){return this.admin.findById(e,t)}async findMany(e){let{schemaRegistry:t}=await import(`@murumets-ee/db`),{and:n,asc:r,desc:i,eq:a,ilike:o,or:s,sql:c}=await import(`drizzle-orm`),l=t.get(`media`);if(!l)throw Error(`Media schema not registered. Is the media() plugin loaded?`);let u=[];if(e?.mediaType&&u.push(a(l.mediaType,e.mediaType)),e?.mimeTypePrefix){let t=e.mimeTypePrefix.replace(/[\\%_]/g,`\\$&`);u.push(o(l.mimeType,`${t}%`))}if(e?.search){let t=`%${e.search.replace(/[\\%_]/g,`\\$&`)}%`;u.push(s(o(l.filename,t),c`${l.fields} ->> 'title' ILIKE ${t}`))}let d=e?.limit??50,f=e?.offset??0,p=u.length>0?n(...u):void 0,m=await this.admin.count({where:p}),h=e?.orderBy===`filename`?l.filename:l.createdAt,g=(e?.orderDirection??`desc`)===`asc`?r:i;return{items:await this.admin.findMany({where:p,limit:d,offset:f,orderBy:g(h)}),total:m,limit:d,offset:f}}async update(e,t){return this.admin.update(e,t)}async delete(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);let n=t.fileKey;await this.admin.delete(e);let r=(await this.storage.getMetadata(n))?.metadata?.variants;if(r)for(let e of Object.values(r))await this.storage.delete(e).catch(()=>{});await this.storage.delete(n).catch(()=>{})}async getUrl(e){let t=await this.admin.findById(e);if(!t)throw Error(`Media not found: ${e}`);return this.storage.getUrl(t.fileKey)}async getUrls(e){if(e.length===0)return new Map;let{schemaRegistry:t}=await import(`@murumets-ee/db`),{inArray:n}=await import(`drizzle-orm`),r=t.get(`media`);if(!r)return new Map;let i=await this.admin.findMany({where:n(r.id,e),limit:e.length}),a=new Map;return await Promise.all(i.map(async e=>{let t=await this.storage.getUrl(e.fileKey);a.set(e.id,t)})),a}async getVariantUrl(e,t){let n=await this.admin.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{schemaRegistry:n}=await import(`@murumets-ee/db`),{inArray:i}=await import(`drizzle-orm`),a=n.get(`media`);if(!a)return new Map;let o=await this.admin.findMany({where:i(a.id,e),limit:e.length}),s=(await this.resolveImageStyles())[t],c=new Map;return await Promise.all(o.map(async e=>{if(s){let n=r(e.fileKey,t,s.format??`webp`);try{let t=await this.storage.getUrl(n);c.set(e.id,t);return}catch{}}try{let t=await this.storage.getUrl(e.fileKey);c.set(e.id,t)}catch{}})),c}};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,` `)}async function s(t){let{createAdminClient:n}=await import(`@murumets-ee/core/clients`);return new i({admin:n(e),storage:t})}let c=null;async function l(){return c||=(async()=>{let{getApp:e}=await import(`@murumets-ee/core`),{createStorageClient:t}=await import(`@murumets-ee/storage`),{getStorageConfig:n}=await import(`@murumets-ee/storage/plugin`),r=e();return t(n(),{app:r})})(),c}async function u(){let{createAdminClient:t}=await import(`@murumets-ee/core/clients`),n=await l();return new i({admin:t(e),storage:n})}export{i as MediaClient,s as createMediaClient,u as getMediaClient};
|
|
2
2
|
//# sourceMappingURL=client.mjs.map
|
package/dist/client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n } catch {\n // Variant upload failure is non-fatal — original is saved\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch(() => {\n // Storage rollback failure is already being handled — propagate original error\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total via AdminClient\n const total = await this.admin.count({ where: whereClause })\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n return this.admin.update(id, data)\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already deleted\n })\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * MediaClient — wraps AdminClient + StorageClient for media management.\n *\n * Usage:\n * import { createMediaClient } from '@murumets-ee/media/client'\n * const media = await createMediaClient(storageClient)\n * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })\n */\n\nimport 'server-only'\n\nimport type { AdminClient } from '@murumets-ee/entity/admin'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport { Media } from './entity.js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type {\n ImageStyle,\n MediaListOptions,\n MediaListResult,\n MediaRecord,\n MediaType,\n MediaUploadOptions,\n MediaUploadResult,\n} from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\ntype MediaFields = typeof Media.allFields\n\nexport interface MediaClientConfig {\n admin: AdminClient<MediaFields>\n storage: StorageClient\n /** Image styles to generate on upload. Loaded from plugin config if not provided. */\n imageStyles?: Record<string, ImageStyle>\n}\n\nexport class MediaClient {\n private admin: AdminClient<MediaFields>\n private storage: StorageClient\n private imageStyles: Record<string, ImageStyle> | null\n\n constructor(config: MediaClientConfig) {\n this.admin = config.admin\n this.storage = config.storage\n this.imageStyles = config.imageStyles ?? null\n }\n\n /**\n * Resolve image styles via the shared waterfall (settings DB → plugin\n * config → hardcoded defaults) and cache the result on this instance.\n * Call `invalidateImageStylesCache()` after a settings update.\n */\n private async resolveImageStyles(): Promise<Record<string, ImageStyle>> {\n if (this.imageStyles) return this.imageStyles\n const { getApp } = await import('@murumets-ee/core')\n const { resolveImageStyles } = await import('./resolve-image-styles.js')\n const app = getApp()\n this.imageStyles = await resolveImageStyles(app, app.logger)\n return this.imageStyles\n }\n\n /** Clear cached styles so next access re-reads from settings DB. */\n invalidateImageStylesCache(): void {\n this.imageStyles = null\n }\n\n // ---------------------------------------------------------------\n // Upload — the key convenience method\n // ---------------------------------------------------------------\n\n /**\n * Upload a file and create a media entity record in one step.\n * 1. Uploads original to storage (StorageClient)\n * 2. If image: extracts dimensions via Sharp + generates variants\n * 3. Creates media entity record (AdminClient)\n * 4. Returns the media record + URL\n *\n * Rolls back storage upload if entity creation fails.\n * Variant generation failures are logged but don't fail the upload.\n */\n async upload(\n data: Buffer | ReadableStream<Uint8Array>,\n options: MediaUploadOptions,\n ): Promise<MediaUploadResult> {\n // 1. Upload original file to storage\n const fileRecord = await this.storage.upload(data, {\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n visibility: options.visibility,\n uploadedBy: options.uploadedBy,\n })\n\n // 2. Image processing — extract dimensions + generate variants\n let width = options.width ?? null\n let height = options.height ?? null\n const variantKeys: Record<string, string> = {}\n\n if (data instanceof Buffer && isProcessableImage(options.mimeType)) {\n try {\n const styles = await this.resolveImageStyles()\n const processed = await processImage(data, styles)\n\n // Set dimensions from Sharp metadata\n width = processed.width\n height = processed.height\n\n // Upload each variant to storage\n const visibility = fileRecord.visibility\n await Promise.all(\n [...processed.variants.entries()].map(async ([styleName, variant]) => {\n const vKey = deriveVariantKey(fileRecord.key, styleName, variant.format)\n try {\n await this.storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${options.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: fileRecord.key, style: styleName },\n uploadedBy: options.uploadedBy,\n })\n variantKeys[styleName] = vKey\n } catch {\n // Variant upload failure is non-fatal — original is saved\n }\n }),\n )\n\n // Store variant keys in original file's metadata\n if (Object.keys(variantKeys).length > 0) {\n await this.storage\n .updateMetadata(fileRecord.key, {\n metadata: {\n ...(fileRecord.metadata ?? {}),\n variants: variantKeys,\n },\n })\n .catch(() => {\n // Metadata update failure is non-fatal\n })\n }\n } catch {\n // Image processing failure is non-fatal — original is saved\n }\n }\n\n // 3. Create media entity record\n const mediaType = deriveMediaType(options.mimeType)\n\n try {\n const entityRecord = await this.admin.create({\n title: options.title ?? deriveTitle(options.filename),\n alt: options.alt ?? null,\n description: options.description ?? null,\n fileKey: fileRecord.key,\n filename: options.filename,\n mimeType: options.mimeType,\n size: options.size,\n width,\n height,\n mediaType,\n })\n\n // 4. Get URL\n const url = await this.storage.getUrl(fileRecord.key)\n\n return {\n media: entityRecord,\n url,\n }\n } catch (error) {\n // Rollback: delete variants + original if entity creation fails\n\n // Delete variants first (best-effort)\n for (const vKey of Object.values(variantKeys)) {\n await this.storage.delete(vKey).catch(() => {})\n }\n\n // Delete original\n await this.storage.delete(fileRecord.key).catch(() => {\n // Storage rollback failure is already being handled — propagate original error\n })\n throw error\n }\n }\n\n // ---------------------------------------------------------------\n // CRUD delegation\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: { locale?: string }): Promise<MediaRecord | null> {\n return this.admin.findById(id, options)\n }\n\n async findMany(options?: MediaListOptions): Promise<MediaListResult> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { and, asc, desc, eq, ilike, or, sql } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered. Is the media() plugin loaded?')\n\n // Build where conditions\n const conditions = []\n\n if (options?.mediaType) {\n conditions.push(eq(table.mediaType, options.mediaType))\n }\n if (options?.mimeTypePrefix) {\n // Escape ILIKE wildcards (%, _) in user input to prevent pattern injection\n const escaped = options.mimeTypePrefix.replace(/[\\\\%_]/g, '\\\\$&')\n conditions.push(ilike(table.mimeType, `${escaped}%`))\n }\n if (options?.search) {\n // Escape ILIKE wildcards in user input, then wrap with %...%\n const escaped = options.search.replace(/[\\\\%_]/g, '\\\\$&')\n const pattern = `%${escaped}%`\n conditions.push(\n or(\n ilike(table.filename, pattern),\n sql`${table.fields} ->> 'title' ILIKE ${pattern}`,\n )!,\n )\n }\n\n const limit = options?.limit ?? 50\n const offset = options?.offset ?? 0\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined\n\n // Count total via AdminClient\n const total = await this.admin.count({ where: whereClause })\n\n // Fetch items via AdminClient for proper DTO shaping\n const orderField = options?.orderBy === 'filename' ? table.filename : table.createdAt\n const orderFn = (options?.orderDirection ?? 'desc') === 'asc' ? asc : desc\n\n const items = await this.admin.findMany({\n where: whereClause,\n limit,\n offset,\n orderBy: orderFn(orderField),\n })\n\n return {\n items,\n total,\n limit,\n offset,\n }\n }\n\n async update(\n id: string,\n data: { title?: string; alt?: string; description?: string },\n ): Promise<MediaRecord> {\n return this.admin.update(id, data)\n }\n\n /**\n * Delete a media entity, its variants, and its original file in storage.\n */\n async delete(id: string): Promise<void> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n const fileKey = record.fileKey\n\n // 1. Delete entity first (ref checking via entity_refs happens here)\n await this.admin.delete(id)\n\n // 2. Look up original file record for variant metadata\n const fileRecord = await this.storage.getMetadata(fileKey)\n const variants = (fileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n\n // 3. Delete variants (best-effort)\n if (variants) {\n for (const vKey of Object.values(variants)) {\n await this.storage.delete(vKey).catch(() => {\n // Variant deletion failure is non-fatal\n })\n }\n }\n\n // 4. Delete original file (best-effort)\n await this.storage.delete(fileKey).catch(() => {\n // Storage deletion failure is non-fatal — entity is already deleted\n })\n }\n\n // ---------------------------------------------------------------\n // URL resolution\n // ---------------------------------------------------------------\n\n /**\n * Get URL for a media entity by its ID.\n * Resolves entity -> fileKey -> storage URL.\n */\n async getUrl(id: string): Promise<string> {\n const record = await this.admin.findById(id)\n if (!record) throw new Error(`Media not found: ${id}`)\n\n return this.storage.getUrl(record.fileKey)\n }\n\n /**\n * Get URLs for multiple media entities (batch).\n * Returns a Map of mediaId -> url.\n */\n async getUrls(ids: string[]): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n }),\n )\n\n return urlMap\n }\n\n /**\n * Get variant URL for a specific image style.\n * Falls back to original URL if the variant doesn't exist.\n *\n * @param id - Media entity ID\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns The variant URL, or original URL as fallback, or null if media not found\n */\n async getVariantUrl(id: string, styleName: string): Promise<string | null> {\n const record = await this.admin.findById(id)\n if (!record) return null\n\n const fileKey = record.fileKey\n\n // Try variant key first\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n if (style) {\n const vKey = deriveVariantKey(fileKey, styleName, style.format ?? 'webp')\n try {\n return await this.storage.getUrl(vKey)\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n return await this.storage.getUrl(fileKey)\n } catch {\n return null\n }\n }\n\n /**\n * Get variant URLs for multiple media entities (batch).\n * Falls back to original URL per item if the variant doesn't exist.\n *\n * @param ids - Media entity IDs\n * @param styleName - Image style name (e.g., 'thumbnail')\n * @returns Map of mediaId -> variant URL (or original URL as fallback)\n */\n async getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>> {\n if (ids.length === 0) return new Map()\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const table = schemaRegistry.get('media')\n if (!table) return new Map()\n\n const records = await this.admin.findMany({\n where: inArray(table.id, ids),\n limit: ids.length,\n })\n\n const styles = await this.resolveImageStyles()\n const style = styles[styleName]\n const urlMap = new Map<string, string>()\n\n await Promise.all(\n records.map(async (record) => {\n // Try variant URL\n if (style) {\n const vKey = deriveVariantKey(record.fileKey, styleName, style.format ?? 'webp')\n try {\n const url = await this.storage.getUrl(vKey)\n urlMap.set(record.id, url)\n return\n } catch {\n // Variant doesn't exist — fall back to original\n }\n }\n\n // Fallback to original\n try {\n const url = await this.storage.getUrl(record.fileKey)\n urlMap.set(record.id, url)\n } catch {\n // Skip — no URL available\n }\n }),\n )\n\n return urlMap\n }\n}\n\n// ---------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------\n\nfunction deriveMediaType(mimeType: string): MediaType {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (\n mimeType === 'application/pdf' ||\n mimeType.startsWith('application/msword') ||\n mimeType.startsWith('application/vnd.')\n ) {\n return 'document'\n }\n return 'other'\n}\n\nfunction deriveTitle(filename: string): string {\n const withoutExt = filename.replace(/\\.[^.]+$/, '')\n return withoutExt.replace(/[-_]/g, ' ')\n}\n\n/**\n * Factory — creates a MediaClient with an explicit StorageClient.\n * Must be called after createApp().\n */\nexport async function createMediaClient(storage: StorageClient): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n\n// ---------------------------------------------------------------------------\n// Per-request MediaClient factory\n// ---------------------------------------------------------------------------\n//\n// NOTE: Storage config is process-global and safe to cache.\n// MediaClient / AdminClient must be built per-request — they carry a\n// context resolver tied to the calling request's user + permissions.\n// A singleton would leak one request's security context across others.\n\nlet _storagePromise: Promise<StorageClient> | null = null\n\nasync function getStorageSingleton(): Promise<StorageClient> {\n if (!_storagePromise) {\n _storagePromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createStorageClient } = await import('@murumets-ee/storage')\n const { getStorageConfig } = await import('@murumets-ee/storage/plugin')\n const app = getApp()\n return createStorageClient(getStorageConfig(), { app })\n })()\n }\n return _storagePromise\n}\n\n/**\n * Returns a fresh MediaClient wired to the current request's context.\n * Must be called after createApp(), inside a request context\n * (withAdminContext, runAsCli, etc.).\n *\n * Despite the name, this is NOT cached — the storage config is cached\n * internally but the MediaClient and its AdminClient are rebuilt per call\n * so the security context resolver attaches to the correct request.\n */\nexport async function getMediaClient(): Promise<MediaClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const storage = await getStorageSingleton()\n const admin = createAdminClient(Media)\n return new MediaClient({ admin, storage })\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{behavior as e,defineEntity as t,field as n,searchable as r}from"@murumets-ee/entity";const i=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(),r({fields:[`filename`,`fileKey`,`mimeType`,`title`,`alt`,`description`],fts:{fields:[`filename`,`title`,`alt`,`description`],language:`simple`},projection:e=>({id:e.id,label:e.title??e.filename,description:e.mimeType})})],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{i as Media};
|
|
2
|
-
//# sourceMappingURL=entity-
|
|
2
|
+
//# sourceMappingURL=entity-Caba_NFw.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-
|
|
1
|
+
{"version":3,"file":"entity-Caba_NFw.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, searchable } 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: [\n behavior.auditable(),\n // filename / fileKey / mimeType live on the main table.\n // title/alt/description are translatable — their tsvector goes on\n // media_translations so non-default-locale callers still get FTS.\n searchable({\n fields: ['filename', 'fileKey', 'mimeType', 'title', 'alt', 'description'],\n fts: {\n fields: ['filename', 'title', 'alt', 'description'],\n language: 'simple',\n },\n projection: (row) => ({\n id: row.id as string,\n label: (row.title as string | undefined) ?? (row.filename as string),\n description: row.mimeType as string,\n }),\n }),\n ],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"4FAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACxC,IAAK,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACtC,YAAa,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EAI9C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGrD,SAAU,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EACvC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EACtD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGpD,MAAO,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EACrC,OAAQ,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EAGtC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,OAAO,EACxD,SAAU,GACV,QAAS,EACX,CAAC,CACH,EACA,UAAW,CACT,EAAS,UAAU,EAInB,EAAW,CACT,OAAQ,CAAC,WAAY,UAAW,WAAY,QAAS,MAAO,aAAa,EACzE,IAAK,CACH,OAAQ,CAAC,WAAY,QAAS,MAAO,aAAa,EAClD,SAAU,QACZ,EACA,WAAa,IAAS,CACpB,GAAI,EAAI,GACR,MAAQ,EAAI,OAAiC,EAAI,SACjD,YAAa,EAAI,QACnB,EACF,CAAC,CACH,EACA,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,aACV,CACF,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{behavior as e,defineEntity as t,field as n,searchable as r}from"@murumets-ee/entity";var i=Object.defineProperty,a=((e,t)=>{let n={};for(var r in e)i(n,r,{get:e[r],enumerable:!0});return t||i(n,Symbol.toStringTag,{value:`Module`}),n})({Media:()=>o});const o=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(),r({fields:[`filename`,`fileKey`,`mimeType`,`title`,`alt`,`description`],fts:{fields:[`filename`,`title`,`alt`,`description`],language:`simple`},projection:e=>({id:e.id,label:e.title??e.filename,description:e.mimeType})})],scope:`global`,access:{view:`public`,create:`group.editor`,update:`group.editor`,delete:`group.admin`}});export{a as n,o as t};
|
|
2
|
-
//# sourceMappingURL=entity-
|
|
2
|
+
//# sourceMappingURL=entity-v0J9plyH.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-
|
|
1
|
+
{"version":3,"file":"entity-v0J9plyH.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, searchable } 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: [\n behavior.auditable(),\n // filename / fileKey / mimeType live on the main table.\n // title/alt/description are translatable — their tsvector goes on\n // media_translations so non-default-locale callers still get FTS.\n searchable({\n fields: ['filename', 'fileKey', 'mimeType', 'title', 'alt', 'description'],\n fts: {\n fields: ['filename', 'title', 'alt', 'description'],\n language: 'simple',\n },\n projection: (row) => ({\n id: row.id as string,\n label: (row.title as string | undefined) ?? (row.filename as string),\n description: row.mimeType as string,\n }),\n }),\n ],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n"],"mappings":"iQAmBA,MAAa,EAAQ,EAAa,CAChC,KAAM,QACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACxC,IAAK,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EACtC,YAAa,EAAM,KAAK,CAAE,aAAc,EAAK,CAAC,EAI9C,QAAS,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGrD,SAAU,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EACvC,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EACtD,KAAM,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,EAAK,CAAC,EAGpD,MAAO,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EACrC,OAAQ,EAAM,OAAO,CAAE,QAAS,EAAK,CAAC,EAGtC,UAAW,EAAM,OAAO,CACtB,QAAS,CAAC,QAAS,QAAS,QAAS,WAAY,OAAO,EACxD,SAAU,GACV,QAAS,EACX,CAAC,CACH,EACA,UAAW,CACT,EAAS,UAAU,EAInB,EAAW,CACT,OAAQ,CAAC,WAAY,UAAW,WAAY,QAAS,MAAO,aAAa,EACzE,IAAK,CACH,OAAQ,CAAC,WAAY,QAAS,MAAO,aAAa,EAClD,SAAU,QACZ,EACA,WAAa,IAAS,CACpB,GAAI,EAAI,GACR,MAAQ,EAAI,OAAiC,EAAI,SACjD,YAAa,EAAI,QACnB,EACF,CAAC,CACH,EACA,MAAO,SACP,OAAQ,CACN,KAAM,SACN,OAAQ,eACR,OAAQ,eACR,OAAQ,aACV,CACF,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{defineSettings as e,setting as t}from"@murumets-ee/settings/define";import{z as n}from"zod";const r=n.object({width:n.number().int().positive().optional(),height:n.number().int().positive().optional(),fit:n.enum([`cover`,`contain`,`inside`,`outside`,`fill`]).optional(),format:n.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:n.number().int().min(1).max(100).optional()}).refine(e=>e.width!==void 0||e.height!==void 0,{message:`Style must have at least width or height`}),i=/^[a-z][a-z0-9_-]*$/,a=n.record(n.string(),r).superRefine((e,t)=>{for(let r of Object.keys(e))i.test(r)||t.addIssue({code:n.ZodIssueCode.custom,path:[r],message:`Invalid style name "${r}": must be lowercase alphanumeric (a-z, 0-9, -, _)`})}),o=e({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,iconName:`image-down`,schema:{imageStyles:t.json({default:{thumbnail:{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}}.thumbnail},label:`Image processing presets`,schema:a,renderer:`media.imageStyles`})}});export{o as imageStylesSettings};
|
|
2
|
-
//# sourceMappingURL=image-styles-settings-
|
|
2
|
+
//# sourceMappingURL=image-styles-settings-ClIA2MJI.mjs.map
|
package/dist/{image-styles-settings-B4QOt7Ve.mjs.map → image-styles-settings-ClIA2MJI.mjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles-settings-
|
|
1
|
+
{"version":3,"file":"image-styles-settings-ClIA2MJI.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/define'\nimport { z } from 'zod'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]*$/\n\nconst imageStylesSchema = z\n .record(z.string(), styleSchema)\n .superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"mGAsBA,MAAM,EAAc,EACjB,OAAO,CACN,MAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAC5C,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAC7C,IAAK,EAAE,KAAK,CAAC,QAAS,UAAW,SAAU,UAAW,MAAM,CAAC,CAAC,CAAC,SAAS,EACxE,OAAQ,EAAE,KAAK,CAAC,OAAQ,OAAQ,MAAO,MAAM,CAAC,CAAC,CAAC,SAAS,EACzD,QAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,CACrD,CAAC,CAAC,CACD,OAAQ,GAAM,EAAE,QAAU,IAAA,IAAa,EAAE,SAAW,IAAA,GAAW,CAC9D,QAAS,0CACX,CAAC,EAEG,EAAgB,qBAEhB,EAAoB,EACvB,OAAO,EAAE,OAAO,EAAG,CAAW,CAAC,CAC/B,aAAa,EAAQ,IAAQ,CAC5B,IAAK,IAAM,KAAQ,OAAO,KAAK,CAAM,EAC9B,EAAc,KAAK,CAAI,GAC1B,EAAI,SAAS,CACX,KAAM,EAAE,aAAa,OACrB,KAAM,CAAC,CAAI,EACX,QAAS,uBAAuB,EAAK,mDACvC,CAAC,CAGP,CAAC,EAsBU,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,SAAU,aACV,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,CAdjB,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAChF,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAC3E,KAAM,CAAE,MAAO,KAAM,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAC5E,KAAM,CAAE,MAAO,KAAM,IAAK,SAAU,OAAQ,OAAQ,QAAS,EAAG,CAW/C,EAAmB,SAChC,EACA,MAAO,2BACP,OAAQ,EACR,SAAU,mBACZ,CAAC,CACH,CACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA+Da,kBAAA,EAAoB,
|
|
1
|
+
{"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA+Da,kBAAA,EAAoB,MAAM,SAAS,UAAA;AAAA,cAOnC,mBAAA,kCAAmB,kBAAA;EAAA,wEAe9B,MAAA,SAAA,UAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings/define'\nimport { z } from 'zod'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]*$/\n\nconst imageStylesSchema = z\n .record(z.string(), styleSchema)\n .superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"mGAsBA,MAAM,EAAc,EACjB,OAAO,CACN,MAAO,EAAE,
|
|
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/define'\nimport { z } from 'zod'\nimport type { ImageStyle } from './types.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]*$/\n\nconst imageStylesSchema = z\n .record(z.string(), styleSchema)\n .superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"mGAsBA,MAAM,EAAc,EACjB,OAAO,CACN,MAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAC5C,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAC7C,IAAK,EAAE,KAAK,CAAC,QAAS,UAAW,SAAU,UAAW,MAAM,CAAC,CAAC,CAAC,SAAS,EACxE,OAAQ,EAAE,KAAK,CAAC,OAAQ,OAAQ,MAAO,MAAM,CAAC,CAAC,CAAC,SAAS,EACzD,QAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,CACrD,CAAC,CAAC,CACD,OAAQ,GAAM,EAAE,QAAU,IAAA,IAAa,EAAE,SAAW,IAAA,GAAW,CAC9D,QAAS,0CACX,CAAC,EAEG,EAAgB,qBAEhB,EAAoB,EACvB,OAAO,EAAE,OAAO,EAAG,CAAW,CAAC,CAC/B,aAAa,EAAQ,IAAQ,CAC5B,IAAK,IAAM,KAAQ,OAAO,KAAK,CAAM,EAC9B,EAAc,KAAK,CAAI,GAC1B,EAAI,SAAS,CACX,KAAM,EAAE,aAAa,OACrB,KAAM,CAAC,CAAI,EACX,QAAS,uBAAuB,EAAK,mDACvC,CAAC,CAGP,CAAC,EAeU,EAAiD,CAC5D,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAChF,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAC3E,KAAM,CAAE,MAAO,KAAM,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,EAC5E,KAAM,CAAE,MAAO,KAAM,IAAK,SAAU,OAAQ,OAAQ,QAAS,EAAG,CAClE,EAEa,EAAsB,EAAe,CAChD,UAAW,oBACX,MAAO,SACP,MAAO,eACP,SAAU,aACV,OAAQ,CACN,YAAa,EAAQ,KAAiC,CACpD,QAAS,CACP,UAAW,EAAmB,SAChC,EACA,MAAO,2BACP,OAAQ,EACR,SAAU,mBACZ,CAAC,CACH,CACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;UA2EiB,UAAA;EClCkB;EDoCjC,KAAA;EClCA;EDoCA,MAAA;EClCS;EDoCT,GAAA;EClCa;EDoCb,MAAA;ECpCyC;EDsCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EArBf;EAuBA,KAAA,EAAO,MAAA,SAAe,UAAA;EArBtB;EAuBA,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EArBjC;EAuBA,KAAA;EAvBE;EAyBF,MAAA,GAAS,wBAAA;EAtBkC;EAwB3C,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,
|
|
1
|
+
{"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;UA2EiB,UAAA;EClCkB;EDoCjC,KAAA;EClCA;EDoCA,MAAA;EClCS;EDoCT,GAAA;EClCa;EDoCb,MAAA;ECpCyC;EDsCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EArBf;EAuBA,KAAA,EAAO,MAAA,SAAe,UAAA;EArBtB;EAuBA,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EArBjC;EAuBA,KAAA;EAvBE;EAyBF,MAAA,GAAS,wBAAA;EAtBkC;EAwB3C,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,YAAY;AAAA;AAAA,UAGG,6BAAA;EA3BR;EA6BP,WAAA;EApBe;EAsBf,MAAA,GAAS,8BAA8B;AAAA;;;;;;ADgBzC;;;iBEjBgB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,+BAAuB,GAAA,CAAA,OAAA;;;;;;AFW1B;;;;iBG9CgB,wBAAA,CAAA;EACd,WAAA;EACA,MAAA,EAAQ;AAAA,GACP,6BAAA,+BAA6B,GAAA,CAAA,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-styles.mjs","names":["DEFAULT_LABELS"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStyle } from '../types.js'\nimport type { ImageStylesManagerProps } from './types.js'\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Variants are generated on upload for each style. Save the form to apply changes.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n px: 'px',\n}\n\n/**\n * Controlled image-styles editor. Designed to slot into the settings-form\n * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state\n * internally; commits through `onChange` so the parent settings form's\n * Save button persists the change.\n */\nexport function ImageStylesManager({\n value,\n onChange,\n error,\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const styles = value ?? {}\n\n // Add / edit dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(() => {\n setFormError(null)\n\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const fit = formFit as NonNullable<ImageStyle['fit']>\n const format = formFormat as NonNullable<ImageStyle['format']>\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit,\n format,\n quality: q,\n }\n\n const next = { ...styles }\n if (editingKey && editingKey !== name) {\n delete next[editingKey]\n }\n next[name] = style\n onChange(next)\n setIsDialogOpen(false)\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n onChange,\n ])\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(() => {\n if (!deleteKey) return\n const next = { ...styles }\n delete next[deleteKey]\n onChange(next)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n }, [deleteKey, styles, onChange])\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-4', classNames?.root)}>\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n <Button onClick={openAddDialog} size=\"sm\" type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button type=\"button\" onClick={handleSaveStyle}>\n {labels.save}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" type=\"button\" onClick={handleDelete}>\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type {\n RegenerateVariantsActionLabels,\n RegenerateVariantsActionProps,\n} from './types.js'\n\nconst DEFAULT_LABELS: Required<RegenerateVariantsActionLabels> = {\n regenerate: 'Regenerate All Variants',\n regenerating: 'Regenerating...',\n}\n\ninterface RegenerateResult {\n total: number\n processed: number\n skipped: number\n errors: number\n}\n\n/**\n * Action button for the `media.imageStyles` settings page header.\n * Calls `POST /api/admin/media/regenerate-variants` and surfaces the\n * result inline. Owns its own request lifecycle — independent from the\n * settings form's save flow because regeneration is an explicit, opt-in\n * job (not a \"save side effect\").\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const [isRunning, setIsRunning] = useState(false)\n const [result, setResult] = useState<RegenerateResult | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setResult(null)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/media/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json().catch(() => ({}))) as { error?: string }\n throw new Error(data.error ?? `Regeneration failed (${res.status})`)\n }\n const data = (await res.json()) as RegenerateResult\n setResult(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Regeneration failed')\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex items-center gap-3\">\n {result && (\n <span className=\"text-xs text-muted-foreground\">\n Processed {result.processed} of {result.total}\n {result.skipped > 0 && `, ${result.skipped} skipped`}\n {result.errors > 0 && (\n <span className=\"text-destructive\">, {result.errors} errors</span>\n )}\n </span>\n )}\n {error && <span className=\"text-xs text-destructive\">{error}</span>}\n <Button variant=\"outline\" size=\"sm\" type=\"button\" onClick={handleClick} disabled={isRunning}>\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {labels.regenerating}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {labels.regenerate}\n </>\n )}\n </Button>\n </div>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image-styles.mjs","names":["DEFAULT_LABELS"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStyle } from '../types.js'\nimport type { ImageStylesManagerProps } from './types.js'\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Variants are generated on upload for each style. Save the form to apply changes.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n px: 'px',\n}\n\n/**\n * Controlled image-styles editor. Designed to slot into the settings-form\n * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state\n * internally; commits through `onChange` so the parent settings form's\n * Save button persists the change.\n */\nexport function ImageStylesManager({\n value,\n onChange,\n error,\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const styles = value ?? {}\n\n // Add / edit dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(() => {\n setFormError(null)\n\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const fit = formFit as NonNullable<ImageStyle['fit']>\n const format = formFormat as NonNullable<ImageStyle['format']>\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit,\n format,\n quality: q,\n }\n\n const next = { ...styles }\n if (editingKey && editingKey !== name) {\n delete next[editingKey]\n }\n next[name] = style\n onChange(next)\n setIsDialogOpen(false)\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n onChange,\n ])\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(() => {\n if (!deleteKey) return\n const next = { ...styles }\n delete next[deleteKey]\n onChange(next)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n }, [deleteKey, styles, onChange])\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-4', classNames?.root)}>\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n <Button onClick={openAddDialog} size=\"sm\" type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button type=\"button\" onClick={handleSaveStyle}>\n {labels.save}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" type=\"button\" onClick={handleDelete}>\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type {\n RegenerateVariantsActionLabels,\n RegenerateVariantsActionProps,\n} from './types.js'\n\nconst DEFAULT_LABELS: Required<RegenerateVariantsActionLabels> = {\n regenerate: 'Regenerate All Variants',\n regenerating: 'Regenerating...',\n}\n\ninterface RegenerateResult {\n total: number\n processed: number\n skipped: number\n errors: number\n}\n\n/**\n * Action button for the `media.imageStyles` settings page header.\n * Calls `POST /api/admin/media/regenerate-variants` and surfaces the\n * result inline. Owns its own request lifecycle — independent from the\n * settings form's save flow because regeneration is an explicit, opt-in\n * job (not a \"save side effect\").\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const [isRunning, setIsRunning] = useState(false)\n const [result, setResult] = useState<RegenerateResult | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setResult(null)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/media/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json().catch(() => ({}))) as { error?: string }\n throw new Error(data.error ?? `Regeneration failed (${res.status})`)\n }\n const data = (await res.json()) as RegenerateResult\n setResult(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Regeneration failed')\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex items-center gap-3\">\n {result && (\n <span className=\"text-xs text-muted-foreground\">\n Processed {result.processed} of {result.total}\n {result.skipped > 0 && `, ${result.skipped} skipped`}\n {result.errors > 0 && (\n <span className=\"text-destructive\">, {result.errors} errors</span>\n )}\n </span>\n )}\n {error && <span className=\"text-xs text-destructive\">{error}</span>}\n <Button variant=\"outline\" size=\"sm\" type=\"button\" onClick={handleClick} disabled={isRunning}>\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {labels.regenerating}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {labels.regenerate}\n </>\n )}\n </Button>\n </div>\n )\n}\n"],"mappings":"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"}
|