@plutocms/supabase 0.16.0 → 0.17.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
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.0](https://github.com/plutocms/supabase/compare/v0.16.0...v0.17.0) (2026-10-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* media previews, sort and rename in the Supabase driver ([#139](https://github.com/plutocms/supabase/issues/139)) ([8cdf071](https://github.com/plutocms/supabase/commit/8cdf0715c5a11db8e99423751708d2ab0feea455))
|
|
9
|
+
|
|
3
10
|
## [0.16.0](https://github.com/plutocms/supabase/compare/v0.15.5...v0.16.0) (2026-10-30)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- The preview image of a file that is not an image (a 3D model), made in the
|
|
2
|
+
-- browser by the kind of the file. Always in the `media` bucket, even for an
|
|
3
|
+
-- older file in another bucket. See the media skill of @plutocms/pluto.
|
|
4
|
+
|
|
5
|
+
alter table public.media add column if not exists preview_path text null;
|
|
6
|
+
alter table public.media add column if not exists preview_width integer null;
|
|
7
|
+
alter table public.media add column if not exists preview_height integer null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"trustedDependencies": [
|
|
6
6
|
"@parcel/watcher",
|
|
7
7
|
"@plutocms/pluto",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@nuxt/eslint": "^1.17.0",
|
|
40
40
|
"@nuxt/ui": "^4.12.0",
|
|
41
41
|
"@nuxtjs/supabase": "^2.1.0",
|
|
42
|
-
"@plutocms/pluto": ">=0.
|
|
42
|
+
"@plutocms/pluto": ">=0.36.0 <1.0.0",
|
|
43
43
|
"@plutocms/utils": ">=0.3.0 <1.0.0",
|
|
44
44
|
"@supabase/supabase-js": "^2.117.3",
|
|
45
45
|
"postgres": "^3.4.9",
|
|
@@ -17,6 +17,23 @@ interface MediaRow {
|
|
|
17
17
|
size: number | null
|
|
18
18
|
width: number | null
|
|
19
19
|
height: number | null
|
|
20
|
+
preview_path: string | null
|
|
21
|
+
preview_width: number | null
|
|
22
|
+
preview_height: number | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `<yyyy>/<mm>/<name>`: the object key of a file, by the month of its record. */
|
|
26
|
+
function monthKey(createdAt: string, fileName: string): string {
|
|
27
|
+
const date = new Date(createdAt)
|
|
28
|
+
return `${date.getUTCFullYear()}/${String(date.getUTCMonth() + 1).padStart(2, '0')}/${fileName}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The order of a list. The id breaks a tie, so pages stay stable. */
|
|
32
|
+
const ORDER: Record<PlutoMediaSort, { column: string; ascending: boolean }> = {
|
|
33
|
+
newest: { column: 'created_at', ascending: false },
|
|
34
|
+
oldest: { column: 'created_at', ascending: true },
|
|
35
|
+
name: { column: 'name', ascending: true },
|
|
36
|
+
size: { column: 'size', ascending: false },
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
function toRecord(client: SupabaseClient, row: MediaRow): PlutoMedia {
|
|
@@ -34,6 +51,16 @@ function toRecord(client: SupabaseClient, row: MediaRow): PlutoMedia {
|
|
|
34
51
|
height: row.height,
|
|
35
52
|
alt: row.alt ?? '',
|
|
36
53
|
createdAt: row.created_at,
|
|
54
|
+
// A preview is always in the `media` bucket.
|
|
55
|
+
preview:
|
|
56
|
+
row.preview_path && row.preview_width && row.preview_height
|
|
57
|
+
? {
|
|
58
|
+
url: client.storage.from(MEDIA_BUCKET).getPublicUrl(row.preview_path)
|
|
59
|
+
.data.publicUrl,
|
|
60
|
+
width: row.preview_width,
|
|
61
|
+
height: row.preview_height,
|
|
62
|
+
}
|
|
63
|
+
: null,
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
|
|
@@ -58,7 +85,8 @@ function numericIds(ids: string[]): number[] {
|
|
|
58
85
|
* The media driver of the Supabase backend: the bytes in Supabase Storage,
|
|
59
86
|
* the record in `public.media` (db/migrations/009_media.sql). Row Level
|
|
60
87
|
* Security checks the capabilities again (`has_capability`). New files go to
|
|
61
|
-
* `media/<yyyy>/<mm>/<name>-<suffix>.<ext
|
|
88
|
+
* `media/<yyyy>/<mm>/<name>-<suffix>.<ext>`, and their previews next to them
|
|
89
|
+
* (`db/migrations/010_media_preview.sql`). See the media skill of
|
|
62
90
|
* `@plutocms/pluto`.
|
|
63
91
|
*/
|
|
64
92
|
export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
@@ -90,11 +118,12 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
90
118
|
|
|
91
119
|
async list(event, query) {
|
|
92
120
|
const client = await supabaseRequestClient(event)
|
|
121
|
+
const order = ORDER[query.sort ?? 'newest']
|
|
93
122
|
let request = client
|
|
94
123
|
.from('media')
|
|
95
124
|
.select('*', { count: 'exact' })
|
|
96
|
-
.order(
|
|
97
|
-
.order('id', { ascending:
|
|
125
|
+
.order(order.column, { ascending: order.ascending, nullsFirst: false })
|
|
126
|
+
.order('id', { ascending: order.ascending })
|
|
98
127
|
.range(query.offset, query.offset + query.limit - 1)
|
|
99
128
|
if (query.search) {
|
|
100
129
|
// Commas and brackets have a meaning in a PostgREST `or` filter.
|
|
@@ -135,8 +164,10 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
135
164
|
|
|
136
165
|
async upload(event, input) {
|
|
137
166
|
const client = await supabaseRequestClient(event)
|
|
138
|
-
const
|
|
139
|
-
const
|
|
167
|
+
const path = monthKey(input.createdAt, input.fileName)
|
|
168
|
+
const previewPath = input.preview
|
|
169
|
+
? monthKey(input.createdAt, input.preview.fileName)
|
|
170
|
+
: null
|
|
140
171
|
const storage = client.storage.from(MEDIA_BUCKET)
|
|
141
172
|
const { error: uploadError } = await storage.upload(path, input.data, {
|
|
142
173
|
contentType: input.mimeType,
|
|
@@ -146,6 +177,17 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
146
177
|
if (uploadError) {
|
|
147
178
|
throw failed(uploadError.message, 'pluto.media.errors.saveFailed')
|
|
148
179
|
}
|
|
180
|
+
if (input.preview && previewPath) {
|
|
181
|
+
const { error: previewError } = await storage.upload(
|
|
182
|
+
previewPath,
|
|
183
|
+
input.preview.data,
|
|
184
|
+
{ contentType: input.preview.mimeType, cacheControl: '31536000', upsert: true }
|
|
185
|
+
)
|
|
186
|
+
if (previewError) {
|
|
187
|
+
await storage.remove([path])
|
|
188
|
+
throw failed(previewError.message, 'pluto.media.errors.saveFailed')
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
const { data, error } = await client
|
|
150
192
|
.from('media')
|
|
151
193
|
.insert({
|
|
@@ -158,12 +200,17 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
158
200
|
width: input.width,
|
|
159
201
|
height: input.height,
|
|
160
202
|
created_at: input.createdAt,
|
|
203
|
+
preview_path: previewPath,
|
|
204
|
+
preview_width: input.preview?.width ?? null,
|
|
205
|
+
preview_height: input.preview?.height ?? null,
|
|
161
206
|
})
|
|
162
207
|
.select('*')
|
|
163
208
|
.single()
|
|
164
209
|
if (error) {
|
|
165
210
|
// No record without its file, and no file without its record.
|
|
166
|
-
const { error: cleanup } = await storage.remove(
|
|
211
|
+
const { error: cleanup } = await storage.remove(
|
|
212
|
+
previewPath ? [path, previewPath] : [path]
|
|
213
|
+
)
|
|
167
214
|
if (cleanup) {
|
|
168
215
|
console.warn('[media] could not remove the file of a failed upload:', path, cleanup.message)
|
|
169
216
|
}
|
|
@@ -176,6 +223,9 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
176
223
|
const client = await supabaseRequestClient(event)
|
|
177
224
|
const row = await readRow(client, id)
|
|
178
225
|
const changes: Partial<MediaRow> = {}
|
|
226
|
+
if (patch.name !== undefined) {
|
|
227
|
+
changes.name = patch.name
|
|
228
|
+
}
|
|
179
229
|
if (patch.alt !== undefined) {
|
|
180
230
|
changes.alt = patch.alt || null
|
|
181
231
|
}
|
|
@@ -197,6 +247,39 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
197
247
|
return toRecord(client, data as MediaRow)
|
|
198
248
|
},
|
|
199
249
|
|
|
250
|
+
async setPreview(event, id, preview) {
|
|
251
|
+
const client = await supabaseRequestClient(event)
|
|
252
|
+
const row = await readRow(client, id)
|
|
253
|
+
const storage = client.storage.from(MEDIA_BUCKET)
|
|
254
|
+
const path = monthKey(row.created_at, preview.fileName)
|
|
255
|
+
const { error: uploadError } = await storage.upload(path, preview.data, {
|
|
256
|
+
contentType: preview.mimeType,
|
|
257
|
+
cacheControl: '31536000',
|
|
258
|
+
upsert: true,
|
|
259
|
+
})
|
|
260
|
+
if (uploadError) {
|
|
261
|
+
throw failed(uploadError.message, 'pluto.media.errors.saveFailed')
|
|
262
|
+
}
|
|
263
|
+
const { data, error } = await client
|
|
264
|
+
.from('media')
|
|
265
|
+
.update({
|
|
266
|
+
preview_path: path,
|
|
267
|
+
preview_width: preview.width,
|
|
268
|
+
preview_height: preview.height,
|
|
269
|
+
})
|
|
270
|
+
.eq('id', row.id)
|
|
271
|
+
.select('*')
|
|
272
|
+
.single()
|
|
273
|
+
if (error) {
|
|
274
|
+
throw failed(error.message, 'pluto.media.errors.saveFailed')
|
|
275
|
+
}
|
|
276
|
+
// An older preview with another name (another type) goes.
|
|
277
|
+
if (row.preview_path && row.preview_path !== path) {
|
|
278
|
+
await storage.remove([row.preview_path])
|
|
279
|
+
}
|
|
280
|
+
return toRecord(client, data as MediaRow)
|
|
281
|
+
},
|
|
282
|
+
|
|
200
283
|
async remove(event, id) {
|
|
201
284
|
const client = await supabaseRequestClient(event)
|
|
202
285
|
const row = await readRow(client, id)
|
|
@@ -209,6 +292,9 @@ export function createSupabaseMediaDriver(): PlutoMediaDriver {
|
|
|
209
292
|
throw failed(error.message, 'pluto.media.errors.deleteFailed')
|
|
210
293
|
}
|
|
211
294
|
}
|
|
295
|
+
if (row.preview_path) {
|
|
296
|
+
await client.storage.from(MEDIA_BUCKET).remove([row.preview_path])
|
|
297
|
+
}
|
|
212
298
|
const { error } = await client.from('media').delete().eq('id', row.id)
|
|
213
299
|
if (error) {
|
|
214
300
|
throw failed(error.message, 'pluto.media.errors.deleteFailed')
|