@murumets-ee/media 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/{admin.d.ts → admin.d.mts} +4 -25
  2. package/dist/admin.d.mts.map +1 -0
  3. package/dist/admin.mjs +2 -0
  4. package/dist/admin.mjs.map +1 -0
  5. package/dist/client-sF8mf4Fg.mjs +2 -0
  6. package/dist/client-sF8mf4Fg.mjs.map +1 -0
  7. package/dist/client.d.mts +95 -0
  8. package/dist/client.d.mts.map +1 -0
  9. package/dist/client.mjs +2 -0
  10. package/dist/client.mjs.map +1 -0
  11. package/dist/entity-D5P2l05s.mjs +2 -0
  12. package/dist/entity-D5P2l05s.mjs.map +1 -0
  13. package/dist/entity-DZFku8b7.mjs +2 -0
  14. package/dist/entity-DZFku8b7.mjs.map +1 -0
  15. package/dist/image-styles-settings-7K_jPNIA.mjs +2 -0
  16. package/dist/image-styles-settings-7K_jPNIA.mjs.map +1 -0
  17. package/dist/image-styles-settings.d.mts +10 -0
  18. package/dist/image-styles-settings.d.mts.map +1 -0
  19. package/dist/image-styles-settings.mjs +2 -0
  20. package/dist/image-styles-settings.mjs.map +1 -0
  21. package/dist/image-styles.d.mts +71 -0
  22. package/dist/image-styles.d.mts.map +1 -0
  23. package/dist/image-styles.mjs +3 -0
  24. package/dist/image-styles.mjs.map +1 -0
  25. package/dist/index.d.mts +112 -0
  26. package/dist/index.d.mts.map +1 -0
  27. package/dist/index.mjs +2 -0
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/picker.d.mts +228 -0
  30. package/dist/picker.d.mts.map +1 -0
  31. package/dist/picker.mjs +3 -0
  32. package/dist/picker.mjs.map +1 -0
  33. package/dist/plugin-DV7lvImm.mjs +2 -0
  34. package/dist/plugin-DV7lvImm.mjs.map +1 -0
  35. package/dist/plugin.d.mts +20 -0
  36. package/dist/plugin.d.mts.map +1 -0
  37. package/dist/plugin.mjs +2 -0
  38. package/dist/plugin.mjs.map +1 -0
  39. package/dist/query-client.d.mts +28 -0
  40. package/dist/query-client.d.mts.map +1 -0
  41. package/dist/query-client.mjs +2 -0
  42. package/dist/query-client.mjs.map +1 -0
  43. package/dist/{ref.d.ts → ref.d.mts} +26 -24
  44. package/dist/ref.d.mts.map +1 -0
  45. package/dist/ref.mjs +2 -0
  46. package/dist/ref.mjs.map +1 -0
  47. package/dist/regenerate-variants-DY7D4Ky3.mjs +2 -0
  48. package/dist/regenerate-variants-DY7D4Ky3.mjs.map +1 -0
  49. package/dist/types-BV_pOm23.d.mts +103 -0
  50. package/dist/types-BV_pOm23.d.mts.map +1 -0
  51. package/dist/usage-D7Bn7Vvv.mjs +2 -0
  52. package/dist/usage-D7Bn7Vvv.mjs.map +1 -0
  53. package/dist/usage.d.mts +21 -0
  54. package/dist/usage.d.mts.map +1 -0
  55. package/dist/usage.mjs +2 -0
  56. package/dist/usage.mjs.map +1 -0
  57. package/dist/variant-key-DZUYURS5.mjs +2 -0
  58. package/dist/variant-key-DZUYURS5.mjs.map +1 -0
  59. package/package.json +29 -29
  60. package/dist/admin.js +0 -1
  61. package/dist/chunk-JGE5BDIT.js +0 -1
  62. package/dist/chunk-L6BDKI76.js +0 -1
  63. package/dist/chunk-QTUXM53A.js +0 -1
  64. package/dist/chunk-YAHM4C5J.js +0 -1
  65. package/dist/client-37WG2Y6P.js +0 -1
  66. package/dist/client.d.ts +0 -102
  67. package/dist/client.js +0 -1
  68. package/dist/entity-QOBW3TFU.js +0 -1
  69. package/dist/image-styles-settings-AB5WFEFF.js +0 -1
  70. package/dist/image-styles-settings.d.ts +0 -9
  71. package/dist/image-styles-settings.js +0 -1
  72. package/dist/image-styles.d.ts +0 -62
  73. package/dist/image-styles.js +0 -2
  74. package/dist/index.d.ts +0 -131
  75. package/dist/index.js +0 -1
  76. package/dist/picker.d.ts +0 -183
  77. package/dist/picker.js +0 -2
  78. package/dist/plugin-CAV5BZMF.js +0 -1
  79. package/dist/plugin.d.ts +0 -35
  80. package/dist/plugin.js +0 -1
  81. package/dist/query-client.d.ts +0 -36
  82. package/dist/query-client.js +0 -1
  83. package/dist/ref.js +0 -1
  84. package/dist/regenerate-variants-IUDIIWXU.js +0 -1
  85. package/dist/types-ChlTxvlq.d.ts +0 -101
  86. package/dist/usage-E5RZMWE4.js +0 -1
  87. package/dist/usage.d.ts +0 -28
  88. package/dist/usage.js +0 -1
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./entity-D5P2l05s.mjs";var t=class{db;logger;query=null;constructor(e){this.db=e.db,this.logger=e.logger}async getQuery(){if(!this.query){let{QueryClient:t}=await import(`@murumets-ee/entity/query`);this.query=new t({entity:e,db:this.db,logger:this.logger})}return this.query}async findById(e,t){return await(await this.getQuery()).findById(e,t)}async findMany(e){return await(await this.getQuery()).findMany(e)}async count(e){return(await this.getQuery()).count(e)}};async function n(){let{getApp:e}=await import(`@murumets-ee/core`),n=e();return new t({db:n.db.readOnly,logger:n.logger.child({mediaQuery:!0})})}export{t as MediaQueryClient,n as createMediaQueryClient};
2
+ //# sourceMappingURL=query-client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-client.mjs","names":[],"sources":["../src/query-client.ts"],"sourcesContent":["/**\n * MediaQueryClient — read-only media client for frontends.\n *\n * Usage:\n * import { createMediaQueryClient } from '@murumets-ee/media/query'\n * const media = await createMediaQueryClient()\n * const image = await media.findById(id)\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type {\n CountOptions,\n FindByIdOptions,\n FindManyOptions,\n QueryClient,\n} from '@murumets-ee/entity/query'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { Media } from './entity.js'\nimport type { MediaRecord } from './types.js'\n\nexport interface MediaQueryClientConfig {\n db: PostgresJsDatabase\n logger?: Logger\n}\n\nexport class MediaQueryClient {\n private db: PostgresJsDatabase\n private logger?: Logger\n private query: QueryClient | null = null\n\n constructor(config: MediaQueryClientConfig) {\n this.db = config.db\n this.logger = config.logger\n }\n\n private async getQuery(): Promise<QueryClient> {\n if (!this.query) {\n const { QueryClient } = await import('@murumets-ee/entity/query')\n this.query = new QueryClient({\n entity: Media,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.query\n }\n\n async findById(id: string, options?: FindByIdOptions): Promise<MediaRecord | null> {\n const query = await this.getQuery()\n const result = await query.findById(id, options)\n return result as unknown as MediaRecord | null\n }\n\n async findMany(options?: FindManyOptions): Promise<MediaRecord[]> {\n const query = await this.getQuery()\n const results = await query.findMany(options)\n return results as unknown as MediaRecord[]\n }\n\n async count(options?: CountOptions): Promise<number> {\n const query = await this.getQuery()\n return query.count(options)\n }\n}\n\n/**\n * Factory — creates a MediaQueryClient.\n * Must be called after createApp().\n */\nexport async function createMediaQueryClient(): Promise<MediaQueryClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new MediaQueryClient({\n db: app.db.readOnly,\n logger: app.logger.child({ mediaQuery: true }),\n })\n}\n"],"mappings":"0CAyBA,IAAa,EAAb,KAA8B,CAC5B,GACA,OACA,MAAoC,KAEpC,YAAY,EAAgC,CAC1C,KAAK,GAAK,EAAO,GACjB,KAAK,OAAS,EAAO,OAGvB,MAAc,UAAiC,CAC7C,GAAI,CAAC,KAAK,MAAO,CACf,GAAM,CAAE,eAAgB,MAAM,OAAO,6BACrC,KAAK,MAAQ,IAAI,EAAY,CAC3B,OAAQ,EACR,GAAI,KAAK,GACT,OAAQ,KAAK,OACd,CAAC,CAEJ,OAAO,KAAK,MAGd,MAAM,SAAS,EAAY,EAAwD,CAGjF,OADe,MADD,MAAM,KAAK,UAAU,EACR,SAAS,EAAI,EAAQ,CAIlD,MAAM,SAAS,EAAmD,CAGhE,OADgB,MADF,MAAM,KAAK,UAAU,EACP,SAAS,EAAQ,CAI/C,MAAM,MAAM,EAAyC,CAEnD,OADc,MAAM,KAAK,UAAU,EACtB,MAAM,EAAQ,GAQ/B,eAAsB,GAAoD,CACxE,GAAM,CAAE,UAAW,MAAM,OAAO,qBAC1B,EAAM,GAAQ,CACpB,OAAO,IAAI,EAAiB,CAC1B,GAAI,EAAI,GAAG,SACX,OAAQ,EAAI,OAAO,MAAM,CAAE,WAAY,GAAM,CAAC,CAC/C,CAAC"}
@@ -1,3 +1,4 @@
1
+ //#region src/ref.d.ts
1
2
  /**
2
3
  * Media reference system — parse, extract, and resolve [media:type:id:variant] tags.
3
4
  *
@@ -29,29 +30,29 @@
29
30
  type MediaRefType = 'image' | 'video' | 'audio' | 'file';
30
31
  /** Parsed media reference from a [media:type:id] or [media:type:id:variant] tag */
31
32
  interface MediaRef {
32
- /** The full original tag string */
33
- raw: string;
34
- /** The type prefix (image, video, audio, file) */
35
- type: MediaRefType;
36
- /** The media entity UUID */
37
- id: string;
38
- /** Optional rendering variant hint (thumbnail, hero, url, banner, etc.) */
39
- variant: string | null;
33
+ /** The full original tag string */
34
+ raw: string;
35
+ /** The type prefix (image, video, audio, file) */
36
+ type: MediaRefType;
37
+ /** The media entity UUID */
38
+ id: string;
39
+ /** Optional rendering variant hint (thumbnail, hero, url, banner, etc.) */
40
+ variant: string | null;
40
41
  }
41
42
  /** Resolved media reference with URL and metadata */
42
43
  interface ResolvedMediaRef extends MediaRef {
43
- /** Resolved URL for the media file */
44
- url: string;
45
- /** Alt text (if available from entity) */
46
- alt?: string;
47
- /** Original filename */
48
- filename?: string;
49
- /** MIME type */
50
- mimeType?: string;
51
- /** Image width */
52
- width?: number;
53
- /** Image height */
54
- height?: number;
44
+ /** Resolved URL for the media file */
45
+ url: string;
46
+ /** Alt text (if available from entity) */
47
+ alt?: string;
48
+ /** Original filename */
49
+ filename?: string;
50
+ /** MIME type */
51
+ mimeType?: string;
52
+ /** Image width */
53
+ width?: number;
54
+ /** Image height */
55
+ height?: number;
55
56
  }
56
57
  /**
57
58
  * Resolver function — provided by consumer to batch-resolve media IDs.
@@ -72,13 +73,13 @@ declare function parseMediaRefs(text: string): MediaRef[];
72
73
  * Useful for preloading media before rendering.
73
74
  */
