@adonis-agora/media 0.12.1 → 0.13.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/CHANGELOG.md +38 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +25 -2
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/src/dashboard/define_config.d.ts +17 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/package.json +9 -6
- package/skills/media-attachments-single-file/SKILL.md +224 -0
- package/skills/media-library-collections/SKILL.md +334 -0
- package/skills/media-setup/SKILL.md +253 -0
- package/skills/media-stores-delivery/SKILL.md +302 -0
- package/skills/media-uploads-resumable/SKILL.md +347 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: media-library-collections
|
|
3
|
+
description: >-
|
|
4
|
+
Attach files to owner collections with @adonis-agora/media — media.library.attach /
|
|
5
|
+
attachExisting / for(ownerType, ownerId), the MediaCollectionConfig policy layer
|
|
6
|
+
(acceptsMimeTypes exact-match whitelist plus magic-byte signature verification,
|
|
7
|
+
single:true atomic replace, append-only ordering, per-collection disk), eager vs lazy
|
|
8
|
+
ConversionPresets (width/height/fit/format/quality, webp default), ensureConversion,
|
|
9
|
+
url/signedUrl resolution, delete semantics, deterministic id retries, customProperties,
|
|
10
|
+
and the MediaRecord.conversions map shared with transformers. Use when uploading files
|
|
11
|
+
to entities, defining collections, generating thumbnails, or fixing
|
|
12
|
+
MimeNotAllowedError / ConversionNotDefinedError / ImageProcessorMissingError /
|
|
13
|
+
TransformNotReadyError.
|
|
14
|
+
license: MIT
|
|
15
|
+
metadata:
|
|
16
|
+
type: core
|
|
17
|
+
library: "@adonis-agora/media"
|
|
18
|
+
library_version: "0.12.1"
|
|
19
|
+
framework: adonisjs
|
|
20
|
+
sources:
|
|
21
|
+
- "DavideCarvalho/adonis-media:docs/getting-started.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-media:docs/collections-and-conversions.mdx"
|
|
23
|
+
- "DavideCarvalho/adonis-media:docs/transformers.mdx"
|
|
24
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/media_library.ts"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Media library: collections, policy, and conversions
|
|
28
|
+
|
|
29
|
+
`media.library` is the spatie/laravel-media-library shape: files belong to an owner
|
|
30
|
+
(`ownerType` + `ownerId`, both just stored strings — the library never loads your models)
|
|
31
|
+
inside named collections that carry the policy. Attach writes bytes to a Drive disk and a
|
|
32
|
+
metadata record to the configured `MediaStore`.
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
```ts title="app/controllers/post_images_controller.ts"
|
|
37
|
+
import { createReadStream } from 'node:fs'
|
|
38
|
+
import Post from '#models/post'
|
|
39
|
+
import media from '@adonis-agora/media/services/main'
|
|
40
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
41
|
+
|
|
42
|
+
export default class PostImagesController {
|
|
43
|
+
async store({ request, response, params }: HttpContext) {
|
|
44
|
+
const post = await Post.findOrFail(params.id)
|
|
45
|
+
|
|
46
|
+
const image = request.file('image', { size: '5mb', extnames: ['jpg', 'png', 'webp'] })
|
|
47
|
+
if (!image) return response.badRequest({ error: 'An "image" file is required' })
|
|
48
|
+
|
|
49
|
+
const record = await media.library.attach({
|
|
50
|
+
ownerType: 'Post',
|
|
51
|
+
ownerId: post.id, // string | number — coerced for you
|
|
52
|
+
collection: 'gallery',
|
|
53
|
+
fileName: image.clientName,
|
|
54
|
+
mimeType: image.type!,
|
|
55
|
+
contents: createReadStream(image.tmpPath!), // Buffer | Readable
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return response.created({ id: record.id, url: await media.library.url(record.id) })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The file lands at `${ownerType}/${ownerId}/${collection}/${id}/${fileName}` on the resolved
|
|
64
|
+
disk (per-call `disk` → collection `disk` → storage default). Eager conversions are
|
|
65
|
+
generated synchronously inside `attach`; lazy ones wait for the first `url(id, name)`.
|
|
66
|
+
|
|
67
|
+
Source: `docs/getting-started.mdx` (Step 3).
|
|
68
|
+
|
|
69
|
+
## Core patterns
|
|
70
|
+
|
|
71
|
+
### Pattern 1 — declare the collection policy in config
|
|
72
|
+
|
|
73
|
+
Collections are opt-in constraints: an undeclared collection is permissive (no whitelist,
|
|
74
|
+
not single, no conversions). Declare one to constrain it:
|
|
75
|
+
|
|
76
|
+
```ts title="config/media.ts"
|
|
77
|
+
defineConfig({
|
|
78
|
+
collections: [
|
|
79
|
+
{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
|
|
80
|
+
{ name: 'invoices', disk: 'private-s3', acceptsMimeTypes: ['application/pdf'] },
|
|
81
|
+
{
|
|
82
|
+
name: 'gallery',
|
|
83
|
+
conversions: [
|
|
84
|
+
{ name: 'thumb', width: 200, height: 200, fit: 'cover' },
|
|
85
|
+
{ name: 'card', width: 600, format: 'webp', quality: 80 },
|
|
86
|
+
{ name: 'og', width: 1200, height: 630, eager: true },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`acceptsMimeTypes` is checked **before anything is written** (no orphan files on rejection)
|
|
94
|
+
and is an exact string match — list each type. When a whitelist is present the library also
|
|
95
|
+
verifies the file's magic-byte signature: a mismatch throws `ContentTypeMismatchError`, and
|
|
96
|
+
on a **closed** whitelist (every accepted type is signature-detectable, e.g.
|
|
97
|
+
`['application/pdf']`) unrecognizable bytes throw `ContentSignatureUnrecognizedError`. An
|
|
98
|
+
**open** whitelist (any type without a signature, e.g. `['application/pdf', 'image/svg+xml']`)
|
|
99
|
+
falls back to the declared type.
|
|
100
|
+
|
|
101
|
+
Source: `docs/collections-and-conversions.mdx`.
|
|
102
|
+
|
|
103
|
+
### Pattern 2 — eager vs lazy conversions
|
|
104
|
+
|
|
105
|
+
`ConversionPreset` maps straight onto sharp. `eager: true` generates synchronously on
|
|
106
|
+
`attach`; the default is lazy — generated on the **first** `url(id, name)` and then cached on
|
|
107
|
+
the record, so every later call is a pure lookup.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const m = await media.library.attach({ ...input, collection: 'gallery' })
|
|
111
|
+
|
|
112
|
+
await media.library.url(m.id, 'og') // eager → already on disk, pure lookup
|
|
113
|
+
await media.library.url(m.id, 'thumb') // lazy → generated NOW, then cached
|
|
114
|
+
await media.library.ensureConversion(m.id, 'thumb') // force it ahead of time (idempotent)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Conversions are written under `conversions/` next to the original, recorded on
|
|
118
|
+
`record.conversions` keyed by preset name, and removed by `delete()` together with the
|
|
119
|
+
original. `format` defaults to `webp`; `fit` defaults to `cover`.
|
|
120
|
+
|
|
121
|
+
Source: `docs/collections-and-conversions.mdx` (Eager vs lazy).
|
|
122
|
+
|
|
123
|
+
### Pattern 3 — bind an owner once with `for()`, adopt existing objects with `attachExisting`
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const gallery = media.library.for('Post', post.id)
|
|
127
|
+
|
|
128
|
+
for (const image of images) {
|
|
129
|
+
await gallery.attach({ collection: 'gallery', fileName: image.clientName, mimeType: image.type!, contents })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// zero-copy adoption of an object ALREADY on the disk (finished TUS / direct upload):
|
|
133
|
+
await gallery.attachExisting({
|
|
134
|
+
collection: 'gallery',
|
|
135
|
+
key: 'uploads/9f2a/clip.mp4',
|
|
136
|
+
fileName: 'clip.mp4',
|
|
137
|
+
mimeType: 'video/mp4',
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`attachExisting` never reads or rewrites bytes; the size comes from a disk HEAD when
|
|
142
|
+
omitted, and the collection's MIME + signature checks still run against a short head read.
|
|
143
|
+
Pass `moveIntoLayout: true` to relocate into the canonical layout using the disk's native
|
|
144
|
+
server-side move.
|
|
145
|
+
|
|
146
|
+
Source: `docs/getting-started.mdx` (Bind an owner once), `docs/uploads/resumable-tus.mdx`.
|
|
147
|
+
|
|
148
|
+
### Pattern 4 — deterministic `id` for retry-safe overwrites
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await media.library.attach({
|
|
152
|
+
ownerType: 'Invoice',
|
|
153
|
+
ownerId: invoice.id,
|
|
154
|
+
collection: 'pdf',
|
|
155
|
+
id: `invoice-${invoice.id}`, // fixed key → retry-safe overwrite, no orphaned UUIDs
|
|
156
|
+
fileName: 'invoice.pdf',
|
|
157
|
+
mimeType: 'application/pdf',
|
|
158
|
+
contents: renderedPdf,
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `id` is used verbatim as the record key and the storage-key segment — you own
|
|
163
|
+
uniqueness. Retries overwrite in place instead of minting a fresh UUID path.
|
|
164
|
+
|
|
165
|
+
Source: `docs/getting-started.mdx` (Idempotent overwrites).
|
|
166
|
+
|
|
167
|
+
## Common mistakes
|
|
168
|
+
|
|
169
|
+
### [CRITICAL] Assuming attach replaces an existing file without single:true
|
|
170
|
+
|
|
171
|
+
Wrong:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// 'avatar' declared WITHOUT single: true — each call appends another file
|
|
175
|
+
await media.library.attach({ ownerType: 'User', ownerId: '1', collection: 'avatar', ... }) // v1
|
|
176
|
+
await media.library.attach({ ownerType: 'User', ownerId: '1', collection: 'avatar', ... }) // v2
|
|
177
|
+
await media.library.list('User', '1', 'avatar') // [v1, v2] — "the avatar" is now a list
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Correct:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// config/media.ts
|
|
184
|
+
defineConfig({ collections: [{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png'] }] })
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Mechanism: replacement is a property of the **collection**, not the call. `single: true`
|
|
188
|
+
deletes the previous file (original + conversions) only AFTER the new one is safely
|
|
189
|
+
persisted, so a failed upload leaves the old media intact; without it ordering is
|
|
190
|
+
append-only and every attach takes the next slot.
|
|
191
|
+
Source: `docs/collections-and-conversions.mdx` (Single-file collections), `docs/single-file.mdx` (warn callout).
|
|
192
|
+
|
|
193
|
+
### [HIGH] Using wildcard MIME types in acceptsMimeTypes
|
|
194
|
+
|
|
195
|
+
Wrong:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
defineConfig({
|
|
199
|
+
collections: [{ name: 'images', acceptsMimeTypes: ['image/*'] }],
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Correct:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
defineConfig({
|
|
207
|
+
collections: [{ name: 'images', acceptsMimeTypes: ['image/png', 'image/jpeg', 'image/webp'] }],
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Mechanism: the whitelist is an exact string match against the declared `mimeType` — there is
|
|
212
|
+
no wildcard expansion, so `'image/*'` matches nothing and every attach throws
|
|
213
|
+
`MimeNotAllowedError` (`E_MEDIA_MIME_NOT_ALLOWED`) before anything is written.
|
|
214
|
+
Source: `docs/collections-and-conversions.mdx` (MIME whitelist).
|
|
215
|
+
|
|
216
|
+
### [HIGH] Requesting conversions with no imageProcessor configured
|
|
217
|
+
|
|
218
|
+
Wrong:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
defineConfig({
|
|
222
|
+
collections: [{ name: 'gallery', conversions: [{ name: 'thumb', width: 200 }] }],
|
|
223
|
+
// no imageProcessor
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Correct:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { defineConfig, processors } from '@adonis-agora/media'
|
|
231
|
+
|
|
232
|
+
defineConfig({
|
|
233
|
+
imageProcessor: processors.sharp(),
|
|
234
|
+
collections: [{ name: 'gallery', conversions: [{ name: 'thumb', width: 200 }] }],
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Mechanism: `imageProcessor` is required only when conversions exist, but asking for one
|
|
239
|
+
without a configured processor throws `ImageProcessorMissingError`
|
|
240
|
+
(`E_MEDIA_IMAGE_PROCESSOR_MISSING`) instead of silently skipping.
|
|
241
|
+
Source: `docs/configuration.mdx` (`imageProcessor`), `docs/errors.mdx`.
|
|
242
|
+
|
|
243
|
+
### [CRITICAL] Reading url() on a transformer conversion expecting lazy generation
|
|
244
|
+
|
|
245
|
+
Wrong:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// 'videos' collection declares transformers.hls()
|
|
249
|
+
const url = await media.library.url(videoId, 'hls') // throws before any transform ran
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Correct:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// run the transform explicitly (usually from a job), then read
|
|
256
|
+
await media.library.transform(videoId, 'hls') // idempotent — retries skip through
|
|
257
|
+
const record = await media.library.find(videoId)
|
|
258
|
+
record!.conversions.hls // { path, prefix, files, meta }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Mechanism: image presets generate lazily on first read, but reads NEVER generate transforms —
|
|
262
|
+
`url()`/`deliver()` throw `TransformNotReadyError` so a request never stalls behind a remux.
|
|
263
|
+
The split is deliberate.
|
|
264
|
+
Source: `docs/transformers.mdx` (Reads never generate a transform).
|
|
265
|
+
|
|
266
|
+
### [HIGH] Declaring conversion presets on a non-image collection
|
|
267
|
+
|
|
268
|
+
Wrong:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
defineConfig({
|
|
272
|
+
collections: [
|
|
273
|
+
{ name: 'videos', acceptsMimeTypes: ['video/mp4'], conversions: [{ name: 'thumb', width: 320 }] },
|
|
274
|
+
],
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Correct:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { defineConfig, transformers } from '@adonis-agora/media'
|
|
282
|
+
|
|
283
|
+
defineConfig({
|
|
284
|
+
collections: [
|
|
285
|
+
{
|
|
286
|
+
name: 'videos',
|
|
287
|
+
acceptsMimeTypes: ['video/mp4'],
|
|
288
|
+
transformers: [transformers.probe({ eager: true })], // non-image derivations
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Mechanism: presets decode the original as an image through sharp; requesting one on a video
|
|
295
|
+
or PDF fails inside sharp. Transformers are the seam for everything beyond images and
|
|
296
|
+
persist into the same `record.conversions` namespace (name collisions throw
|
|
297
|
+
`TransformerConflictError` at boot).
|
|
298
|
+
Source: `docs/collections-and-conversions.mdx` (warn callout), `docs/transformers.mdx`.
|
|
299
|
+
|
|
300
|
+
### [HIGH] Trusting the declared mimeType as the real content type
|
|
301
|
+
|
|
302
|
+
Wrong:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
// client renames payload.png → report.pdf and declares 'application/pdf'
|
|
306
|
+
await media.library.attach({
|
|
307
|
+
ownerType: 'Patient', ownerId: '7', collection: 'exams',
|
|
308
|
+
fileName: 'scan.pdf', mimeType: 'application/pdf', contents, // bytes are PNG
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Correct:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
try {
|
|
316
|
+
await media.library.attach({ ownerType: 'Patient', ownerId: '7', collection: 'exams',
|
|
317
|
+
fileName: 'scan.pdf', mimeType: 'application/pdf', contents })
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error instanceof ContentTypeMismatchError) {
|
|
320
|
+
// error.declaredMimeType vs error.detectedMimeType — reject with a 4xx
|
|
321
|
+
}
|
|
322
|
+
throw error
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Mechanism: with `acceptsMimeTypes` present the library reads the file's magic-byte signature
|
|
327
|
+
(first 189 bytes, replayed for streams) and validates the REAL content — a renamed file is a
|
|
328
|
+
`ContentTypeMismatchError` even though the declared type is whitelisted. Only the head is
|
|
329
|
+
read; the payload stays streaming.
|
|
330
|
+
Source: `docs/collections-and-conversions.mdx` (The content is checked too).
|
|
331
|
+
|
|
332
|
+
See also: `media-attachments-single-file/SKILL.md` — the column-attachment shape for
|
|
333
|
+
single-file fields; `media-uploads-resumable/SKILL.md` — landing large uploads as records
|
|
334
|
+
via `attachExisting`; `media-stores-delivery/SKILL.md` — serving with delivery modes.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: media-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Install and configure @adonis-agora/media in an AdonisJS app. Covers node ace
|
|
5
|
+
configure (media_provider + dashboard_provider registration), config/media.ts via
|
|
6
|
+
defineConfig with stores/processors/disks/uploadSessions factory maps, the
|
|
7
|
+
media singleton from @adonis-agora/media/services/main, disk resolution precedence
|
|
8
|
+
(per-call > collection > top-level disk > Drive default), lazy optional peers
|
|
9
|
+
(@adonisjs/lucid, sharp, @aws-sdk/client-s3, mediabunny), StoreNotConfiguredError,
|
|
10
|
+
DriveNotReadyError, and the embedded console provider. Use when setting up media,
|
|
11
|
+
choosing a MediaStore, wiring disks, or fixing boot errors.
|
|
12
|
+
license: MIT
|
|
13
|
+
metadata:
|
|
14
|
+
type: core
|
|
15
|
+
library: "@adonis-agora/media"
|
|
16
|
+
library_version: "0.12.1"
|
|
17
|
+
framework: adonisjs
|
|
18
|
+
sources:
|
|
19
|
+
- "DavideCarvalho/adonis-media:README.md"
|
|
20
|
+
- "DavideCarvalho/adonis-media:docs/getting-started.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-media:docs/configuration.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/define_config.ts"
|
|
23
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/media_manager.ts"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Setting up @adonis-agora/media
|
|
27
|
+
|
|
28
|
+
`@adonis-agora/media` is a media library for AdonisJS on top of `@adonisjs/drive`: owner
|
|
29
|
+
collections with policy, column attachments, image conversions, content transformers,
|
|
30
|
+
large-file uploads, configurable delivery, and an embedded management console. Storage is
|
|
31
|
+
delegated entirely to Drive — this package never reimplements disk drivers.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Install Drive (the storage backend) plus the package, then run the configure codemod:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node ace add @adonisjs/drive # required — media owns no disk drivers
|
|
39
|
+
npm i @adonis-agora/media
|
|
40
|
+
node ace configure @adonis-agora/media
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`configure` registers **both** providers in `adonisrc.ts`, publishes `config/media.ts`
|
|
44
|
+
and `config/media_dashboard.ts`, and publishes two Lucid migrations (one for the optional
|
|
45
|
+
`lucid` media store, one for the upload-session store). Delete the migrations you don't
|
|
46
|
+
need; the in-memory stores need none.
|
|
47
|
+
|
|
48
|
+
```ts title="config/media.ts"
|
|
49
|
+
import { defineConfig, stores, processors } from '@adonis-agora/media'
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
// disk: 's3', // omit to use Drive's default disk
|
|
53
|
+
store: 'memory',
|
|
54
|
+
stores: {
|
|
55
|
+
memory: stores.memory(),
|
|
56
|
+
lucid: stores.lucid({ connection: 'pg' }),
|
|
57
|
+
},
|
|
58
|
+
imageProcessor: processors.sharp(),
|
|
59
|
+
collections: [
|
|
60
|
+
{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
|
|
61
|
+
{
|
|
62
|
+
name: 'gallery',
|
|
63
|
+
conversions: [
|
|
64
|
+
{ name: 'thumb', width: 200, height: 200 },
|
|
65
|
+
{ name: 'og', width: 1200, eager: true },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Reach the singleton manager anywhere:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import media from '@adonis-agora/media/services/main'
|
|
76
|
+
|
|
77
|
+
const url = await media.library.url(recordId)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Source: `docs/getting-started.mdx`, `docs/configuration.mdx`.
|
|
81
|
+
|
|
82
|
+
## Core patterns
|
|
83
|
+
|
|
84
|
+
### Pattern 1 — select drivers by name from lazy factory maps
|
|
85
|
+
|
|
86
|
+
The Agora idiom: drivers live in the core package (`stores`, `processors`, `disks`,
|
|
87
|
+
`uploadSessions`, `transformers`), you select them **by name**, and each heavy optional peer
|
|
88
|
+
(`@adonisjs/lucid`, `sharp`, `@aws-sdk/client-s3`, `mediabunny`) is imported only when its
|
|
89
|
+
driver is selected at boot.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { defineConfig, stores, processors, disks } from '@adonis-agora/media'
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
store: 'lucid',
|
|
96
|
+
stores: { lucid: stores.lucid({ connection: 'pg' }) },
|
|
97
|
+
imageProcessor: processors.sharp(),
|
|
98
|
+
disks: { s3: disks.s3({ bucket: 'my-bucket', region: 'us-east-1' }) },
|
|
99
|
+
disk: 's3',
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Listing a driver you never select costs nothing — the thunk imports nothing until built.
|
|
104
|
+
An empty `defineConfig({})` gives the in-memory store on Drive's default disk, no
|
|
105
|
+
conversions, no upload routes, `auto` delivery, diagnostics on.
|
|
106
|
+
|
|
107
|
+
Source: `docs/configuration.mdx`.
|
|
108
|
+
|
|
109
|
+
### Pattern 2 — disk precedence
|
|
110
|
+
|
|
111
|
+
Which disk a write targets resolves per-call first, then collection, then top-level, then
|
|
112
|
+
Drive's default:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
defineConfig({
|
|
116
|
+
disk: 'public-s3', // 3rd choice
|
|
117
|
+
collections: [
|
|
118
|
+
{ name: 'invoices', disk: 'private-s3' }, // 2nd choice
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// per-call wins over everything:
|
|
123
|
+
await media.library.attach({ ..., disk: 'temp-fs' }) // 1st choice
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Where the NAME resolves from has separate precedence: the config's own `disks` map first,
|
|
127
|
+
then `config/drive.ts`. A record stores which disk it lives on, so moving a collection to a
|
|
128
|
+
new disk never strands existing files.
|
|
129
|
+
|
|
130
|
+
Source: `docs/configuration.mdx` (`disk` — where files live).
|
|
131
|
+
|
|
132
|
+
### Pattern 3 — DI instead of the service import
|
|
133
|
+
|
|
134
|
+
The provider binds one singleton `MediaManager`. Prefer it through the container when you
|
|
135
|
+
want testability:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { MediaManager } from '@adonis-agora/media'
|
|
139
|
+
|
|
140
|
+
export default class PostImagesController {
|
|
141
|
+
constructor(protected media: MediaManager) {}
|
|
142
|
+
}
|
|
143
|
+
// or: const media = await app.container.make(MediaManager)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Accessors: `media.library` (collections), `media.attachments` (column attachments),
|
|
147
|
+
`media.disk(name?)` (raw Drive disk escape hatch), `media.store` (cross-owner reads),
|
|
148
|
+
and the opt-in coordinators `media.uploads` / `media.resumable` / `media.direct`.
|
|
149
|
+
|
|
150
|
+
Source: `docs/getting-started.mdx` (Step 2), `packages/adonis/src/media_manager.ts`.
|
|
151
|
+
|
|
152
|
+
## Common mistakes
|
|
153
|
+
|
|
154
|
+
### [CRITICAL] Calling the media singleton at module scope before Drive boots
|
|
155
|
+
|
|
156
|
+
Wrong:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// module scope — runs before the app (and Drive) has booted
|
|
160
|
+
import media from '@adonis-agora/media/services/main'
|
|
161
|
+
export const DEFAULT_URL = await media.library.url('seed-image')
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Correct:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import media from '@adonis-agora/media/services/main'
|
|
168
|
+
|
|
169
|
+
export default class LogoController {
|
|
170
|
+
async show() {
|
|
171
|
+
const url = await media.library.url('seed-image') // inside request lifecycle
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Mechanism: a disk resolution reaching `@adonisjs/drive` before its manager has booted throws
|
|
177
|
+
`DriveNotReadyError` (`E_MEDIA_DRIVE_NOT_READY`) — almost always a media call at module scope
|
|
178
|
+
instead of inside a request or `app.booted`.
|
|
179
|
+
Source: `docs/errors.mdx` (Storage and configuration).
|
|
180
|
+
|
|
181
|
+
### [HIGH] Naming a `store` that has no matching factory entry
|
|
182
|
+
|
|
183
|
+
Wrong:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
defineConfig({
|
|
187
|
+
store: 'luicd', // typo
|
|
188
|
+
stores: { lucid: stores.lucid() },
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Correct:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
defineConfig({
|
|
196
|
+
store: 'lucid',
|
|
197
|
+
stores: { lucid: stores.lucid() },
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Mechanism: the provider throws `StoreNotConfiguredError` at boot — deliberately NOT a silent
|
|
202
|
+
fallback to in-memory, because a typo quietly swapping durable persistence for a process-local
|
|
203
|
+
Map is discovered only after data loss. Only the zero-config path (no `store` named at all)
|
|
204
|
+
resolves to in-memory. Same rule for upload-session stores.
|
|
205
|
+
Source: `docs/configuration.mdx` (How the provider builds it), `docs/errors.mdx`.
|
|
206
|
+
|
|
207
|
+
### [HIGH] Selecting lucid/sharp without installing the peer or running the migration
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
npm i @adonis-agora/media # no @adonisjs/lucid, no sharp, no migration:run
|
|
213
|
+
# config selects store: 'lucid' + conversions → fails at boot / first attach
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Correct:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
node ace add @adonisjs/lucid # or ensure it exists
|
|
220
|
+
npm i sharp
|
|
221
|
+
node ace migration:run # published by configure for the lucid stores
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Mechanism: the factory thunks import their peers lazily at boot-time selection; selecting
|
|
225
|
+
`lucid` without the published `media` table fails on the first store write, not at
|
|
226
|
+
`configure` time.
|
|
227
|
+
Source: `docs/getting-started.mdx` (Prerequisites, Callout on migrations).
|
|
228
|
+
|
|
229
|
+
### [MEDIUM] Importing the dashboard's defineConfig into config/media.ts
|
|
230
|
+
|
|
231
|
+
Wrong:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// config/media.ts
|
|
235
|
+
import { defineConfig } from '@adonis-agora/media/dashboard' // wrong file's helper
|
|
236
|
+
export default defineConfig({ collections: [{ name: 'gallery' }] })
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Correct:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// config/media.ts
|
|
243
|
+
import { defineConfig } from '@adonis-agora/media'
|
|
244
|
+
// config/media_dashboard.ts uses:
|
|
245
|
+
// import { defineConfig } from '@adonis-agora/media/dashboard'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Mechanism: both packages export a `defineConfig`; the wrong one types the wrong file and
|
|
249
|
+
silently drops unknown keys instead of failing.
|
|
250
|
+
Source: README.md (subpath exports table), `docs/dashboard/index.mdx`.
|
|
251
|
+
|
|
252
|
+
See also: `media-library-collections/SKILL.md` — attaching to collections after setup;
|
|
253
|
+
`media-stores-delivery/SKILL.md` — writing custom stores/processors/disks.
|