@zerotal/media 1.4.0 → 1.5.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/src/index.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  // @zerotal/media — public API
2
+ //
3
+ // Test and advanced-wiring seams (state swapping, disk resolver overrides,
4
+ // queue-bridge internals) live at `@zerotal/media/testing`. They are
5
+ // deliberately not re-exported here: this entry point is what the package's
6
+ // stability guarantee covers, and it should stay small enough to keep.
2
7
 
3
8
  // Model + model mixin
4
9
  // `Media` is the mixin — it reads as `Model.using(Media)`. `MediaItem` is one
@@ -19,7 +24,7 @@ export type { CollectionHost } from "./collections/resolve.ts";
19
24
  export { applyRetentionRules } from "./collections/retention.ts";
20
25
 
21
26
  // Conversions
22
- export { ConversionRunner, partitionConversions } from "./conversions/ConversionRunner.ts";
27
+ export { ConversionRunner } from "./conversions/ConversionRunner.ts";
23
28
  export { BunImageDriver } from "./conversions/BunImageDriver.ts";
24
29
  export { SharpImageDriver } from "./conversions/SharpImageDriver.ts";
25
30
  export {
@@ -34,13 +39,7 @@ export type {
34
39
  ImageMetadata,
35
40
  ImageResult,
36
41
  } from "./conversions/ImageDriver.ts";
37
- export {
38
- setConversionDispatcher,
39
- isQueueAvailable,
40
- dispatchConversions,
41
- } from "./conversions/dispatch.ts";
42
- export type { ConversionDispatcher } from "./conversions/dispatch.ts";
43
- export { performConversions, ownerClassFor } from "./conversions/queueBridge.ts";
42
+ export { isQueueAvailable, dispatchConversions } from "./conversions/dispatch.ts";
44
43
 
45
44
  // Paths
46
45
  export { DefaultPathGenerator } from "./paths/PathGenerator.ts";
@@ -50,15 +49,8 @@ export type { PathGenerator } from "./paths/PathGenerator.ts";
50
49
  export { fromValue, fromUrl, fromDisk, fromPath } from "./sources.ts";
51
50
  export type { MediaSource, ResolvedSource, SourceResolver } from "./sources.ts";
52
51
 
53
- // Disk resolution (the seam tests use to skip building a container)
54
- export {
55
- diskFor,
56
- diskNameFor,
57
- defaultDiskName,
58
- setDiskResolver,
59
- setDefaultDiskName,
60
- } from "./support/disks.ts";
61
- export type { DiskResolver } from "./support/disks.ts";
52
+ // Disk resolution
53
+ export { diskFor, defaultDiskName } from "./support/disks.ts";
62
54
 
63
55
  // Schema provisioning
64
56
  export { mediaSchemaConcern } from "./mediaSchemaConcern.ts";
@@ -67,10 +59,6 @@ export { mediaSchemaConcern } from "./mediaSchemaConcern.ts";
67
59
  export { MediaConfig, mediaDefaults } from "./config.ts";
68
60
  export type { MediaConfigShape } from "./config.ts";
69
61
 
70
- // Shared state (mainly for tests and advanced wiring)
71
- export { mediaState, setMediaState, resetMediaState } from "./state.ts";
72
- export type { MediaState } from "./state.ts";
73
-
74
62
  // Testing
75
63
  export { MediaFake } from "./MediaFake.ts";
76
64
 
@@ -12,6 +12,8 @@ import { Schema } from "@zerotal/orm";
12
12
  *
13
13
  * Mirrors `auditSchemaConcern`, and exists for the same reason: the alternative
14
14
  * is an app that boots cleanly and then fails on its first upload, in production.
15
+ *
16
+ * @internal — registered by MediaProvider so the `media` table provisions itself.
15
17
  */
16
18
  export const mediaSchemaConcern: ConcernDescriptor = {
17
19
  name: "media-schema",
@@ -25,7 +25,7 @@ export interface PathGenerator {
25
25
  * media/<uuid>/responsive/640.webp
26
26
  * ```
27
27
  *
28
- * Keyed on `uuid` rather than the numeric `id` that Laravel's media library
28
+ * Keyed on `uuid` rather than the numeric `id` a media library normally
29
29
  * uses. These paths end up in public URLs, and a sequential id in a public URL
30
30
  * discloses how many rows the table has — plus it lets anyone walk the range.
31
31
  * The uuid costs nothing and leaks nothing.
package/src/sources.ts CHANGED
@@ -3,6 +3,7 @@ import { MediaError } from "./errors.ts";
3
3
  import { diskFor } from "./support/disks.ts";
4
4
 
5
5
  /** A file's bytes plus the name it arrived under. */
6
+ /** @internal — source-resolution plumbing. */
6
7
  export interface ResolvedSource {
7
8
  bytes: Uint8Array;
8
9
  /** Original filename, used to derive the default media name. */
@@ -10,6 +11,7 @@ export interface ResolvedSource {
10
11
  }
11
12
 
12
13
  /** Anything that can be resolved to bytes on demand. */
14
+ /** @internal — source-resolution plumbing. */
13
15
  export type SourceResolver = () => Promise<ResolvedSource>;
14
16
 
15
17
  /** Things `addMedia()` accepts directly. */
@@ -21,6 +23,8 @@ export type MediaSource = UploadedFile | File | Blob | Uint8Array | ArrayBuffer;
21
23
  * Resolution is deferred so a rule that can reject on metadata alone — a
22
24
  * collection that accepts only PDFs, say — does not have to buffer the file
23
25
  * first.
26
+ *
27
+ * @internal — source resolution; apps pass a `MediaSource` to `addMedia*`.
24
28
  */
25
29
  export function fromValue(source: MediaSource, fileName?: string): SourceResolver {
26
30
  if (source instanceof UploadedFile) {
@@ -66,6 +70,8 @@ export function fromValue(source: MediaSource, fileName?: string): SourceResolve
66
70
  * often enough that downloading whatever arrives is how one request exhausts the
67
71
  * heap; the limit is checked against `Content-Length` first and again against
68
72
  * what actually arrived, since the header is a claim.
73
+ *
74
+ * @internal — source resolution; apps pass a `MediaSource` to `addMedia*`.
69
75
  */
70
76
  export function fromUrl(url: string, maxBytes: number): SourceResolver {
71
77
  return async () => {
@@ -108,6 +114,7 @@ export function fromUrl(url: string, maxBytes: number): SourceResolver {
108
114
  }
109
115
 
110
116
  /** Read a file already sitting on one of the app's storage disks. */
117
+ /** @internal — source resolution; apps pass a `MediaSource` to `addMedia*`. */
111
118
  export function fromDisk(path: string, disk?: string): SourceResolver {
112
119
  return async () => {
113
120
  const bytes = await diskFor(disk).getBuffer(path);
@@ -119,6 +126,7 @@ export function fromDisk(path: string, disk?: string): SourceResolver {
119
126
  }
120
127
 
121
128
  /** Read a file from the local filesystem. */
129
+ /** @internal — source resolution; apps pass a `MediaSource` to `addMedia*`. */
122
130
  export function fromPath(path: string): SourceResolver {
123
131
  return async () => {
124
132
  const file = Bun.file(path);
@@ -33,6 +33,8 @@ export function setDiskResolver(resolver: DiskResolver | null): void {
33
33
  * empty string is looked up literally and throws `DiskNotConfiguredError`. Media
34
34
  * config uses `""` to mean "inherit the default", so every lookup goes through
35
35
  * here rather than reaching for `Storage.disk()` directly.
36
+ *
37
+ * @internal — disk resolution.
36
38
  */
37
39
  export function diskFor(name?: string | null): StorageDriver {
38
40
  const trimmed = name?.trim();
@@ -65,6 +67,8 @@ export function setDefaultDiskName(name: string | null): void {
65
67
  *
66
68
  * Falls back to `""` when there is no app or no storage config to ask — the read
67
69
  * path treats that as "the default", which is the best answer available.
70
+ *
71
+ * @internal — disk resolution.
68
72
  */
69
73
  export function defaultDiskName(): string {
70
74
  if (_defaultName !== null) return _defaultName;
package/src/testing.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Test and advanced-wiring seams for `@zerotal/media`.
3
+ *
4
+ * These let a test drive the package without standing up a container: swap the
5
+ * disk resolver, plant a config/driver pair, reset it afterwards. They are the
6
+ * package's own internals, exposed because testing media without them is
7
+ * genuinely awkward — not because they are part of the API.
8
+ *
9
+ * They live behind `@zerotal/media/testing` so the main entry point can be
10
+ * frozen while these stay free to change:
11
+ *
12
+ * ```ts
13
+ * import { setDiskResolver, resetMediaState } from "@zerotal/media/testing";
14
+ * ```
15
+ *
16
+ * Nothing here is covered by the package's stability guarantee. If you find
17
+ * yourself needing one of these in application code, that is a gap in the real
18
+ * API worth reporting.
19
+ */
20
+
21
+ // Shared state — the config and driver every operation reads.
22
+ export { mediaState, setMediaState, resetMediaState } from "./state.ts";
23
+ export type { MediaState } from "./state.ts";
24
+
25
+ // Disk resolution — the seam that lets tests skip building a container.
26
+ export { setDiskResolver, setDefaultDiskName, diskNameFor } from "./support/disks.ts";
27
+ export type { DiskResolver } from "./support/disks.ts";
28
+
29
+ // Conversion dispatch — swap the queue for a spy, or force the inline path.
30
+ export { setConversionDispatcher } from "./conversions/dispatch.ts";
31
+ export type { ConversionDispatcher } from "./conversions/dispatch.ts";
32
+
33
+ // Queue-bridge internals, reachable for tests that assert on job behaviour.
34
+ export { performConversions, ownerClassFor } from "./conversions/queueBridge.ts";
35
+ export { partitionConversions } from "./conversions/ConversionRunner.ts";
package/src/types.ts CHANGED
@@ -24,8 +24,13 @@ export type ConversionFormat = SafeConversionFormat | "avif" | "heic";
24
24
  * ratio. The result may be smaller than the box in one dimension.
25
25
  * - `fill` — stretch to exactly `width` × `height`, ignoring aspect ratio.
26
26
  * - `cover` — scale and centre-crop to exactly fill the box, preserving aspect
27
- * ratio. **Requires an image driver that can crop**; the default
28
- * `BunImageDriver` cannot, and says so with a named error.
27
+ * ratio. Needs both `width` and `height`: given one, there is nothing to crop
28
+ * away and it behaves as `inside`.
29
+ *
30
+ * With `withoutEnlargement` (the default), a source too small to fill the box is
31
+ * never scaled up — `cover` then yields the largest centre window the source can
32
+ * supply, which may not have the requested aspect ratio. A 300×500 source asked
33
+ * to cover 400×400 gives 300×400.
29
34
  */
30
35
  export type ConversionFit = "inside" | "fill" | "cover";
31
36
 
@@ -43,6 +48,15 @@ export interface ConversionDefinition {
43
48
  quality?: number;
44
49
  /** Clockwise rotation in degrees applied before resizing. */
45
50
  rotate?: number;
51
+ /**
52
+ * Allow scaling a source *up* to meet the target box. Default: `false`.
53
+ *
54
+ * Off by default because upscaling produces a larger file that looks worse.
55
+ * Turn it on when the exact box matters more than fidelity — a `cover`
56
+ * thumbnail for a fixed-size grid slot, say, which would otherwise come back
57
+ * undersized (and off-aspect) for small sources.
58
+ */
59
+ allowEnlargement?: boolean;
46
60
  /** Generate this conversion on the queue instead of inline. Default: `false`. */
47
61
  queued?: boolean;
48
62
  }