74
75
  declare function extractMediaIds(text: string, options?: {
75
- type?: MediaRefType;
76
+ type?: MediaRefType;
76
77
  }): string[];
77
78
  /**
78
79
  * Extract unique media entity UUIDs from multiple text strings.
79
80
  */
80
81
  declare function extractAllMediaIds(texts: string[], options?: {
81
- type?: MediaRefType;
82
+ type?: MediaRefType;
82
83
  }): string[];
83
84
  /**
84
85
  * Create a media reference tag string.
@@ -96,5 +97,6 @@ declare function createMediaRef(type: MediaRefType, id: string, variant?: string
96
97
  * @param renderers - Optional custom renderers per type (override defaults)
97
98
  */
98
99
  declare function resolveMediaRefs(text: string, resolver: MediaRefResolver, renderers?: Partial<Record<MediaRefType, MediaRefRenderer>>): Promise<string>;
99
-
100
- export { type MediaRef, type MediaRefRenderer, type MediaRefResolver, type MediaRefType, type ResolvedMediaRef, createMediaRef, extractAllMediaIds, extractMediaIds, parseMediaRefs, resolveMediaRefs };
100
+ //#endregion
101
+ export { MediaRef, MediaRefRenderer, MediaRefResolver, MediaRefType, ResolvedMediaRef, createMediaRef, extractAllMediaIds, extractMediaIds, parseMediaRefs, resolveMediaRefs };
102
+ //# sourceMappingURL=ref.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref.d.mts","names":[],"sources":["../src/ref.ts"],"mappings":";;AAiCA;;;;;AAGA;;;;;;;;;;;AAYA;;;;;;;;;;;KAfY,YAAA;;UAGK,QAAA;EA+BW;EA7B1B,GAAA;EA6BoC;EA3BpC,IAAA,EAAM,YAAA;EA2BqD;EAzB3D,EAAA;EAyB0D;EAvB1D,OAAA;AAAA;;UAIe,gBAAA,SAAyB,QAAA;EAmBmB;EAjB3D,GAAA;EAiBuF;EAfvF,GAAA;EAqBU;EAnBV,QAAA;;EAEA,QAAA;EAiBmD;EAfnD,KAAA;EA2C4B;EAzC5B,MAAA;AAAA;;AAqEF;;;KA9DY,gBAAA,IAAoB,IAAA,EAAM,QAAA,OAAe,OAAA,CAAQ,GAAA,SAAY,gBAAA;;;;;KAM7D,gBAAA,IAAoB,GAAA,EAAK,gBAAA;;AAiErC;;iBArCgB,cAAA,CAAe,IAAA,WAAe,QAAA;;;;;iBA4B9B,eAAA,CAAgB,IAAA,UAAc,OAAA;EAAY,IAAA,GAAO,YAAA;AAAA;AAqBjE;;;AAAA,iBAZgB,kBAAA,CAAmB,KAAA,YAAiB,OAAA;EAAY,IAAA,GAAO,YAAA;AAAA;;;;iBAYvD,cAAA,CAAe,IAAA,EAAM,YAAA,EAAc,EAAA,UAAY,OAAA;AAsC/D;;;;;;;;;;;AAAA,iBAAsB,gBAAA,CACpB,IAAA,UACA,QAAA,EAAU,gBAAA,EACV,SAAA,GAAY,OAAA,CAAQ,MAAA,CAAO,YAAA,EAAc,gBAAA,KACxC,OAAA"}
package/dist/ref.mjs ADDED
@@ -0,0 +1,2 @@
1
+ const e=/\[media:(image|video|audio|file):([a-f0-9-]{36})(?::([a-z0-9-]+))?\]/g;function t(t){let n=[];e.lastIndex=0;let r=e.exec(t);for(;r!==null;)n.push({raw:r[0],type:r[1],id:r[2],variant:r[3]??null}),r=e.exec(t);return n}function n(e,n){let r=t(e),i=n?.type?r.filter(e=>e.type===n.type):r;return[...new Set(i.map(e=>e.id))]}function r(e,t){let r=e.flatMap(e=>n(e,t));return[...new Set(r)]}function i(e,t,n){return n?`[media:${e}:${t}:${n}]`:`[media:${e}:${t}]`}const a={image:e=>`<img ${[`src="${e.url}"`,`alt="${e.alt??e.filename??``}"`,e.width?`width="${e.width}"`:``,e.height?`height="${e.height}"`:``].filter(Boolean).join(` `)} />`,video:e=>`<video src="${e.url}" controls></video>`,audio:e=>`<audio src="${e.url}" controls></audio>`,file:e=>`<a href="${e.url}" download="${e.filename??``}">${e.filename??e.url}</a>`};async function o(e,n,r){let i=t(e);if(i.length===0)return e;let o=await n(i),s=e;for(let e of i){let t=o.get(e.id);if(!t)continue;let n=r?.[e.type]??a[e.type];n&&(s=s.replace(e.raw,n(t)))}return s}export{i as createMediaRef,r as extractAllMediaIds,n as extractMediaIds,t as parseMediaRefs,o as resolveMediaRefs};
2
+ //# sourceMappingURL=ref.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref.mjs","names":[],"sources":["../src/ref.ts"],"sourcesContent":["/**\n * Media reference system — parse, extract, and resolve [media:type:id:variant] tags.\n *\n * Two complementary ways to reference media:\n * 1. field.media() — stores UUID directly (cover image, hero background)\n * 2. [media:type:id:variant] — inline in text/richtext prose\n *\n * Both resolve to the same Media entity.\n *\n * @example\n * ```typescript\n * import { parseMediaRefs, resolveMediaRefs, createMediaRef } from '@murumets-ee/media/ref'\n *\n * // Create a tag\n * const tag = createMediaRef('image', 'abc-123', 'thumbnail')\n * // → '[media:image:abc-123:thumbnail]'\n *\n * // Parse tags from text\n * const refs = parseMediaRefs('Here is [media:image:abc-123:hero] a photo')\n * // → [{ raw: '[media:image:abc-123:hero]', type: 'image', id: 'abc-123', variant: 'hero' }]\n *\n * // Resolve tags to HTML\n * const html = await resolveMediaRefs(text, myResolver, {\n * image: (ref) => ref.variant === 'url' ? ref.url : `<img src=\"${ref.url}\" />`,\n * })\n * ```\n */\n\n// ---------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------\n\n/** Supported media reference types in tags */\nexport type MediaRefType = 'image' | 'video' | 'audio' | 'file'\n\n/** Parsed media reference from a [media:type:id] or [media:type:id:variant] tag */\nexport interface MediaRef {\n /** The full original tag string */\n raw: string\n /** The type prefix (image, video, audio, file) */\n type: MediaRefType\n /** The media entity UUID */\n id: string\n /** Optional rendering variant hint (thumbnail, hero, url, banner, etc.) */\n variant: string | null\n}\n\n/** Resolved media reference with URL and metadata */\nexport interface ResolvedMediaRef extends MediaRef {\n /** Resolved URL for the media file */\n url: string\n /** Alt text (if available from entity) */\n alt?: string\n /** Original filename */\n filename?: string\n /** MIME type */\n mimeType?: string\n /** Image width */\n width?: number\n /** Image height */\n height?: number\n}\n\n/**\n * Resolver function — provided by consumer to batch-resolve media IDs.\n * Receives parsed references, returns a Map of id → resolved reference.\n */\nexport type MediaRefResolver = (refs: MediaRef[]) => Promise<Map<string, ResolvedMediaRef>>\n\n/**\n * Renderer function — converts a resolved reference to a string representation.\n * Consumer provides custom renderers per type.\n */\nexport type MediaRefRenderer = (ref: ResolvedMediaRef) => string\n\n// ---------------------------------------------------------------\n// Pattern\n// ---------------------------------------------------------------\n\n/**\n * Regex pattern for media reference tags.\n *\n * Format: [media:type:uuid] or [media:type:uuid:variant]\n * - type: image | video | audio | file\n * - uuid: UUID v4 (36 chars with hyphens)\n * - variant: optional lowercase alphanumeric + hyphens\n *\n * Examples:\n * [media:image:550e8400-e29b-41d4-a716-446655440000]\n * [media:video:550e8400-e29b-41d4-a716-446655440000:thumbnail]\n * [media:file:550e8400-e29b-41d4-a716-446655440000:download]\n */\nconst MEDIA_REF_PATTERN = /\\[media:(image|video|audio|file):([a-f0-9-]{36})(?::([a-z0-9-]+))?\\]/g\n\n// ---------------------------------------------------------------\n// Parser\n// ---------------------------------------------------------------\n\n/**\n * Parse all media reference tags from a text string.\n */\nexport function parseMediaRefs(text: string): MediaRef[] {\n const refs: MediaRef[] = []\n\n // Reset regex lastIndex (global flag)\n MEDIA_REF_PATTERN.lastIndex = 0\n\n let match = MEDIA_REF_PATTERN.exec(text)\n while (match !== null) {\n refs.push({\n raw: match[0],\n type: match[1] as MediaRefType,\n id: match[2],\n variant: match[3] ?? null,\n })\n match = MEDIA_REF_PATTERN.exec(text)\n }\n\n return refs\n}\n\n// ---------------------------------------------------------------\n// Extractor\n// ---------------------------------------------------------------\n\n/**\n * Extract unique media entity UUIDs from text.\n * Useful for preloading media before rendering.\n */\nexport function extractMediaIds(text: string, options?: { type?: MediaRefType }): string[] {\n const refs = parseMediaRefs(text)\n const filtered = options?.type ? refs.filter((r) => r.type === options.type) : refs\n return [...new Set(filtered.map((r) => r.id))]\n}\n\n/**\n * Extract unique media entity UUIDs from multiple text strings.\n */\nexport function extractAllMediaIds(texts: string[], options?: { type?: MediaRefType }): string[] {\n const allIds = texts.flatMap((text) => extractMediaIds(text, options))\n return [...new Set(allIds)]\n}\n\n// ---------------------------------------------------------------\n// Creator\n// ---------------------------------------------------------------\n\n/**\n * Create a media reference tag string.\n */\nexport function createMediaRef(type: MediaRefType, id: string, variant?: string): string {\n return variant ? `[media:${type}:${id}:${variant}]` : `[media:${type}:${id}]`\n}\n\n// ---------------------------------------------------------------\n// Resolver\n// ---------------------------------------------------------------\n\n/** Default HTML renderers per media type */\nconst defaultRenderers: Record<MediaRefType, MediaRefRenderer> = {\n image: (ref) => {\n const attrs = [\n `src=\"${ref.url}\"`,\n `alt=\"${ref.alt ?? ref.filename ?? ''}\"`,\n ref.width ? `width=\"${ref.width}\"` : '',\n ref.height ? `height=\"${ref.height}\"` : '',\n ]\n .filter(Boolean)\n .join(' ')\n return `<img ${attrs} />`\n },\n video: (ref) => `<video src=\"${ref.url}\" controls></video>`,\n audio: (ref) => `<audio src=\"${ref.url}\" controls></audio>`,\n file: (ref) =>\n `<a href=\"${ref.url}\" download=\"${ref.filename ?? ''}\">${ref.filename ?? ref.url}</a>`,\n}\n\n/**\n * Resolve all media reference tags in a text string.\n *\n * 1. Parses all [media:type:id:variant] tags\n * 2. Calls the resolver function to get URLs + metadata\n * 3. Replaces each tag with rendered output\n *\n * @param text - Input text containing media reference tags\n * @param resolver - Function that batch-resolves MediaRef[] → Map<id, ResolvedMediaRef>\n * @param renderers - Optional custom renderers per type (override defaults)\n */\nexport async function resolveMediaRefs(\n text: string,\n resolver: MediaRefResolver,\n renderers?: Partial<Record<MediaRefType, MediaRefRenderer>>,\n): Promise<string> {\n const refs = parseMediaRefs(text)\n if (refs.length === 0) return text\n\n // Batch-resolve all references\n const resolvedMap = await resolver(refs)\n\n // Replace each tag with rendered output\n let result = text\n for (const ref of refs) {\n const resolved = resolvedMap.get(ref.id)\n if (!resolved) continue\n\n const renderer = renderers?.[ref.type] ?? defaultRenderers[ref.type]\n if (!renderer) continue\n\n result = result.replace(ref.raw, renderer(resolved))\n }\n\n return result\n}\n"],"mappings":"AA4FA,MAAM,EAAoB,wEAS1B,SAAgB,EAAe,EAA0B,CACvD,IAAM,EAAmB,EAAE,CAG3B,EAAkB,UAAY,EAE9B,IAAI,EAAQ,EAAkB,KAAK,EAAK,CACxC,KAAO,IAAU,MACf,EAAK,KAAK,CACR,IAAK,EAAM,GACX,KAAM,EAAM,GACZ,GAAI,EAAM,GACV,QAAS,EAAM,IAAM,KACtB,CAAC,CACF,EAAQ,EAAkB,KAAK,EAAK,CAGtC,OAAO,EAWT,SAAgB,EAAgB,EAAc,EAA6C,CACzF,IAAM,EAAO,EAAe,EAAK,CAC3B,EAAW,GAAS,KAAO,EAAK,OAAQ,GAAM,EAAE,OAAS,EAAQ,KAAK,CAAG,EAC/E,MAAO,CAAC,GAAG,IAAI,IAAI,EAAS,IAAK,GAAM,EAAE,GAAG,CAAC,CAAC,CAMhD,SAAgB,EAAmB,EAAiB,EAA6C,CAC/F,IAAM,EAAS,EAAM,QAAS,GAAS,EAAgB,EAAM,EAAQ,CAAC,CACtE,MAAO,CAAC,GAAG,IAAI,IAAI,EAAO,CAAC,CAU7B,SAAgB,EAAe,EAAoB,EAAY,EAA0B,CACvF,OAAO,EAAU,UAAU,EAAK,GAAG,EAAG,GAAG,EAAQ,GAAK,UAAU,EAAK,GAAG,EAAG,GAQ7E,MAAM,EAA2D,CAC/D,MAAQ,GASC,QARO,CACZ,QAAQ,EAAI,IAAI,GAChB,QAAQ,EAAI,KAAO,EAAI,UAAY,GAAG,GACtC,EAAI,MAAQ,UAAU,EAAI,MAAM,GAAK,GACrC,EAAI,OAAS,WAAW,EAAI,OAAO,GAAK,GACzC,CACE,OAAO,QAAQ,CACf,KAAK,IAAI,CACS,KAEvB,MAAQ,GAAQ,eAAe,EAAI,IAAI,qBACvC,MAAQ,GAAQ,eAAe,EAAI,IAAI,qBACvC,KAAO,GACL,YAAY,EAAI,IAAI,cAAc,EAAI,UAAY,GAAG,IAAI,EAAI,UAAY,EAAI,IAAI,MACpF,CAaD,eAAsB,EACpB,EACA,EACA,EACiB,CACjB,IAAM,EAAO,EAAe,EAAK,CACjC,GAAI,EAAK,SAAW,EAAG,OAAO,EAG9B,IAAM,EAAc,MAAM,EAAS,EAAK,CAGpC,EAAS,EACb,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAW,EAAY,IAAI,EAAI,GAAG,CACxC,GAAI,CAAC,EAAU,SAEf,IAAM,EAAW,IAAY,EAAI,OAAS,EAAiB,EAAI,MAC1D,IAEL,EAAS,EAAO,QAAQ,EAAI,IAAK,EAAS,EAAS,CAAC,EAGtD,OAAO"}
@@ -0,0 +1,2 @@
1
+ import{n as e,r as t,t as n}from"./variant-key-DZUYURS5.mjs";import"server-only";async function r(r){let{db:i,storage:a,logger:o,styles:s}=r,{AdminClient:c}=await import(`@murumets-ee/entity/admin`),{Media:l}=await import(`./entity-DZFku8b7.mjs`),{schemaRegistry:u}=await import(`@murumets-ee/db`),{eq:d}=await import(`drizzle-orm`),f=new c({entity:l,db:i,logger:o}),p=u.get(`media`);if(!p)throw Error(`Media schema not registered`);let m={total:0,processed:0,skipped:0,errors:0},h=0;for(o?.info({styles:Object.keys(s)},`Starting variant regeneration`);;){let r=await f.findMany({where:d(p.mediaType,`image`),limit:100,offset:h});if(r.length===0)break;m.total+=r.length;for(let i of r)try{if(!e(i.mimeType)){m.skipped++;continue}let r=await a.download(i.fileKey),c;if(Buffer.isBuffer(r.body))c=r.body;else{let e=[],t=r.body.getReader();for(;;){let{done:n,value:r}=await t.read();if(n)break;r&&e.push(r)}c=Buffer.concat(e)}let l=await t(c,s),u=await a.getMetadata(i.fileKey),d=u?.metadata?.variants;if(d)for(let e of Object.values(d))await a.delete(e).catch(()=>{});let f={},p=u?.visibility??`public`;for(let[e,t]of l.variants.entries()){let r=n(i.fileKey,e,t.format);try{await a.upload(t.buffer,{key:r,filename:`${e}_${i.filename}`,mimeType:t.mimeType,size:t.buffer.byteLength,visibility:p,metadata:{variantOf:i.fileKey,style:e}}),f[e]=r}catch(t){o?.warn({style:e,key:r,error:t},`Failed to upload regenerated variant (non-fatal)`)}}Object.keys(f).length>0&&await a.updateMetadata(i.fileKey,{metadata:{...u?.metadata??{},variants:f}}).catch(e=>{o?.warn({key:i.fileKey,error:e},`Failed to update variant metadata (non-fatal)`)}),m.processed++,o?.debug({id:i.id,variants:Object.keys(f)},`Regenerated variants`)}catch(e){m.errors++,o?.error({id:i.id,fileKey:i.fileKey,error:e},`Failed to regenerate variants for media record`)}if(h+=100,r.length<100)break}return o?.info(m,`Variant regeneration complete`),m}export{r as regenerateAllVariants};
2
+ //# sourceMappingURL=regenerate-variants-DY7D4Ky3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regenerate-variants-DY7D4Ky3.mjs","names":[],"sources":["../src/regenerate-variants.ts"],"sourcesContent":["/**\n * Batch variant regeneration — reprocesses all image media with updated styles.\n *\n * Downloads each original from storage, generates new variants via Sharp,\n * cleans up old variant files, and uploads new ones.\n *\n * Per-image errors are logged but don't stop the batch.\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { StorageClient } from '@murumets-ee/storage'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { isProcessableImage, processImage } from './process-image.js'\nimport type { ImageStyle, MediaRecord } from './types.js'\nimport { deriveVariantKey } from './variant-key.js'\n\nconst BATCH_SIZE = 100\n\nexport interface RegenerateOptions {\n db: PostgresJsDatabase\n storage: StorageClient\n logger?: Logger\n /** Current image styles to generate */\n styles: Record<string, ImageStyle>\n}\n\nexport interface RegenerateResult {\n /** Total image media records found */\n total: number\n /** Successfully reprocessed */\n processed: number\n /** Skipped (non-processable mimeType, download failed, etc.) */\n skipped: number\n /** Failed with errors */\n errors: number\n}\n\n/**\n * Regenerate variants for all image media.\n * Processes in batches of 100 to avoid memory pressure.\n */\nexport async function regenerateAllVariants(options: RegenerateOptions): Promise<RegenerateResult> {\n const { db, storage, logger, styles } = options\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n const { Media } = await import('./entity.js')\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n\n const admin = new AdminClient({ entity: Media, db, logger })\n const table = schemaRegistry.get('media')\n if (!table) throw new Error('Media schema not registered')\n\n const result: RegenerateResult = { total: 0, processed: 0, skipped: 0, errors: 0 }\n let offset = 0\n\n logger?.info({ styles: Object.keys(styles) }, 'Starting variant regeneration')\n\n // Process in batches\n while (true) {\n const batch = (await admin.findMany({\n where: eq(table.mediaType, 'image'),\n limit: BATCH_SIZE,\n offset,\n })) as unknown as MediaRecord[]\n\n if (batch.length === 0) break\n result.total += batch.length\n\n for (const record of batch) {\n try {\n // Skip non-processable images (SVG, GIF)\n if (!isProcessableImage(record.mimeType)) {\n result.skipped++\n continue\n }\n\n // Download original from storage\n const downloaded = await storage.download(record.fileKey)\n let buffer: Buffer\n if (Buffer.isBuffer(downloaded.body)) {\n buffer = downloaded.body\n } else {\n // ReadableStream → Buffer\n const chunks: Uint8Array[] = []\n const reader = downloaded.body.getReader()\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n buffer = Buffer.concat(chunks)\n }\n\n // Generate new variants\n const processed = await processImage(buffer, styles)\n\n // Delete old variant files (best-effort)\n const oldFileRecord = await storage.getMetadata(record.fileKey)\n const oldVariants = (oldFileRecord?.metadata as Record<string, unknown> | null)?.variants as\n | Record<string, string>\n | undefined\n if (oldVariants) {\n for (const vKey of Object.values(oldVariants)) {\n await storage.delete(vKey).catch(() => {})\n }\n }\n\n // Upload new variants\n const newVariantKeys: Record<string, string> = {}\n const visibility = oldFileRecord?.visibility ?? 'public'\n for (const [styleName, variant] of processed.variants.entries()) {\n const vKey = deriveVariantKey(record.fileKey, styleName, variant.format)\n try {\n await storage.upload(variant.buffer, {\n key: vKey,\n filename: `${styleName}_${record.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility,\n metadata: { variantOf: record.fileKey, style: styleName },\n })\n newVariantKeys[styleName] = vKey\n } catch (uploadErr) {\n logger?.warn(\n { style: styleName, key: vKey, error: uploadErr },\n 'Failed to upload regenerated variant (non-fatal)',\n )\n }\n }\n\n // Update original file's metadata with new variant keys\n if (Object.keys(newVariantKeys).length > 0) {\n await storage\n .updateMetadata(record.fileKey, {\n metadata: {\n ...(oldFileRecord?.metadata ?? {}),\n variants: newVariantKeys,\n },\n })\n .catch((metaErr: unknown) => {\n logger?.warn(\n { key: record.fileKey, error: metaErr },\n 'Failed to update variant metadata (non-fatal)',\n )\n })\n }\n\n result.processed++\n logger?.debug(\n { id: record.id, variants: Object.keys(newVariantKeys) },\n 'Regenerated variants',\n )\n } catch (err) {\n result.errors++\n logger?.error(\n { id: record.id, fileKey: record.fileKey, error: err },\n 'Failed to regenerate variants for media record',\n )\n }\n }\n\n offset += BATCH_SIZE\n if (batch.length < BATCH_SIZE) break\n }\n\n logger?.info(result, 'Variant regeneration complete')\n return result\n}\n"],"mappings":"iFA2CA,eAAsB,EAAsB,EAAuD,CACjG,GAAM,CAAE,KAAI,UAAS,SAAQ,UAAW,EAClC,CAAE,eAAgB,MAAM,OAAO,6BAC/B,CAAE,SAAU,MAAM,OAAO,yBACzB,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAO,MAAM,OAAO,eAEtB,EAAQ,IAAI,EAAY,CAAE,OAAQ,EAAO,KAAI,SAAQ,CAAC,CACtD,EAAQ,EAAe,IAAI,QAAQ,CACzC,GAAI,CAAC,EAAO,MAAU,MAAM,8BAA8B,CAE1D,IAAM,EAA2B,CAAE,MAAO,EAAG,UAAW,EAAG,QAAS,EAAG,OAAQ,EAAG,CAC9E,EAAS,EAKb,IAHA,GAAQ,KAAK,CAAE,OAAQ,OAAO,KAAK,EAAO,CAAE,CAAE,gCAAgC,GAGjE,CACX,IAAM,EAAS,MAAM,EAAM,SAAS,CAClC,MAAO,EAAG,EAAM,UAAW,QAAQ,CACnC,MAAO,IACP,SACD,CAAC,CAEF,GAAI,EAAM,SAAW,EAAG,MACxB,EAAO,OAAS,EAAM,OAEtB,IAAK,IAAM,KAAU,EACnB,GAAI,CAEF,GAAI,CAAC,EAAmB,EAAO,SAAS,CAAE,CACxC,EAAO,UACP,SAIF,IAAM,EAAa,MAAM,EAAQ,SAAS,EAAO,QAAQ,CACrD,EACJ,GAAI,OAAO,SAAS,EAAW,KAAK,CAClC,EAAS,EAAW,SACf,CAEL,IAAM,EAAuB,EAAE,CACzB,EAAS,EAAW,KAAK,WAAW,CAC1C,OAAa,CACX,GAAM,CAAE,OAAM,SAAU,MAAM,EAAO,MAAM,CAC3C,GAAI,EAAM,MACN,GAAO,EAAO,KAAK,EAAM,CAE/B,EAAS,OAAO,OAAO,EAAO,CAIhC,IAAM,EAAY,MAAM,EAAa,EAAQ,EAAO,CAG9C,EAAgB,MAAM,EAAQ,YAAY,EAAO,QAAQ,CACzD,EAAe,GAAe,UAA6C,SAGjF,GAAI,EACF,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAY,CAC3C,MAAM,EAAQ,OAAO,EAAK,CAAC,UAAY,GAAG,CAK9C,IAAM,EAAyC,EAAE,CAC3C,EAAa,GAAe,YAAc,SAChD,IAAK,GAAM,CAAC,EAAW,KAAY,EAAU,SAAS,SAAS,CAAE,CAC/D,IAAM,EAAO,EAAiB,EAAO,QAAS,EAAW,EAAQ,OAAO,CACxE,GAAI,CACF,MAAM,EAAQ,OAAO,EAAQ,OAAQ,CACnC,IAAK,EACL,SAAU,GAAG,EAAU,GAAG,EAAO,WACjC,SAAU,EAAQ,SAClB,KAAM,EAAQ,OAAO,WACrB,aACA,SAAU,CAAE,UAAW,EAAO,QAAS,MAAO,EAAW,CAC1D,CAAC,CACF,EAAe,GAAa,QACrB,EAAW,CAClB,GAAQ,KACN,CAAE,MAAO,EAAW,IAAK,EAAM,MAAO,EAAW,CACjD,mDACD,EAKD,OAAO,KAAK,EAAe,CAAC,OAAS,GACvC,MAAM,EACH,eAAe,EAAO,QAAS,CAC9B,SAAU,CACR,GAAI,GAAe,UAAY,EAAE,CACjC,SAAU,EACX,CACF,CAAC,CACD,MAAO,GAAqB,CAC3B,GAAQ,KACN,CAAE,IAAK,EAAO,QAAS,MAAO,EAAS,CACvC,gDACD,EACD,CAGN,EAAO,YACP,GAAQ,MACN,CAAE,GAAI,EAAO,GAAI,SAAU,OAAO,KAAK,EAAe,CAAE,CACxD,uBACD,OACM,EAAK,CACZ,EAAO,SACP,GAAQ,MACN,CAAE,GAAI,EAAO,GAAI,QAAS,EAAO,QAAS,MAAO,EAAK,CACtD,iDACD,CAKL,GADA,GAAU,IACN,EAAM,OAAS,IAAY,MAIjC,OADA,GAAQ,KAAK,EAAQ,gCAAgC,CAC9C"}
@@ -0,0 +1,103 @@
1
+ import { FileVisibility } from "@murumets-ee/storage";
2
+
3
+ //#region src/types.d.ts
4
+ /** Media type derived from MIME type */
5
+ type MediaType = 'image' | 'video' | 'audio' | 'document' | 'other';
6
+ /** Options for uploading media through MediaClient */
7
+ interface MediaUploadOptions {
8
+ /** Original filename */
9
+ filename: string;
10
+ /** MIME type */
11
+ mimeType: string;
12
+ /** File size in bytes */
13
+ size: number;
14
+ /** File visibility (default: from plugin config) */
15
+ visibility?: FileVisibility;
16
+ /** Display title (defaults to filename without extension) */
17
+ title?: string;
18
+ /** Alt text for images */
19
+ alt?: string;
20
+ /** Description */
21
+ description?: string;
22
+ /** Image width in pixels (client-measured) */
23
+ width?: number;
24
+ /** Image height in pixels (client-measured) */
25
+ height?: number;
26
+ /** UUID of the uploading user */
27
+ uploadedBy?: string;
28
+ }
29
+ /** Result of a media upload: entity record + storage URL */
30
+ interface MediaUploadResult {
31
+ /** The created media entity record */
32
+ media: MediaRecord;
33
+ /** The file's public/signed URL */
34
+ url: string;
35
+ }
36
+ /** Lightweight view of a media entity for API consumers */
37
+ interface MediaRecord {
38
+ id: string;
39
+ title: string | null;
40
+ alt: string | null;
41
+ description: string | null;
42
+ fileKey: string;
43
+ filename: string;
44
+ mimeType: string;
45
+ size: number;
46
+ width: number | null;
47
+ height: number | null;
48
+ mediaType: MediaType;
49
+ createdBy: string | null;
50
+ createdAt: Date | string;
51
+ updatedAt: Date | string;
52
+ }
53
+ /** Options for listing media */
54
+ interface MediaListOptions {
55
+ /** Filter by media type */
56
+ mediaType?: MediaType;
57
+ /** Filter by MIME type prefix (e.g., 'image/') */
58
+ mimeTypePrefix?: string;
59
+ /** Search in title and filename */
60
+ search?: string;
61
+ /** Pagination limit (default: 50) */
62
+ limit?: number;
63
+ /** Pagination offset (default: 0) */
64
+ offset?: number;
65
+ /** Order by field (default: 'createdAt') */
66
+ orderBy?: 'createdAt' | 'filename' | 'size';
67
+ /** Order direction (default: 'desc') */
68
+ orderDirection?: 'asc' | 'desc';
69
+ }
70
+ /** Paginated media list result */
71
+ interface MediaListResult {
72
+ items: MediaRecord[];
73
+ total: number;
74
+ limit: number;
75
+ offset: number;
76
+ }
77
+ /** Named image processing preset — defines how to resize/convert an image variant */
78
+ interface ImageStyle {
79
+ /** Max width in pixels (omit for height-only constraint) */
80
+ width?: number;
81
+ /** Max height in pixels (omit for height-only constraint) */
82
+ height?: number;
83
+ /** Resize strategy (default: 'cover') */
84
+ fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
85
+ /** Output format (default: 'webp') */
86
+ format?: 'webp' | 'jpeg' | 'png' | 'avif';
87
+ /** Output quality 1–100 (default: 80) */
88
+ quality?: number;
89
+ }
90
+ /** Plugin configuration for @murumets-ee/media */
91
+ interface MediaPluginConfig {
92
+ /** Accepted MIME type patterns (default: ['image/*', 'video/*', 'audio/*', 'application/pdf']) */
93
+ acceptedTypes?: string[];
94
+ /** Max upload size in bytes (default: 50MB) */
95
+ maxUploadSize?: number;
96
+ /** Default file visibility (default: 'public') */
97
+ defaultVisibility?: FileVisibility;
98
+ /** Named image processing presets. Variants generated on upload for each style. */
99
+ imageStyles?: Record<string, ImageStyle>;
100
+ }
101
+ //#endregion
102
+ export { MediaRecord as a, MediaUploadResult as c, MediaPluginConfig as i, MediaListOptions as n, MediaType as o, MediaListResult as r, MediaUploadOptions as s, ImageStyle as t };
103
+ //# sourceMappingURL=types-BV_pOm23.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BV_pOm23.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;KAGY,SAAA;AAAZ;AAAA,UAGiB,kBAAA;;EAEf,QAAA;EALmB;EAOnB,QAAA;EAJiC;EAMjC,IAAA;EAE2B;EAA3B,UAAA,GAAa,cAAA;EAJb;EAMA,KAAA;EAFA;EAIA,GAAA;EAFA;EAIA,WAAA;EAAA;EAEA,KAAA;EAEA;EAAA,MAAA;EAEU;EAAV,UAAA;AAAA;;UAIe,iBAAA;EAEG;EAAlB,KAAA,EAAO,WAAA;EAAA;EAEP,GAAA;AAAA;;UAIe,WAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,WAAA;EACA,OAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA,EAAW,SAAA;EACX,SAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;;UAII,gBAAA;EAPf;EASA,SAAA,GAAY,SAAA;EARZ;EAUA,cAAA;EATW;EAWX,MAAA;EAVW;EAYX,KAAA;EAZe;EAcf,MAAA;EAV+B;EAY/B,OAAA;EAVqB;EAYrB,cAAA;AAAA;;UAIe,eAAA;EACf,KAAA,EAAO,WAAA;EACP,KAAA;EACA,KAAA;EACA,MAAA;AAAA;;UAIe,UAAA;EARe;EAU9B,KAAA;EATkB;EAWlB,MAAA;EAXO;EAaP,GAAA;EAXA;EAaA,MAAA;EAZM;EAcN,OAAA;AAAA;;UAIe,iBAAA;EAdU;EAgBzB,aAAA;EAZA;EAcA,aAAA;EAVA;EAYA,iBAAA,GAAoB,cAAA;EAVb;EAYP,WAAA,GAAc,MAAA,SAAe,UAAA;AAAA"}
@@ -0,0 +1,2 @@
1
+ import"server-only";import{findEntityUsages as e}from"@murumets-ee/entity/refs";async function t(t,n){let r=await e(`media`,t,n),{getApp:i}=await import(`@murumets-ee/core`),a=i();return r.map(e=>{let t=`field`,n=a.entities.get(e.sourceEntity);if(n){let r=n.allFields[e.sourceField];r&&r.type===`blocks`&&(t=`block`)}return{entityName:e.sourceEntity,entityId:e.sourceId,fieldName:e.sourceField,context:t}})}export{t as findMediaUsages};
2
+ //# sourceMappingURL=usage-D7Bn7Vvv.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-D7Bn7Vvv.mjs","names":[],"sources":["../src/usage.ts"],"sourcesContent":["/**\n * Media usage lookup — finds all entity references to a media item.\n *\n * Delegates to the universal entity_refs tracking table (populated at write time\n * by AdminClient). One indexed query instead of scanning every table.\n *\n * Used by delete protection (409 Conflict) and the usage API endpoint.\n */\n\nimport 'server-only'\n\nimport { findEntityUsages } from '@murumets-ee/entity/refs'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\nexport interface MediaUsage {\n entityName: string\n entityId: string\n fieldName: string\n /** 'field' = entity column, 'block' = layout/blocks field */\n context: 'field' | 'block'\n}\n\n/**\n * Find all entities that reference a given media ID.\n *\n * @param mediaId - UUID of the media item to check\n * @param db - Database connection\n * @returns Array of usage records (empty if unreferenced)\n */\nexport async function findMediaUsages(\n mediaId: string,\n db: PostgresJsDatabase,\n): Promise<MediaUsage[]> {\n const usages = await findEntityUsages('media', mediaId, db)\n\n // Derive context from field name — blocks fields are tracked with their field name\n // which we can check against entity definitions. For simplicity, we check if the\n // source entity has a blocks-type field matching the source field name.\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n\n return usages.map((usage) => {\n let context: 'field' | 'block' = 'field'\n const entity = app.entities.get(usage.sourceEntity)\n if (entity) {\n const fieldConfig = entity.allFields[usage.sourceField]\n if (fieldConfig && fieldConfig.type === 'blocks') {\n context = 'block'\n }\n }\n\n return {\n entityName: usage.sourceEntity,\n entityId: usage.sourceId,\n fieldName: usage.sourceField,\n context,\n }\n })\n}\n"],"mappings":"gFA6BA,eAAsB,EACpB,EACA,EACuB,CACvB,IAAM,EAAS,MAAM,EAAiB,QAAS,EAAS,EAAG,CAKrD,CAAE,UAAW,MAAM,OAAO,qBAC1B,EAAM,GAAQ,CAEpB,OAAO,EAAO,IAAK,GAAU,CAC3B,IAAI,EAA6B,QAC3B,EAAS,EAAI,SAAS,IAAI,EAAM,aAAa,CACnD,GAAI,EAAQ,CACV,IAAM,EAAc,EAAO,UAAU,EAAM,aACvC,GAAe,EAAY,OAAS,WACtC,EAAU,SAId,MAAO,CACL,WAAY,EAAM,aAClB,SAAU,EAAM,SAChB,UAAW,EAAM,YACjB,UACD,EACD"}
@@ -0,0 +1,21 @@
1
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+
3
+ //#region src/usage.d.ts
4
+ interface MediaUsage {
5
+ entityName: string;
6
+ entityId: string;
7
+ fieldName: string;
8
+ /** 'field' = entity column, 'block' = layout/blocks field */
9
+ context: 'field' | 'block';
10
+ }
11
+ /**
12
+ * Find all entities that reference a given media ID.
13
+ *
14
+ * @param mediaId - UUID of the media item to check
15
+ * @param db - Database connection
16
+ * @returns Array of usage records (empty if unreferenced)
17
+ */
18
+ declare function findMediaUsages(mediaId: string, db: PostgresJsDatabase): Promise<MediaUsage[]>;
19
+ //#endregion
20
+ export { MediaUsage, findMediaUsages };
21
+ //# sourceMappingURL=usage.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.mts","names":[],"sources":["../src/usage.ts"],"mappings":";;;UAciB,UAAA;EACf,UAAA;EACA,QAAA;EACA,SAAA;EAYoB;EAVpB,OAAA;AAAA;;;;;;;;iBAUoB,eAAA,CACpB,OAAA,UACA,EAAA,EAAI,kBAAA,GACH,OAAA,CAAQ,UAAA"}
package/dist/usage.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import"server-only";import{findEntityUsages as e}from"@murumets-ee/entity/refs";async function t(t,n){let r=await e(`media`,t,n),{getApp:i}=await import(`@murumets-ee/core`),a=i();return r.map(e=>{let t=`field`,n=a.entities.get(e.sourceEntity);if(n){let r=n.allFields[e.sourceField];r&&r.type===`blocks`&&(t=`block`)}return{entityName:e.sourceEntity,entityId:e.sourceId,fieldName:e.sourceField,context:t}})}export{t as findMediaUsages};
2
+ //# sourceMappingURL=usage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.mjs","names":[],"sources":["../src/usage.ts"],"sourcesContent":["/**\n * Media usage lookup — finds all entity references to a media item.\n *\n * Delegates to the universal entity_refs tracking table (populated at write time\n * by AdminClient). One indexed query instead of scanning every table.\n *\n * Used by delete protection (409 Conflict) and the usage API endpoint.\n */\n\nimport 'server-only'\n\nimport { findEntityUsages } from '@murumets-ee/entity/refs'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\nexport interface MediaUsage {\n entityName: string\n entityId: string\n fieldName: string\n /** 'field' = entity column, 'block' = layout/blocks field */\n context: 'field' | 'block'\n}\n\n/**\n * Find all entities that reference a given media ID.\n *\n * @param mediaId - UUID of the media item to check\n * @param db - Database connection\n * @returns Array of usage records (empty if unreferenced)\n */\nexport async function findMediaUsages(\n mediaId: string,\n db: PostgresJsDatabase,\n): Promise<MediaUsage[]> {\n const usages = await findEntityUsages('media', mediaId, db)\n\n // Derive context from field name — blocks fields are tracked with their field name\n // which we can check against entity definitions. For simplicity, we check if the\n // source entity has a blocks-type field matching the source field name.\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n\n return usages.map((usage) => {\n let context: 'field' | 'block' = 'field'\n const entity = app.entities.get(usage.sourceEntity)\n if (entity) {\n const fieldConfig = entity.allFields[usage.sourceField]\n if (fieldConfig && fieldConfig.type === 'blocks') {\n context = 'block'\n }\n }\n\n return {\n entityName: usage.sourceEntity,\n entityId: usage.sourceId,\n fieldName: usage.sourceField,\n context,\n }\n })\n}\n"],"mappings":"gFA6BA,eAAsB,EACpB,EACA,EACuB,CACvB,IAAM,EAAS,MAAM,EAAiB,QAAS,EAAS,EAAG,CAKrD,CAAE,UAAW,MAAM,OAAO,qBAC1B,EAAM,GAAQ,CAEpB,OAAO,EAAO,IAAK,GAAU,CAC3B,IAAI,EAA6B,QAC3B,EAAS,EAAI,SAAS,IAAI,EAAM,aAAa,CACnD,GAAI,EAAQ,CACV,IAAM,EAAc,EAAO,UAAU,EAAM,aACvC,GAAe,EAAY,OAAS,WACtC,EAAU,SAId,MAAO,CACL,WAAY,EAAM,aAClB,SAAU,EAAM,SAChB,UAAW,EAAM,YACjB,UACD,EACD"}
@@ -0,0 +1,2 @@
1
+ import e from"sharp";const t=new Set([`image/svg+xml`,`image/gif`]);function n(e){return e.startsWith(`image/`)&&!t.has(e)}async function r(t,n){let r=await e(t).metadata(),i=r.width??0,a=r.height??0,o=new Map,s=Object.entries(n);return await Promise.all(s.map(async([n,r])=>{let i=r.format??`webp`,a=r.quality??80,s=r.fit??`cover`,{data:c,info:l}=await e(t).resize({width:r.width,height:r.height,fit:s,withoutEnlargement:!0})[i]({quality:a}).toBuffer({resolveWithObject:!0});o.set(n,{buffer:c,format:i,mimeType:`image/${i}`,width:l.width,height:l.height})})),{width:i,height:a,variants:o}}function i(e,t,n=`webp`){let r=e.lastIndexOf(`/`);return`${e.substring(0,r)}/${t}_${e.substring(r+1).replace(/\.[^.]+$/,``)}.${n}`}export{n,r,i as t};
2
+ //# sourceMappingURL=variant-key-DZUYURS5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variant-key-DZUYURS5.mjs","names":[],"sources":["../src/process-image.ts","../src/variant-key.ts"],"sourcesContent":["/**\n * Server-side image processing via Sharp.\n *\n * Pure processing module — no DB or storage dependencies.\n * Takes a buffer and image styles, returns metadata + variant buffers.\n */\n\nimport sharp from 'sharp'\nimport type { ImageStyle } from './types.js'\n\n/** Result of processing an image through Sharp */\nexport interface ProcessedImage {\n /** Original image width in pixels */\n width: number\n /** Original image height in pixels */\n height: number\n /** Generated variant buffers keyed by style name */\n variants: Map<string, ProcessedVariant>\n}\n\n/** A single processed variant */\nexport interface ProcessedVariant {\n buffer: Buffer\n format: string\n mimeType: string\n width: number\n height: number\n}\n\n/** MIME types that should NOT be processed (vectors, animations) */\nconst SKIP_MIME_TYPES = new Set(['image/svg+xml', 'image/gif'])\n\n/**\n * Check if a MIME type is eligible for Sharp processing.\n * Returns false for SVG, GIF, and non-image types.\n */\nexport function isProcessableImage(mimeType: string): boolean {\n return mimeType.startsWith('image/') && !SKIP_MIME_TYPES.has(mimeType)\n}\n\n/**\n * Extract image dimensions and generate resized variants.\n *\n * @param buffer - Original image file as a Buffer\n * @param styles - Named image style presets to generate\n * @returns Metadata (width/height) and variant buffers\n */\nexport async function processImage(\n buffer: Buffer,\n styles: Record<string, ImageStyle>,\n): Promise<ProcessedImage> {\n const meta = await sharp(buffer).metadata()\n const width = meta.width ?? 0\n const height = meta.height ?? 0\n\n const variants = new Map<string, ProcessedVariant>()\n\n const entries = Object.entries(styles)\n await Promise.all(\n entries.map(async ([name, style]) => {\n const fmt = style.format ?? 'webp'\n const quality = style.quality ?? 80\n const fit = style.fit ?? 'cover'\n\n const resized = sharp(buffer).resize({\n width: style.width,\n height: style.height,\n fit,\n withoutEnlargement: true,\n })\n\n const { data: variantBuffer, info } = await resized[fmt]({ quality }).toBuffer({\n resolveWithObject: true,\n })\n\n variants.set(name, {\n buffer: variantBuffer,\n format: fmt,\n mimeType: `image/${fmt}`,\n width: info.width,\n height: info.height,\n })\n }),\n )\n\n return { width, height, variants }\n}\n\n/**\n * Extract only image dimensions (no variant generation).\n * Useful for getting width/height when styles are empty.\n */\nexport async function getImageDimensions(\n buffer: Buffer,\n): Promise<{ width: number; height: number }> {\n const meta = await sharp(buffer).metadata()\n return { width: meta.width ?? 0, height: meta.height ?? 0 }\n}\n","/**\n * Variant key derivation convention.\n *\n * Given an original storage key and a style name, produces a deterministic\n * variant key in the same directory.\n *\n * Convention:\n * Original: uploads/2026/02/{uuid}/photo.jpg\n * Variant: uploads/2026/02/{uuid}/thumbnail_photo.webp\n */\n\n/**\n * Derive a variant storage key from the original key + style name.\n *\n * @param originalKey - The original file's storage key\n * @param styleName - The image style name (e.g., 'thumbnail', 'medium')\n * @param format - The variant output format (default: 'webp')\n * @returns The derived variant key\n */\nexport function deriveVariantKey(originalKey: string, styleName: string, format = 'webp'): string {\n const lastSlash = originalKey.lastIndexOf('/')\n const dir = originalKey.substring(0, lastSlash)\n const filename = originalKey.substring(lastSlash + 1)\n const baseName = filename.replace(/\\.[^.]+$/, '')\n return `${dir}/${styleName}_${baseName}.${format}`\n}\n"],"mappings":"qBA8BA,MAAM,EAAkB,IAAI,IAAI,CAAC,gBAAiB,YAAY,CAAC,CAM/D,SAAgB,EAAmB,EAA2B,CAC5D,OAAO,EAAS,WAAW,SAAS,EAAI,CAAC,EAAgB,IAAI,EAAS,CAUxE,eAAsB,EACpB,EACA,EACyB,CACzB,IAAM,EAAO,MAAM,EAAM,EAAO,CAAC,UAAU,CACrC,EAAQ,EAAK,OAAS,EACtB,EAAS,EAAK,QAAU,EAExB,EAAW,IAAI,IAEf,EAAU,OAAO,QAAQ,EAAO,CA4BtC,OA3BA,MAAM,QAAQ,IACZ,EAAQ,IAAI,MAAO,CAAC,EAAM,KAAW,CACnC,IAAM,EAAM,EAAM,QAAU,OACtB,EAAU,EAAM,SAAW,GAC3B,EAAM,EAAM,KAAO,QASnB,CAAE,KAAM,EAAe,QAAS,MAPtB,EAAM,EAAO,CAAC,OAAO,CACnC,MAAO,EAAM,MACb,OAAQ,EAAM,OACd,MACA,mBAAoB,GACrB,CAAC,CAEkD,GAAK,CAAE,UAAS,CAAC,CAAC,SAAS,CAC7E,kBAAmB,GACpB,CAAC,CAEF,EAAS,IAAI,EAAM,CACjB,OAAQ,EACR,OAAQ,EACR,SAAU,SAAS,IACnB,MAAO,EAAK,MACZ,OAAQ,EAAK,OACd,CAAC,EACF,CACH,CAEM,CAAE,QAAO,SAAQ,WAAU,CClEpC,SAAgB,EAAiB,EAAqB,EAAmB,EAAS,OAAgB,CAChG,IAAM,EAAY,EAAY,YAAY,IAAI,CAI9C,MAAO,GAHK,EAAY,UAAU,EAAG,EAAU,CAGjC,GAAG,EAAU,GAFV,EAAY,UAAU,EAAY,EAAE,CAC3B,QAAQ,WAAY,GAAG,CACV,GAAG"}
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
1
  {
2
2
  "name": "@murumets-ee/media",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs"
10
10
  },
11
11
  "./client": {
12
- "types": "./dist/client.d.ts",
13
- "import": "./dist/client.js"
12
+ "types": "./dist/client.d.mts",
13
+ "import": "./dist/client.mjs"
14
14
  },
15
15
  "./query": {
16
- "types": "./dist/query-client.d.ts",
17
- "import": "./dist/query-client.js"
16
+ "types": "./dist/query-client.d.mts",
17
+ "import": "./dist/query-client.mjs"
18
18
  },
19
19
  "./plugin": {
20
- "types": "./dist/plugin.d.ts",
21
- "import": "./dist/plugin.js"
20
+ "types": "./dist/plugin.d.mts",
21
+ "import": "./dist/plugin.mjs"
22
22
  },
23
23
  "./ref": {
24
- "types": "./dist/ref.d.ts",
25
- "import": "./dist/ref.js"
24
+ "types": "./dist/ref.d.mts",
25
+ "import": "./dist/ref.mjs"
26
26
  },
27
27
  "./picker": {
28
- "types": "./dist/picker.d.ts",
29
- "import": "./dist/picker.js"
28
+ "types": "./dist/picker.d.mts",
29
+ "import": "./dist/picker.mjs"
30
30
  },
31
31
  "./admin": {
32
- "types": "./dist/admin.d.ts",
33
- "import": "./dist/admin.js"
32
+ "types": "./dist/admin.d.mts",
33
+ "import": "./dist/admin.mjs"
34
34
  },
35
35
  "./usage": {
36
- "types": "./dist/usage.d.ts",
37
- "import": "./dist/usage.js"
36
+ "types": "./dist/usage.d.mts",
37
+ "import": "./dist/usage.mjs"
38
38
  },
39
39
  "./image-styles": {
40
- "types": "./dist/image-styles.d.ts",
41
- "import": "./dist/image-styles.js"
40
+ "types": "./dist/image-styles.d.mts",
41
+ "import": "./dist/image-styles.mjs"
42
42
  }
43
43
  },
44
44
  "files": [
@@ -52,18 +52,18 @@
52
52
  "server-only": "^0.0.1",
53
53
  "sharp": "^0.34.5",
54
54
  "tailwind-merge": "^2.6.0",
55
- "@murumets-ee/core": "0.1.5",
56
- "@murumets-ee/entity": "0.1.4",
57
- "@murumets-ee/settings": "0.1.5",
58
- "@murumets-ee/storage": "0.1.5",
59
- "@murumets-ee/logging": "0.1.5",
60
- "@murumets-ee/db": "0.1.4"
55
+ "@murumets-ee/logging": "0.1.6",
56
+ "@murumets-ee/settings": "0.1.6",
57
+ "@murumets-ee/core": "0.1.6",
58
+ "@murumets-ee/storage": "0.1.6",
59
+ "@murumets-ee/db": "0.1.5",
60
+ "@murumets-ee/entity": "0.1.5"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "lucide-react": ">=0.400.0",
64
64
  "react": ">=19.0.0",
65
65
  "react-dom": ">=19.0.0",
66
- "@murumets-ee/ui": "0.1.4"
66
+ "@murumets-ee/ui": "0.1.5"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "@murumets-ee/ui": {
@@ -78,13 +78,13 @@
78
78
  "lucide-react": "^0.563.0",
79
79
  "react": "^19.0.0",
80
80
  "react-dom": "^19.0.0",
81
- "tsup": "^8.3.5",
81
+ "tsdown": "^0.21.7",
82
82
  "typescript": "^5.7.3",
83
83
  "vitest": "^2.1.8"
84
84
  },
85
85
  "scripts": {
86
- "build": "rm -rf dist && tsup",
87
- "dev": "tsup --watch",
86
+ "build": "tsdown",
87
+ "dev": "tsdown --watch",
88
88
  "test": "vitest"
89
89
  }
90
90
  }
package/dist/admin.js DELETED
@@ -1 +0,0 @@
1
- var P=["cover","contain","inside","outside","fill"],j=["webp","jpeg","png","avif"];function F(r){for(let[e,a]of Object.entries(r)){if(typeof e!="string"||e.length===0||!/^[a-z][a-z0-9_-]*$/.test(e))return{valid:!1,error:`Invalid style name "${e}": must be lowercase alphanumeric (a-z, 0-9, -, _)`};if(!a||typeof a!="object")return{valid:!1,error:`Style "${e}" must be an object`};let t=a;if(t.width!==void 0&&(typeof t.width!="number"||t.width<=0))return{valid:!1,error:`Invalid width for style "${e}": must be a positive number`};if(t.height!==void 0&&(typeof t.height!="number"||t.height<=0))return{valid:!1,error:`Invalid height for style "${e}": must be a positive number`};if(t.width===void 0&&t.height===void 0)return{valid:!1,error:`Style "${e}" must have at least width or height`};if(t.quality!==void 0&&(typeof t.quality!="number"||t.quality<1||t.quality>100))return{valid:!1,error:`Invalid quality for style "${e}": must be 1-100`};if(t.fit!==void 0&&!P.includes(t.fit))return{valid:!1,error:`Invalid fit for style "${e}": must be one of ${P.join(", ")}`};if(t.format!==void 0&&!j.includes(t.format))return{valid:!1,error:`Invalid format for style "${e}": must be one of ${j.join(", ")}`}}return{valid:!0,styles:r}}var k=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function U(r){return k.test(r)}var A=null;function I(){return A||(A=(async()=>{let{getApp:r}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:a}=await import("@murumets-ee/storage/plugin"),{MediaClient:t}=await import("./client-37WG2Y6P.js"),o=r(),g=a(),b=e(g,{app:o});return new t({db:o.db.readWrite,storage:b,logger:o.logger.child({media:!0})})})()),A}function c(r,e=200){return new Response(JSON.stringify(r),{status:e,headers:{"Content-Type":"application/json"}})}function l(r,e){return c({error:r},e)}async function q(r,{segments:e}){let a=await I();if(e.length===1&&e[0]==="settings"){let{createSettingsClient:i}=await import("@murumets-ee/settings"),{imageStylesSettings:d}=await import("./image-styles-settings-AB5WFEFF.js"),{getApp:s}=await import("@murumets-ee/core"),u=s(),h=await i(d,{app:u}).get("imageStyles");return c({imageStyles:h??{}})}if(e.length===2&&e[1]==="usage"){let i=e[0];if(!U(i))return l("Invalid media ID format",400);let{findMediaUsages:d}=await import("./usage-E5RZMWE4.js"),{getApp:s}=await import("@murumets-ee/core"),u=s(),f=await d(i,u.db.readWrite);return c({usages:f})}if(e.length>0){let i=e[0],d=await a.findById(i);if(!d)return l("Media not found",404);let s=await a.getUrl(i);return c({...d,url:s})}let t=new URL(r.url),o=t.searchParams.get("search")??void 0,g=t.searchParams.get("mediaType")??void 0,b=Math.min(Math.max(Number(t.searchParams.get("limit"))||24,1),100),m=Math.max(Number(t.searchParams.get("offset"))||0,0),p=await a.findMany({search:o,mediaType:g,limit:b,offset:m}),y=p.items.map(i=>i.id),[T,v]=await Promise.all([a.getUrls(y),a.getVariantUrls(y,"thumbnail")]),n={items:p.items.map(i=>({id:i.id,title:i.title,alt:i.alt,filename:i.filename,mimeType:i.mimeType,size:i.size,mediaType:i.mediaType,url:T.get(i.id)??"",thumbnailUrl:v.get(i.id),width:i.width,height:i.height})),total:p.total};return c(n)}async function L(r,{segments:e,user:a,audit:t,checkPermission:o}){if(e.length===1&&e[0]==="regenerate-variants"){if(!o("media","update"))return l("Forbidden: media update permission required for variant regeneration",403);let{createSettingsClient:d}=await import("@murumets-ee/settings"),{imageStylesSettings:s}=await import("./image-styles-settings-AB5WFEFF.js"),{regenerateAllVariants:u}=await import("./regenerate-variants-IUDIIWXU.js"),{getApp:f}=await import("@murumets-ee/core"),{createStorageClient:h}=await import("@murumets-ee/storage"),{getStorageConfig:M}=await import("@murumets-ee/storage/plugin"),w=f(),C=await d(s,{app:w}).get("imageStyles");if(!C||Object.keys(C).length===0)return l("No image styles configured",400);let z=M(),D=h(z,{app:w}),S=await u({db:w.db.readWrite,storage:D,logger:w.logger.child({media:!0}),styles:C});return t?.({action:"media.regenerate_variants",userId:a.id,userName:a.name,metadata:{total:S.total,processed:S.processed,errors:S.errors}}),c(S)}if(e.length===1&&e[0]==="settings"){if(!o("media","update"))return l("Forbidden: media update permission required for image style management",403);let d=await r.json();if(!d.imageStyles||typeof d.imageStyles!="object")return l('Body must contain "imageStyles" object',400);let s=F(d.imageStyles);if(!s.valid)return l(s.error,400);let{createSettingsClient:u}=await import("@murumets-ee/settings"),{imageStylesSettings:f}=await import("./image-styles-settings-AB5WFEFF.js"),{getApp:h}=await import("@murumets-ee/core"),M=h();return await u(f,{app:M}).set("imageStyles",s.styles),(await I()).invalidateImageStylesCache(),t?.({action:"media.settings.update",entityType:"settings",userId:a.id,userName:a.name,changes:{imageStyles:s.styles}}),c({imageStyles:s.styles})}let g=await I(),m=(await r.formData()).get("file");if(!m||m.size===0)return l("No file provided",400);let p=50*1024*1024;if(m.size>p)return l(`File too large: ${(m.size/1024/1024).toFixed(1)} MB exceeds 50 MB limit`,400);let y=Buffer.from(await m.arrayBuffer()),{detectMimeType:T}=await import("@murumets-ee/storage"),{mimeType:v,mismatch:R}=await T(y,m.type||"application/octet-stream");if(R)return l(`File content doesn't match declared type: claimed ${m.type}, detected ${v}`,400);let n=await g.upload(y,{filename:m.name,mimeType:v,size:m.size,uploadedBy:a.id}),i={id:n.media.id,title:n.media.title,alt:n.media.alt,filename:n.media.filename,mimeType:n.media.mimeType,size:n.media.size,mediaType:n.media.mediaType,url:n.url,width:n.media.width,height:n.media.height};return t?.({action:"media.upload",entityType:"media",entityId:n.media.id,userId:a.id,userName:a.name,changes:{filename:n.media.filename,mimeType:n.media.mimeType,size:n.media.size,mediaType:n.media.mediaType}}),c(i,201)}async function N(r,{segments:e,user:a,audit:t}){if(e.length===0)return l("Media ID required",400);let o=e[0];return U(o)?(await(await I()).delete(o),t?.({action:"media.delete",entityType:"media",entityId:o,userId:a.id,userName:a.name}),c({deleted:1})):l("Invalid media ID format",400)}function _(){return{prefix:"media",resource:"media",actions:["view","create","update","delete"],handlers:{GET:q,POST:L,DELETE:N}}}export{_ as mediaRoutes};
@@ -1 +0,0 @@
1
- import{defineSettings as e,setting as t}from"@murumets-ee/settings";var a=e({namespace:"media.imageStyles",scope:"global",label:"Image Styles",schema:{imageStyles:t.json({default:{thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}},label:"Image processing presets"})}});export{a};
@@ -1 +0,0 @@
1
- import{behavior as t,defineEntity as r,field as e}from"@murumets-ee/entity";var u=r({name:"media",fields:{title:e.text({translatable:!0}),alt:e.text({translatable:!0}),description:e.text({translatable:!0}),fileKey:e.text({required:!0,indexed:!0}),filename:e.text({required:!0}),mimeType:e.text({required:!0,indexed:!0}),size:e.number({required:!0,integer:!0}),width:e.number({integer:!0}),height:e.number({integer:!0}),mediaType:e.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[t.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}});export{u as a};
@@ -1 +0,0 @@
1
- import c from"sharp";var p=new Set(["image/svg+xml","image/gif"]);function l(t){return t.startsWith("image/")&&!p.has(t)}async function P(t,a){let i=await c(t).metadata(),r=i.width??0,n=i.height??0,s=new Map,o=Object.entries(a);return await Promise.all(o.map(async([g,e])=>{let m=e.format??"webp",f=e.quality??80,u=e.fit??"cover",d=c(t).resize({width:e.width,height:e.height,fit:u,withoutEnlargement:!0}),{data:w,info:h}=await d[m]({quality:f}).toBuffer({resolveWithObject:!0});s.set(g,{buffer:w,format:m,mimeType:`image/${m}`,width:h.width,height:h.height})})),{width:r,height:n,variants:s}}function x(t,a,i="webp"){let r=t.lastIndexOf("/"),n=t.substring(0,r),o=t.substring(r+1).replace(/\.[^.]+$/,"");return`${n}/${a}_${o}.${i}`}export{l as a,P as b,x as c};
@@ -1 +0,0 @@
1
- import{behavior as t,defineEntity as r,field as e}from"@murumets-ee/entity";var u=r({name:"media",fields:{title:e.text({translatable:!0}),alt:e.text({translatable:!0}),description:e.text({translatable:!0}),fileKey:e.text({required:!0,indexed:!0}),filename:e.text({required:!0}),mimeType:e.text({required:!0,indexed:!0}),size:e.number({required:!0,integer:!0}),width:e.number({integer:!0}),height:e.number({integer:!0}),mediaType:e.select({options:["image","video","audio","document","other"],required:!0,indexed:!0})},behaviors:[t.auditable()],scope:"global",access:{view:"public",create:"group.editor",update:"group.editor",delete:"group.admin"}});export{u as a};
@@ -1 +0,0 @@
1
- import{a as b}from"./chunk-YAHM4C5J.js";import{a as v,b as k,c as h}from"./chunk-QTUXM53A.js";import"server-only";var f=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:e}=await import("@murumets-ee/entity/admin");this.admin=new e({entity:b,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import("@murumets-ee/settings"),{imageStylesSettings:t}=await import("./image-styles-settings-AB5WFEFF.js"),{getApp:i}=await import("@murumets-ee/core"),a=i(),r=await e(t,{app:a}).get("imageStyles");if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import("./plugin-CAV5BZMF.js"),t=e();return this.imageStyles=t.imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let i=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),a=t.width??null,s=t.height??null,r={};if(e instanceof Buffer&&v(t.mimeType))try{let l=await this.resolveImageStyles(),n=await k(e,l);a=n.width,s=n.height;let m=i.visibility;await Promise.all([...n.variants.entries()].map(async([d,y])=>{let u=h(i.key,d,y.format);try{await this.storage.upload(y.buffer,{key:u,filename:`${d}_${t.filename}`,mimeType:y.mimeType,size:y.buffer.byteLength,visibility:m,metadata:{variantOf:i.key,style:d},uploadedBy:t.uploadedBy}),r[d]=u,this.logger?.debug({style:d,key:u,width:y.width,height:y.height},"Variant uploaded")}catch(w){this.logger?.warn({style:d,key:u,error:w},"Failed to upload variant (non-fatal)")}})),Object.keys(r).length>0&&await this.storage.updateMetadata(i.key,{metadata:{...i.metadata??{},variants:r}}).catch(d=>{this.logger?.warn({key:i.key,error:d},"Failed to update original file metadata with variant keys (non-fatal)")}),this.logger?.info({width:a,height:s,variants:Object.keys(r)},"Image processed")}catch(l){this.logger?.warn({filename:t.filename,error:l},"Image processing failed (non-fatal, original saved)")}let o=C(t.mimeType),c=await this.getAdmin();try{let l=await c.create({title:t.title??P(t.filename),alt:t.alt??null,description:t.description??null,fileKey:i.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:a,height:s,mediaType:o}),n=await this.storage.getUrl(i.key);return this.logger?.info({id:l.id,fileKey:i.key,mediaType:o},"Media uploaded"),{media:l,url:n}}catch(l){this.logger?.error({fileKey:i.key,error:l},"Media entity creation failed, rolling back storage uploads");for(let n of Object.values(r))await this.storage.delete(n).catch(()=>{});throw await this.storage.delete(i.key).catch(n=>{this.logger?.error({fileKey:i.key,error:n},"Storage rollback also failed")}),l}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{and:a,asc:s,desc:r,eq:o,ilike:c,sql:l}=await import("drizzle-orm"),n=i.get("media");if(!n)throw new Error("Media schema not registered. Is the media() plugin loaded?");let m=[];if(e?.mediaType&&m.push(o(n.mediaType,e.mediaType)),e?.mimeTypePrefix&&m.push(c(n.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let M=`%${e.search}%`;m.push(l`(${c(n.filename,M)} OR ${n.fields} ->> 'title' ILIKE ${M})`)}let d=e?.limit??50,y=e?.offset??0,u=m.length>0?a(...m):void 0,[w]=await this.db.select({count:l`count(*)::int`}).from(n).where(u),S=e?.orderBy==="filename"?n.filename:n.createdAt,R=(e?.orderDirection??"desc")==="asc"?s:r;return{items:await t.findMany({where:u,limit:d,offset:y,orderBy:R(S)}),total:w?.count??0,limit:d,offset:y}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),i=await t.findById(e);if(!i)throw new Error(`Media not found: ${e}`);let a=i.fileKey;await t.delete(e);let r=(await this.storage.getMetadata(a))?.metadata?.variants;if(r)for(let o of Object.values(r))await this.storage.delete(o).catch(c=>{this.logger?.warn({variantKey:o,error:c},"Failed to delete variant file")});await this.storage.delete(a).catch(o=>{this.logger?.error({id:e,fileKey:a,error:o},"Failed to delete file from storage after entity deletion")}),this.logger?.info({id:e,fileKey:a,deletedVariants:r?Object.keys(r).length:0},"Media deleted")}async getUrl(e){let i=await(await this.getAdmin()).findById(e);if(!i)throw new Error(`Media not found: ${e}`);return this.storage.getUrl(i.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{inArray:a}=await import("drizzle-orm"),s=i.get("media");if(!s)return new Map;let r=await t.findMany({where:a(s.id,e),limit:e.length}),o=new Map;return await Promise.all(r.map(async c=>{let l=await this.storage.getUrl(c.fileKey);o.set(c.id,l)})),o}async getVariantUrl(e,t){let a=await(await this.getAdmin()).findById(e);if(!a)return null;let s=a.fileKey,o=(await this.resolveImageStyles())[t];if(o){let c=h(s,t,o.format??"webp");try{return await this.storage.getUrl(c)}catch{}}try{return await this.storage.getUrl(s)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let i=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{inArray:s}=await import("drizzle-orm"),r=a.get("media");if(!r)return new Map;let o=await i.findMany({where:s(r.id,e),limit:e.length}),l=(await this.resolveImageStyles())[t],n=new Map;return await Promise.all(o.map(async m=>{if(l){let d=h(m.fileKey,t,l.format??"webp");try{let y=await this.storage.getUrl(d);n.set(m.id,y);return}catch{}}try{let d=await this.storage.getUrl(m.fileKey);n.set(m.id,d)}catch{}})),n}};function C(g){return g.startsWith("image/")?"image":g.startsWith("video/")?"video":g.startsWith("audio/")?"audio":g==="application/pdf"||g.startsWith("application/msword")||g.startsWith("application/vnd.")?"document":"other"}function P(g){return g.replace(/\.[^.]+$/,"").replace(/[-_]/g," ")}async function B(g){let{getApp:e}=await import("@murumets-ee/core"),t=e();return new f({db:t.db.readWrite,storage:g,logger:t.logger.child({media:!0})})}var p=null;function F(){return p||(p=(async()=>{let{getApp:g}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),i=g(),a=t(),s=e(a,{app:i});return new f({db:i.db.readWrite,storage:s,logger:i.logger.child({media:!0})})})()),p}export{f as MediaClient,B as createMediaClient,F as getMediaClient};
package/dist/client.d.ts DELETED
@@ -1,102 +0,0 @@
1
- import { Logger } from '@murumets-ee/core';
2
- import { StorageClient } from '@murumets-ee/storage';
3
- import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
4
- import { I as ImageStyle, M as MediaUploadOptions, a as MediaUploadResult, b as MediaRecord, c as MediaListOptions, d as MediaListResult } from './types-ChlTxvlq.js';
5
-
6
- /**
7
- * MediaClient — wraps AdminClient + StorageClient for media management.
8
- *
9
- * Usage:
10
- * import { createMediaClient } from '@murumets-ee/media/client'
11
- * const media = await createMediaClient(storageClient)
12
- * const result = await media.upload(buffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', size: 12345 })
13
- */
14
-
15
- interface MediaClientConfig {
16
- db: PostgresJsDatabase;
17
- storage: StorageClient;
18
- logger?: Logger;
19
- /** Image styles to generate on upload. Loaded from plugin config if not provided. */
20
- imageStyles?: Record<string, ImageStyle>;
21
- }
22
- declare class MediaClient {
23
- private db;
24
- private storage;
25
- private logger?;
26
- private admin;
27
- private imageStyles;
28
- constructor(config: MediaClientConfig);
29
- private getAdmin;
30
- /**
31
- * Resolve image styles: settings DB → plugin config → hardcoded defaults.
32
- * Result is cached; call `invalidateImageStylesCache()` after settings update.
33
- */
34
- private resolveImageStyles;
35
- /** Clear cached styles so next access re-reads from settings DB. */
36
- invalidateImageStylesCache(): void;
37
- /**
38
- * Upload a file and create a media entity record in one step.
39
- * 1. Uploads original to storage (StorageClient)
40
- * 2. If image: extracts dimensions via Sharp + generates variants
41
- * 3. Creates media entity record (AdminClient)
42
- * 4. Returns the media record + URL
43
- *
44
- * Rolls back storage upload if entity creation fails.
45
- * Variant generation failures are logged but don't fail the upload.
46
- */
47
- upload(data: Buffer | ReadableStream<Uint8Array>, options: MediaUploadOptions): Promise<MediaUploadResult>;
48
- findById(id: string, options?: {
49
- locale?: string;
50
- }): Promise<MediaRecord | null>;
51
- findMany(options?: MediaListOptions): Promise<MediaListResult>;
52
- update(id: string, data: {
53
- title?: string;
54
- alt?: string;
55
- description?: string;
56
- }): Promise<MediaRecord>;
57
- /**
58
- * Delete a media entity, its variants, and its original file in storage.
59
- */
60
- delete(id: string): Promise<void>;
61
- /**
62
- * Get URL for a media entity by its ID.
63
- * Resolves entity -> fileKey -> storage URL.
64
- */
65
- getUrl(id: string): Promise<string>;
66
- /**
67
- * Get URLs for multiple media entities (batch).
68
- * Returns a Map of mediaId -> url.
69
- */
70
- getUrls(ids: string[]): Promise<Map<string, string>>;
71
- /**
72
- * Get variant URL for a specific image style.
73
- * Falls back to original URL if the variant doesn't exist.
74
- *
75
- * @param id - Media entity ID
76
- * @param styleName - Image style name (e.g., 'thumbnail')
77
- * @returns The variant URL, or original URL as fallback, or null if media not found
78
- */
79
- getVariantUrl(id: string, styleName: string): Promise<string | null>;
80
- /**
81
- * Get variant URLs for multiple media entities (batch).
82
- * Falls back to original URL per item if the variant doesn't exist.
83
- *
84
- * @param ids - Media entity IDs
85
- * @param styleName - Image style name (e.g., 'thumbnail')
86
- * @returns Map of mediaId -> variant URL (or original URL as fallback)
87
- */
88
- getVariantUrls(ids: string[], styleName: string): Promise<Map<string, string>>;
89
- }
90
- /**
91
- * Factory — creates a MediaClient with an explicit StorageClient.
92
- * Must be called after createApp().
93
- */
94
- declare function createMediaClient(storage: StorageClient): Promise<MediaClient>;
95
- /**
96
- * Lazy singleton — returns a shared MediaClient instance.
97
- * Auto-discovers storage configuration from the storage plugin.
98
- * Must be called after createApp().
99
- */
100
- declare function getMediaClient(): Promise<MediaClient>;
101
-
102
- export { MediaClient, type MediaClientConfig, createMediaClient, getMediaClient };
package/dist/client.js DELETED
@@ -1 +0,0 @@
1
- import{a as b}from"./chunk-L6BDKI76.js";import"server-only";import v from"sharp";var I=new Set(["image/svg+xml","image/gif"]);function S(s){return s.startsWith("image/")&&!I.has(s)}async function k(s,e){let t=await v(s).metadata(),i=t.width??0,a=t.height??0,o=new Map,r=Object.entries(e);return await Promise.all(r.map(async([d,g])=>{let l=g.format??"webp",n=g.quality??80,m=g.fit??"cover",c=v(s).resize({width:g.width,height:g.height,fit:m,withoutEnlargement:!0}),{data:u,info:h}=await c[l]({quality:n}).toBuffer({resolveWithObject:!0});o.set(d,{buffer:u,format:l,mimeType:`image/${l}`,width:h.width,height:h.height})})),{width:i,height:a,variants:o}}function f(s,e,t="webp"){let i=s.lastIndexOf("/"),a=s.substring(0,i),r=s.substring(i+1).replace(/\.[^.]+$/,"");return`${a}/${e}_${r}.${t}`}var y=class{db;storage;logger;admin=null;imageStyles;constructor(e){this.db=e.db,this.storage=e.storage,this.logger=e.logger,this.imageStyles=e.imageStyles??null}async getAdmin(){if(!this.admin){let{AdminClient:e}=await import("@murumets-ee/entity/admin");this.admin=new e({entity:b,db:this.db,logger:this.logger})}return this.admin}async resolveImageStyles(){if(this.imageStyles)return this.imageStyles;try{let{createSettingsClient:e}=await import("@murumets-ee/settings"),{imageStylesSettings:t}=await import("./image-styles-settings.js"),{getApp:i}=await import("@murumets-ee/core"),a=i(),r=await e(t,{app:a}).get("imageStyles");if(r&&Object.keys(r).length>0)return this.imageStyles=r,r}catch{}try{let{getMediaConfig:e}=await import("./plugin.js"),t=e();return this.imageStyles=t.imageStyles,this.imageStyles}catch{let e={thumbnail:{width:200,height:200,fit:"cover",format:"webp",quality:80}};return this.imageStyles=e,e}}invalidateImageStylesCache(){this.imageStyles=null}async upload(e,t){let i=await this.storage.upload(e,{filename:t.filename,mimeType:t.mimeType,size:t.size,visibility:t.visibility,uploadedBy:t.uploadedBy}),a=t.width??null,o=t.height??null,r={};if(e instanceof Buffer&&S(t.mimeType))try{let l=await this.resolveImageStyles(),n=await k(e,l);a=n.width,o=n.height;let m=i.visibility;await Promise.all([...n.variants.entries()].map(async([c,u])=>{let h=f(i.key,c,u.format);try{await this.storage.upload(u.buffer,{key:h,filename:`${c}_${t.filename}`,mimeType:u.mimeType,size:u.buffer.byteLength,visibility:m,metadata:{variantOf:i.key,style:c},uploadedBy:t.uploadedBy}),r[c]=h,this.logger?.debug({style:c,key:h,width:u.width,height:u.height},"Variant uploaded")}catch(w){this.logger?.warn({style:c,key:h,error:w},"Failed to upload variant (non-fatal)")}})),Object.keys(r).length>0&&await this.storage.updateMetadata(i.key,{metadata:{...i.metadata??{},variants:r}}).catch(c=>{this.logger?.warn({key:i.key,error:c},"Failed to update original file metadata with variant keys (non-fatal)")}),this.logger?.info({width:a,height:o,variants:Object.keys(r)},"Image processed")}catch(l){this.logger?.warn({filename:t.filename,error:l},"Image processing failed (non-fatal, original saved)")}let d=C(t.mimeType),g=await this.getAdmin();try{let l=await g.create({title:t.title??A(t.filename),alt:t.alt??null,description:t.description??null,fileKey:i.key,filename:t.filename,mimeType:t.mimeType,size:t.size,width:a,height:o,mediaType:d}),n=await this.storage.getUrl(i.key);return this.logger?.info({id:l.id,fileKey:i.key,mediaType:d},"Media uploaded"),{media:l,url:n}}catch(l){this.logger?.error({fileKey:i.key,error:l},"Media entity creation failed, rolling back storage uploads");for(let n of Object.values(r))await this.storage.delete(n).catch(()=>{});throw await this.storage.delete(i.key).catch(n=>{this.logger?.error({fileKey:i.key,error:n},"Storage rollback also failed")}),l}}async findById(e,t){return await(await this.getAdmin()).findById(e,t)}async findMany(e){let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{and:a,asc:o,desc:r,eq:d,ilike:g,sql:l}=await import("drizzle-orm"),n=i.get("media");if(!n)throw new Error("Media schema not registered. Is the media() plugin loaded?");let m=[];if(e?.mediaType&&m.push(d(n.mediaType,e.mediaType)),e?.mimeTypePrefix&&m.push(g(n.mimeType,`${e.mimeTypePrefix}%`)),e?.search){let M=`%${e.search}%`;m.push(l`(${g(n.filename,M)} OR ${n.fields} ->> 'title' ILIKE ${M})`)}let c=e?.limit??50,u=e?.offset??0,h=m.length>0?a(...m):void 0,[w]=await this.db.select({count:l`count(*)::int`}).from(n).where(h),R=e?.orderBy==="filename"?n.filename:n.createdAt,P=(e?.orderDirection??"desc")==="asc"?o:r;return{items:await t.findMany({where:h,limit:c,offset:u,orderBy:P(R)}),total:w?.count??0,limit:c,offset:u}}async update(e,t){return await(await this.getAdmin()).update(e,t)}async delete(e){let t=await this.getAdmin(),i=await t.findById(e);if(!i)throw new Error(`Media not found: ${e}`);let a=i.fileKey;await t.delete(e);let r=(await this.storage.getMetadata(a))?.metadata?.variants;if(r)for(let d of Object.values(r))await this.storage.delete(d).catch(g=>{this.logger?.warn({variantKey:d,error:g},"Failed to delete variant file")});await this.storage.delete(a).catch(d=>{this.logger?.error({id:e,fileKey:a,error:d},"Failed to delete file from storage after entity deletion")}),this.logger?.info({id:e,fileKey:a,deletedVariants:r?Object.keys(r).length:0},"Media deleted")}async getUrl(e){let i=await(await this.getAdmin()).findById(e);if(!i)throw new Error(`Media not found: ${e}`);return this.storage.getUrl(i.fileKey)}async getUrls(e){if(e.length===0)return new Map;let t=await this.getAdmin(),{schemaRegistry:i}=await import("@murumets-ee/db"),{inArray:a}=await import("drizzle-orm"),o=i.get("media");if(!o)return new Map;let r=await t.findMany({where:a(o.id,e),limit:e.length}),d=new Map;return await Promise.all(r.map(async g=>{let l=await this.storage.getUrl(g.fileKey);d.set(g.id,l)})),d}async getVariantUrl(e,t){let a=await(await this.getAdmin()).findById(e);if(!a)return null;let o=a.fileKey,d=(await this.resolveImageStyles())[t];if(d){let g=f(o,t,d.format??"webp");try{return await this.storage.getUrl(g)}catch{}}try{return await this.storage.getUrl(o)}catch{return null}}async getVariantUrls(e,t){if(e.length===0)return new Map;let i=await this.getAdmin(),{schemaRegistry:a}=await import("@murumets-ee/db"),{inArray:o}=await import("drizzle-orm"),r=a.get("media");if(!r)return new Map;let d=await i.findMany({where:o(r.id,e),limit:e.length}),l=(await this.resolveImageStyles())[t],n=new Map;return await Promise.all(d.map(async m=>{if(l){let c=f(m.fileKey,t,l.format??"webp");try{let u=await this.storage.getUrl(c);n.set(m.id,u);return}catch{}}try{let c=await this.storage.getUrl(m.fileKey);n.set(m.id,c)}catch{}})),n}};function C(s){return s.startsWith("image/")?"image":s.startsWith("video/")?"video":s.startsWith("audio/")?"audio":s==="application/pdf"||s.startsWith("application/msword")||s.startsWith("application/vnd.")?"document":"other"}function A(s){return s.replace(/\.[^.]+$/,"").replace(/[-_]/g," ")}async function F(s){let{getApp:e}=await import("@murumets-ee/core"),t=e();return new y({db:t.db.readWrite,storage:s,logger:t.logger.child({media:!0})})}var p=null;function V(){return p||(p=(async()=>{let{getApp:s}=await import("@murumets-ee/core"),{createStorageClient:e}=await import("@murumets-ee/storage"),{getStorageConfig:t}=await import("@murumets-ee/storage/plugin"),i=s(),a=t(),o=e(a,{app:i});return new y({db:i.db.readWrite,storage:o,logger:i.logger.child({media:!0})})})()),p}export{y as MediaClient,F as createMediaClient,V as getMediaClient};