includio-cms 0.36.7 → 0.36.9
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/API.md +9 -2
- package/CHANGELOG.md +29 -0
- package/DOCS.md +1 -1
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/media-jobs.d.ts +5 -0
- package/dist/admin/api/media-jobs.js +33 -0
- package/dist/admin/client/collection/collection-entries.svelte +20 -1
- package/dist/admin/client/collection/row-actions.svelte +12 -4
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +9 -0
- package/dist/admin/client/maintenance/media-jobs-panel.svelte +310 -0
- package/dist/admin/client/maintenance/media-jobs-panel.svelte.d.ts +3 -0
- package/dist/admin/remote/entry.remote.d.ts +3 -0
- package/dist/admin/remote/entry.remote.js +14 -3
- package/dist/core/cms.js +7 -0
- package/dist/core/fields/dotPath.d.ts +4 -0
- package/dist/core/fields/dotPath.js +18 -0
- package/dist/core/fields/slugify.d.ts +3 -0
- package/dist/core/fields/slugify.js +4 -0
- package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
- package/dist/core/server/entries/operations/duplicate.js +73 -0
- package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
- package/dist/core/server/entries/operations/slugNormalize.js +20 -0
- package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
- package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
- package/dist/core/server/fields/utils/imageStyles.js +41 -4
- package/dist/core/server/index.d.ts +3 -0
- package/dist/core/server/index.js +3 -0
- package/dist/core/server/media/jobs/enqueue.d.ts +6 -0
- package/dist/core/server/media/jobs/enqueue.js +20 -0
- package/dist/core/server/media/jobs/processImageStyleJob.d.ts +19 -0
- package/dist/core/server/media/jobs/processImageStyleJob.js +23 -0
- package/dist/core/server/media/jobs/startWorker.d.ts +2 -0
- package/dist/core/server/media/jobs/startWorker.js +29 -0
- package/dist/core/server/media/jobs/worker.d.ts +29 -0
- package/dist/core/server/media/jobs/worker.js +54 -0
- package/dist/core/server/media/operations/replaceFile.js +2 -2
- package/dist/core/server/media/operations/uploadFile.js +10 -3
- package/dist/core/server/media/styles/buildStyleSpecs.d.ts +12 -0
- package/dist/core/server/media/styles/buildStyleSpecs.js +36 -0
- package/dist/core/server/media/styles/imagesConfig.d.ts +3 -0
- package/dist/core/server/media/styles/imagesConfig.js +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +19 -41
- package/dist/core/server/media/styles/operations/generateFallbackVariant.d.ts +10 -0
- package/dist/core/server/media/styles/operations/generateFallbackVariant.js +21 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +12 -8
- package/dist/core/server/media/styles/sharp/sharpConfig.d.ts +7 -0
- package/dist/core/server/media/styles/sharp/sharpConfig.js +12 -0
- package/dist/db-postgres/index.js +113 -1
- package/dist/db-postgres/schema/imageStyle.js +1 -1
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema/mediaFile.d.ts +17 -0
- package/dist/db-postgres/schema/mediaFile.js +1 -0
- package/dist/db-postgres/schema/mediaJob.d.ts +279 -0
- package/dist/db-postgres/schema/mediaJob.js +23 -0
- package/dist/sveltekit/components/image.svelte +1 -1
- package/dist/types/adapters/db.d.ts +51 -0
- package/dist/types/cms.d.ts +46 -0
- package/dist/types/media.d.ts +1 -0
- package/dist/updates/0.36.8/index.d.ts +2 -0
- package/dist/updates/0.36.8/index.js +20 -0
- package/dist/updates/0.36.9/index.d.ts +2 -0
- package/dist/updates/0.36.9/index.js +12 -0
- package/dist/updates/index.js +5 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCMS } from '../../../../cms.js';
|
|
2
|
-
import { isProcessableImage
|
|
3
|
-
import {
|
|
2
|
+
import { isProcessableImage } from '../../../fields/utils/imageStyles.js';
|
|
3
|
+
import { buildStyleSpecs, styleDedupeKey } from '../buildStyleSpecs.js';
|
|
4
|
+
import { resolveImagesConfig } from '../imagesConfig.js';
|
|
4
5
|
import { generateAndSaveAdminThumbnail } from '../../utils/generateAdminThumbnail.js';
|
|
5
6
|
const IMAGE_MIME_TYPES = [
|
|
6
7
|
'image/jpeg',
|
|
@@ -12,39 +13,26 @@ const IMAGE_MIME_TYPES = [
|
|
|
12
13
|
];
|
|
13
14
|
async function generateStylesForFile(file) {
|
|
14
15
|
const cms = getCMS();
|
|
15
|
-
const
|
|
16
|
-
const expanded = expandStyleFormats(defaultStyles, origFormat);
|
|
16
|
+
const specs = buildStyleSpecs(file, undefined, resolveImagesConfig(cms.mediaConfig.images));
|
|
17
17
|
let created = 0;
|
|
18
18
|
let skipped = 0;
|
|
19
|
-
|
|
19
|
+
// Enqueue brakujących wariantów (worker w tle je wygeneruje) — sweep NIE generuje inline.
|
|
20
|
+
for (const style of specs) {
|
|
20
21
|
const existing = await cms.databaseAdapter.getImageStyle(file.id, style);
|
|
21
22
|
if (existing) {
|
|
22
23
|
skipped++;
|
|
24
|
+
continue;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
width: w,
|
|
35
|
-
srcset: undefined,
|
|
36
|
-
sizes: undefined
|
|
37
|
-
};
|
|
38
|
-
const existingW = await cms.databaseAdapter.getImageStyle(file.id, widthStyle);
|
|
39
|
-
if (existingW) {
|
|
40
|
-
skipped++;
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
await getImageStyle(file.id, widthStyle);
|
|
44
|
-
created++;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
26
|
+
await cms.databaseAdapter
|
|
27
|
+
.enqueueMediaJob({
|
|
28
|
+
type: 'image_style',
|
|
29
|
+
mediaFileId: file.id,
|
|
30
|
+
payload: JSON.stringify(style),
|
|
31
|
+
dedupeKey: styleDedupeKey(file.id, style),
|
|
32
|
+
priority: 0
|
|
33
|
+
})
|
|
34
|
+
.catch((e) => console.warn('[media] sweep enqueue failed:', e));
|
|
35
|
+
created++;
|
|
48
36
|
}
|
|
49
37
|
// Generate admin thumbnail if missing
|
|
50
38
|
if (!file.thumbnailUrl) {
|
|
@@ -99,18 +87,8 @@ export async function* batchGenerateAllStyles(signal) {
|
|
|
99
87
|
yield { type: 'done', total, processed: total, created: totalCreated, skipped: totalSkipped };
|
|
100
88
|
}
|
|
101
89
|
function getExpectedStyleNames(file) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const names = [];
|
|
105
|
-
for (const style of expanded) {
|
|
106
|
-
names.push(style.name);
|
|
107
|
-
if (style.srcset && file.width) {
|
|
108
|
-
for (const w of style.srcset.filter((sw) => sw <= file.width)) {
|
|
109
|
-
names.push(`${style.name}_${w}w`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return names;
|
|
90
|
+
const specs = buildStyleSpecs(file, undefined, resolveImagesConfig(getCMS().mediaConfig.images));
|
|
91
|
+
return specs.map((s) => s.name);
|
|
114
92
|
}
|
|
115
93
|
export async function getStylesStatus() {
|
|
116
94
|
const cms = getCMS();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MediaFile } from '../../../../../types/media.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lekki, uniwersalny wariant (domyślnie jpeg ~1280) generowany synchronicznie przy uploadzie.
|
|
4
|
+
* Serwowany jako `<img src>` dopóki pełne warianty są pending — użytkownik nigdy nie dostaje
|
|
5
|
+
* ciężkiego oryginału. Tania operacja (mała szerokość, jpeg/webp), z timeoutem.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateFallbackVariant(buffer: Buffer, mediaFile: Pick<MediaFile, 'id'>, opts: {
|
|
8
|
+
width: number;
|
|
9
|
+
format: 'jpeg' | 'webp';
|
|
10
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getCMS } from '../../../../cms.js';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { withTimeout, sharpTimeoutMs } from '../../../../../server/utils/withTimeout.js';
|
|
4
|
+
const FALLBACK_QUALITY = 72;
|
|
5
|
+
/**
|
|
6
|
+
* Lekki, uniwersalny wariant (domyślnie jpeg ~1280) generowany synchronicznie przy uploadzie.
|
|
7
|
+
* Serwowany jako `<img src>` dopóki pełne warianty są pending — użytkownik nigdy nie dostaje
|
|
8
|
+
* ciężkiego oryginału. Tania operacja (mała szerokość, jpeg/webp), z timeoutem.
|
|
9
|
+
*/
|
|
10
|
+
export async function generateFallbackVariant(buffer, mediaFile, opts) {
|
|
11
|
+
const output = await withTimeout(sharp(buffer)
|
|
12
|
+
.rotate()
|
|
13
|
+
.resize(opts.width, undefined, { withoutEnlargement: true })
|
|
14
|
+
.toFormat(opts.format, { quality: FALLBACK_QUALITY })
|
|
15
|
+
.toBuffer(), sharpTimeoutMs(), 'sharp.fallback');
|
|
16
|
+
const ext = opts.format === 'jpeg' ? 'jpg' : opts.format;
|
|
17
|
+
const mime = opts.format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|
18
|
+
const filename = `${mediaFile.id}_fallback_${Date.now().toString(36)}.${ext}`;
|
|
19
|
+
const uploaded = await getCMS().filesAdapter.uploadFile(new File([new Uint8Array(output)], filename, { type: mime }));
|
|
20
|
+
return uploaded.url;
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCMS } from '../../../../cms.js';
|
|
2
2
|
import { imageStyleKey } from '../../../../../types/adapters/db.js';
|
|
3
3
|
import { createImageStyle } from './createMediaStyle.js';
|
|
4
|
+
import { styleDedupeKey } from '../buildStyleSpecs.js';
|
|
4
5
|
export async function getImageStyle(mediaFileId, style) {
|
|
5
6
|
const imageStyle = await getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
|
|
6
7
|
if (imageStyle)
|
|
@@ -22,14 +23,17 @@ export async function getImageStylesBatch(requests) {
|
|
|
22
23
|
const cache = await cms.databaseAdapter.getImageStylesBatch(requests);
|
|
23
24
|
const missing = requests.filter((r) => !cache.has(imageStyleKey(r.mediaFileId, r.style)));
|
|
24
25
|
for (const { mediaFileId, style } of missing) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
// Render NIE generuje sharpem — kolejkujemy brakujący styl (priority=high) i zwracamy
|
|
27
|
+
// tylko istniejące. Worker w tle dogeneruje; frontend serwuje fallback w międzyczasie.
|
|
28
|
+
await cms.databaseAdapter
|
|
29
|
+
.enqueueMediaJob({
|
|
30
|
+
type: 'image_style',
|
|
31
|
+
mediaFileId,
|
|
32
|
+
payload: JSON.stringify(style),
|
|
33
|
+
dedupeKey: styleDedupeKey(mediaFileId, style),
|
|
34
|
+
priority: 10
|
|
35
|
+
})
|
|
36
|
+
.catch((e) => console.warn(`[media] enqueue failed for style "${style.name}" media ${mediaFileId}:`, e));
|
|
33
37
|
}
|
|
34
38
|
return cache;
|
|
35
39
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Globalne limity sharp — wołane raz przy init CMS. Chroni proces przed wysyceniem CPU/RAM. */
|
|
2
|
+
export declare function applySharpLimits(opts: {
|
|
3
|
+
concurrency: number;
|
|
4
|
+
cacheMemoryMB: number;
|
|
5
|
+
}): void;
|
|
6
|
+
/** Poison-guard: odrzuca „image bomby" zanim sharp spróbuje je przetworzyć. */
|
|
7
|
+
export declare function assertWithinPixelLimit(width: number, height: number, limitPixels: number): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/** Globalne limity sharp — wołane raz przy init CMS. Chroni proces przed wysyceniem CPU/RAM. */
|
|
3
|
+
export function applySharpLimits(opts) {
|
|
4
|
+
sharp.concurrency(opts.concurrency);
|
|
5
|
+
sharp.cache({ memory: opts.cacheMemoryMB, files: 0 });
|
|
6
|
+
}
|
|
7
|
+
/** Poison-guard: odrzuca „image bomby" zanim sharp spróbuje je przetworzyć. */
|
|
8
|
+
export function assertWithinPixelLimit(width, height, limitPixels) {
|
|
9
|
+
if (width * height > limitPixels) {
|
|
10
|
+
throw new Error(`image exceeds pixel limit (${width}x${height} > ${limitPixels}px)`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -688,7 +688,7 @@ export function pg(config) {
|
|
|
688
688
|
const rows = await db.execute(sql `
|
|
689
689
|
INSERT INTO image_styles (id, media_file_id, name, url, width, height, crop, quality, media, mime_type, aspect_ratio)
|
|
690
690
|
VALUES (gen_random_uuid(), ${mediaFileId}, ${style.name}, ${file.url}, ${style.width ?? null}, ${style.height ?? null}, ${style.crop ?? false}, ${style.quality ?? null}, ${style.media ?? null}, ${mimeType}, ${style.aspectRatio ?? null})
|
|
691
|
-
ON CONFLICT (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0), COALESCE(aspect_ratio, 0))
|
|
691
|
+
ON CONFLICT (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0), COALESCE(aspect_ratio, 0), crop)
|
|
692
692
|
DO UPDATE SET url = EXCLUDED.url, mime_type = EXCLUDED.mime_type
|
|
693
693
|
RETURNING url, mime_type, media
|
|
694
694
|
`);
|
|
@@ -699,6 +699,118 @@ export function pg(config) {
|
|
|
699
699
|
media: row.media ? row.media : undefined
|
|
700
700
|
};
|
|
701
701
|
},
|
|
702
|
+
enqueueMediaJob: async (input) => {
|
|
703
|
+
await db
|
|
704
|
+
.insert(schema.mediaJobTable)
|
|
705
|
+
.values({
|
|
706
|
+
type: input.type,
|
|
707
|
+
mediaFileId: input.mediaFileId,
|
|
708
|
+
payload: input.payload,
|
|
709
|
+
dedupeKey: input.dedupeKey,
|
|
710
|
+
priority: input.priority ?? 0,
|
|
711
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
712
|
+
})
|
|
713
|
+
.onConflictDoNothing({ target: schema.mediaJobTable.dedupeKey });
|
|
714
|
+
},
|
|
715
|
+
claimNextMediaJob: async (lockedBy) => {
|
|
716
|
+
return db.transaction(async (tx) => {
|
|
717
|
+
const rows = await tx.execute(sql `
|
|
718
|
+
SELECT id FROM media_job
|
|
719
|
+
WHERE status = 'pending' AND run_after <= now()
|
|
720
|
+
ORDER BY priority DESC, run_after ASC
|
|
721
|
+
LIMIT 1 FOR UPDATE SKIP LOCKED
|
|
722
|
+
`);
|
|
723
|
+
const id = rows[0]?.id;
|
|
724
|
+
if (!id)
|
|
725
|
+
return null;
|
|
726
|
+
const [job] = await tx
|
|
727
|
+
.update(schema.mediaJobTable)
|
|
728
|
+
.set({ status: 'processing', lockedAt: new Date(), lockedBy, updatedAt: new Date() })
|
|
729
|
+
.where(eq(schema.mediaJobTable.id, id))
|
|
730
|
+
.returning();
|
|
731
|
+
return job ?? null;
|
|
732
|
+
});
|
|
733
|
+
},
|
|
734
|
+
completeMediaJob: async (id) => {
|
|
735
|
+
await db
|
|
736
|
+
.update(schema.mediaJobTable)
|
|
737
|
+
.set({
|
|
738
|
+
status: 'done',
|
|
739
|
+
completedAt: new Date(),
|
|
740
|
+
updatedAt: new Date(),
|
|
741
|
+
lockedAt: null,
|
|
742
|
+
lockedBy: null
|
|
743
|
+
})
|
|
744
|
+
.where(eq(schema.mediaJobTable.id, id));
|
|
745
|
+
},
|
|
746
|
+
failMediaJob: async (id, error, opts) => {
|
|
747
|
+
await db
|
|
748
|
+
.update(schema.mediaJobTable)
|
|
749
|
+
.set({
|
|
750
|
+
status: opts.retry ? 'pending' : 'failed',
|
|
751
|
+
attempts: sql `${schema.mediaJobTable.attempts} + 1`,
|
|
752
|
+
lastError: error,
|
|
753
|
+
runAfter: opts.runAfter ?? new Date(),
|
|
754
|
+
lockedAt: null,
|
|
755
|
+
lockedBy: null,
|
|
756
|
+
updatedAt: new Date(),
|
|
757
|
+
completedAt: opts.retry ? null : new Date()
|
|
758
|
+
})
|
|
759
|
+
.where(eq(schema.mediaJobTable.id, id));
|
|
760
|
+
},
|
|
761
|
+
resetStaleMediaJobs: async (olderThan) => {
|
|
762
|
+
const res = await db
|
|
763
|
+
.update(schema.mediaJobTable)
|
|
764
|
+
.set({ status: 'pending', lockedAt: null, lockedBy: null, updatedAt: new Date() })
|
|
765
|
+
.where(and(eq(schema.mediaJobTable.status, 'processing'), lte(schema.mediaJobTable.lockedAt, olderThan)))
|
|
766
|
+
.returning({ id: schema.mediaJobTable.id });
|
|
767
|
+
return res.length;
|
|
768
|
+
},
|
|
769
|
+
listMediaJobs: async (opts) => {
|
|
770
|
+
const conds = [];
|
|
771
|
+
if (opts.status)
|
|
772
|
+
conds.push(eq(schema.mediaJobTable.status, opts.status));
|
|
773
|
+
if (opts.type)
|
|
774
|
+
conds.push(eq(schema.mediaJobTable.type, opts.type));
|
|
775
|
+
const rows = await db
|
|
776
|
+
.select()
|
|
777
|
+
.from(schema.mediaJobTable)
|
|
778
|
+
.where(conds.length ? and(...conds) : undefined)
|
|
779
|
+
.orderBy(desc(schema.mediaJobTable.updatedAt))
|
|
780
|
+
.limit(opts.limit)
|
|
781
|
+
.offset(opts.offset ?? 0);
|
|
782
|
+
return rows;
|
|
783
|
+
},
|
|
784
|
+
countMediaJobs: async () => {
|
|
785
|
+
const rows = await db
|
|
786
|
+
.select({ status: schema.mediaJobTable.status, c: count() })
|
|
787
|
+
.from(schema.mediaJobTable)
|
|
788
|
+
.groupBy(schema.mediaJobTable.status);
|
|
789
|
+
const out = {
|
|
790
|
+
pending: 0,
|
|
791
|
+
processing: 0,
|
|
792
|
+
done: 0,
|
|
793
|
+
failed: 0,
|
|
794
|
+
skipped: 0
|
|
795
|
+
};
|
|
796
|
+
for (const r of rows)
|
|
797
|
+
out[r.status] = Number(r.c);
|
|
798
|
+
return out;
|
|
799
|
+
},
|
|
800
|
+
retryMediaJob: async (id) => {
|
|
801
|
+
await db
|
|
802
|
+
.update(schema.mediaJobTable)
|
|
803
|
+
.set({
|
|
804
|
+
status: 'pending',
|
|
805
|
+
attempts: 0,
|
|
806
|
+
lastError: null,
|
|
807
|
+
runAfter: new Date(),
|
|
808
|
+
lockedAt: null,
|
|
809
|
+
lockedBy: null,
|
|
810
|
+
updatedAt: new Date()
|
|
811
|
+
})
|
|
812
|
+
.where(eq(schema.mediaJobTable.id, id));
|
|
813
|
+
},
|
|
702
814
|
deleteMediaFile: async (id) => {
|
|
703
815
|
await db.delete(schema.mediaFilesTable).where(eq(schema.mediaFilesTable.id, id));
|
|
704
816
|
},
|
|
@@ -13,5 +13,5 @@ export const imageStylesTable = pgTable('image_styles', {
|
|
|
13
13
|
mimeType: text('mime_type').notNull(),
|
|
14
14
|
aspectRatio: real('aspect_ratio')
|
|
15
15
|
}, (t) => [
|
|
16
|
-
uniqueIndex('image_styles_unique_key').on(t.mediaFileId, t.name, sql `COALESCE(${t.width}, 0)`, sql `COALESCE(${t.height}, 0)`, sql `COALESCE(${t.quality}, 0)`, sql `COALESCE(${t.aspectRatio}, 0)
|
|
16
|
+
uniqueIndex('image_styles_unique_key').on(t.mediaFileId, t.name, sql `COALESCE(${t.width}, 0)`, sql `COALESCE(${t.height}, 0)`, sql `COALESCE(${t.quality}, 0)`, sql `COALESCE(${t.aspectRatio}, 0)`, t.crop)
|
|
17
17
|
]);
|
|
@@ -225,6 +225,23 @@ export declare const mediaFilesTable: import("drizzle-orm/pg-core/table", { with
|
|
|
225
225
|
identity: undefined;
|
|
226
226
|
generated: undefined;
|
|
227
227
|
}, {}, {}>;
|
|
228
|
+
fallbackUrl: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
229
|
+
name: "fallback_url";
|
|
230
|
+
tableName: "media_file";
|
|
231
|
+
dataType: "string";
|
|
232
|
+
columnType: "PgText";
|
|
233
|
+
data: string;
|
|
234
|
+
driverParam: string;
|
|
235
|
+
notNull: false;
|
|
236
|
+
hasDefault: false;
|
|
237
|
+
isPrimaryKey: false;
|
|
238
|
+
isAutoincrement: false;
|
|
239
|
+
hasRuntimeDefault: false;
|
|
240
|
+
enumValues: [string, ...string[]];
|
|
241
|
+
baseColumn: never;
|
|
242
|
+
identity: undefined;
|
|
243
|
+
generated: undefined;
|
|
244
|
+
}, {}, {}>;
|
|
228
245
|
focalX: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
229
246
|
name: "focal_x";
|
|
230
247
|
tableName: "media_file";
|
|
@@ -15,6 +15,7 @@ export const mediaFilesTable = pgTable('media_file', {
|
|
|
15
15
|
posterUrl: text('poster_url'), // obraz poster (alternatywa dla thumbnail)
|
|
16
16
|
mimeType: text('mime_type'), // dokładny typ MIME (np. video/mp4, video/webm)
|
|
17
17
|
blurDataUrl: text('blur_data_url'), // LQIP placeholder for images
|
|
18
|
+
fallbackUrl: text('fallback_url'), // lekki fallback serwowany dopóki warianty pending
|
|
18
19
|
focalX: real('focal_x'), // focal point X (0-1)
|
|
19
20
|
focalY: real('focal_y'), // focal point Y (0-1)
|
|
20
21
|
// Accessibility: powiązane pliki (transkrypcja, audiodeskrypcja)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
export declare const mediaJobTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
|
|
2
|
+
name: "media_job";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "media_job";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgUUID";
|
|
10
|
+
data: string;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: true;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
type: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
23
|
+
name: "type";
|
|
24
|
+
tableName: "media_job";
|
|
25
|
+
dataType: "string";
|
|
26
|
+
columnType: "PgText";
|
|
27
|
+
data: string;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: false;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: [string, ...string[]];
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
mediaFileId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
40
|
+
name: "media_file_id";
|
|
41
|
+
tableName: "media_job";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgUUID";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: true;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: undefined;
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
payload: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
57
|
+
name: "payload";
|
|
58
|
+
tableName: "media_job";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgText";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: true;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: [string, ...string[]];
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
dedupeKey: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
74
|
+
name: "dedupe_key";
|
|
75
|
+
tableName: "media_job";
|
|
76
|
+
dataType: "string";
|
|
77
|
+
columnType: "PgText";
|
|
78
|
+
data: string;
|
|
79
|
+
driverParam: string;
|
|
80
|
+
notNull: true;
|
|
81
|
+
hasDefault: false;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: [string, ...string[]];
|
|
86
|
+
baseColumn: never;
|
|
87
|
+
identity: undefined;
|
|
88
|
+
generated: undefined;
|
|
89
|
+
}, {}, {}>;
|
|
90
|
+
status: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
91
|
+
name: "status";
|
|
92
|
+
tableName: "media_job";
|
|
93
|
+
dataType: "string";
|
|
94
|
+
columnType: "PgText";
|
|
95
|
+
data: string;
|
|
96
|
+
driverParam: string;
|
|
97
|
+
notNull: true;
|
|
98
|
+
hasDefault: true;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: [string, ...string[]];
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
priority: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
108
|
+
name: "priority";
|
|
109
|
+
tableName: "media_job";
|
|
110
|
+
dataType: "number";
|
|
111
|
+
columnType: "PgInteger";
|
|
112
|
+
data: number;
|
|
113
|
+
driverParam: string | number;
|
|
114
|
+
notNull: true;
|
|
115
|
+
hasDefault: true;
|
|
116
|
+
isPrimaryKey: false;
|
|
117
|
+
isAutoincrement: false;
|
|
118
|
+
hasRuntimeDefault: false;
|
|
119
|
+
enumValues: undefined;
|
|
120
|
+
baseColumn: never;
|
|
121
|
+
identity: undefined;
|
|
122
|
+
generated: undefined;
|
|
123
|
+
}, {}, {}>;
|
|
124
|
+
attempts: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
125
|
+
name: "attempts";
|
|
126
|
+
tableName: "media_job";
|
|
127
|
+
dataType: "number";
|
|
128
|
+
columnType: "PgInteger";
|
|
129
|
+
data: number;
|
|
130
|
+
driverParam: string | number;
|
|
131
|
+
notNull: true;
|
|
132
|
+
hasDefault: true;
|
|
133
|
+
isPrimaryKey: false;
|
|
134
|
+
isAutoincrement: false;
|
|
135
|
+
hasRuntimeDefault: false;
|
|
136
|
+
enumValues: undefined;
|
|
137
|
+
baseColumn: never;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}, {}, {}>;
|
|
141
|
+
maxAttempts: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
142
|
+
name: "max_attempts";
|
|
143
|
+
tableName: "media_job";
|
|
144
|
+
dataType: "number";
|
|
145
|
+
columnType: "PgInteger";
|
|
146
|
+
data: number;
|
|
147
|
+
driverParam: string | number;
|
|
148
|
+
notNull: true;
|
|
149
|
+
hasDefault: true;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: undefined;
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
lastError: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
159
|
+
name: "last_error";
|
|
160
|
+
tableName: "media_job";
|
|
161
|
+
dataType: "string";
|
|
162
|
+
columnType: "PgText";
|
|
163
|
+
data: string;
|
|
164
|
+
driverParam: string;
|
|
165
|
+
notNull: false;
|
|
166
|
+
hasDefault: false;
|
|
167
|
+
isPrimaryKey: false;
|
|
168
|
+
isAutoincrement: false;
|
|
169
|
+
hasRuntimeDefault: false;
|
|
170
|
+
enumValues: [string, ...string[]];
|
|
171
|
+
baseColumn: never;
|
|
172
|
+
identity: undefined;
|
|
173
|
+
generated: undefined;
|
|
174
|
+
}, {}, {}>;
|
|
175
|
+
runAfter: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
176
|
+
name: "run_after";
|
|
177
|
+
tableName: "media_job";
|
|
178
|
+
dataType: "date";
|
|
179
|
+
columnType: "PgTimestamp";
|
|
180
|
+
data: Date;
|
|
181
|
+
driverParam: string;
|
|
182
|
+
notNull: true;
|
|
183
|
+
hasDefault: true;
|
|
184
|
+
isPrimaryKey: false;
|
|
185
|
+
isAutoincrement: false;
|
|
186
|
+
hasRuntimeDefault: false;
|
|
187
|
+
enumValues: undefined;
|
|
188
|
+
baseColumn: never;
|
|
189
|
+
identity: undefined;
|
|
190
|
+
generated: undefined;
|
|
191
|
+
}, {}, {}>;
|
|
192
|
+
lockedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
193
|
+
name: "locked_at";
|
|
194
|
+
tableName: "media_job";
|
|
195
|
+
dataType: "date";
|
|
196
|
+
columnType: "PgTimestamp";
|
|
197
|
+
data: Date;
|
|
198
|
+
driverParam: string;
|
|
199
|
+
notNull: false;
|
|
200
|
+
hasDefault: false;
|
|
201
|
+
isPrimaryKey: false;
|
|
202
|
+
isAutoincrement: false;
|
|
203
|
+
hasRuntimeDefault: false;
|
|
204
|
+
enumValues: undefined;
|
|
205
|
+
baseColumn: never;
|
|
206
|
+
identity: undefined;
|
|
207
|
+
generated: undefined;
|
|
208
|
+
}, {}, {}>;
|
|
209
|
+
lockedBy: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
210
|
+
name: "locked_by";
|
|
211
|
+
tableName: "media_job";
|
|
212
|
+
dataType: "string";
|
|
213
|
+
columnType: "PgText";
|
|
214
|
+
data: string;
|
|
215
|
+
driverParam: string;
|
|
216
|
+
notNull: false;
|
|
217
|
+
hasDefault: false;
|
|
218
|
+
isPrimaryKey: false;
|
|
219
|
+
isAutoincrement: false;
|
|
220
|
+
hasRuntimeDefault: false;
|
|
221
|
+
enumValues: [string, ...string[]];
|
|
222
|
+
baseColumn: never;
|
|
223
|
+
identity: undefined;
|
|
224
|
+
generated: undefined;
|
|
225
|
+
}, {}, {}>;
|
|
226
|
+
createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
227
|
+
name: "created_at";
|
|
228
|
+
tableName: "media_job";
|
|
229
|
+
dataType: "date";
|
|
230
|
+
columnType: "PgTimestamp";
|
|
231
|
+
data: Date;
|
|
232
|
+
driverParam: string;
|
|
233
|
+
notNull: true;
|
|
234
|
+
hasDefault: true;
|
|
235
|
+
isPrimaryKey: false;
|
|
236
|
+
isAutoincrement: false;
|
|
237
|
+
hasRuntimeDefault: false;
|
|
238
|
+
enumValues: undefined;
|
|
239
|
+
baseColumn: never;
|
|
240
|
+
identity: undefined;
|
|
241
|
+
generated: undefined;
|
|
242
|
+
}, {}, {}>;
|
|
243
|
+
updatedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
244
|
+
name: "updated_at";
|
|
245
|
+
tableName: "media_job";
|
|
246
|
+
dataType: "date";
|
|
247
|
+
columnType: "PgTimestamp";
|
|
248
|
+
data: Date;
|
|
249
|
+
driverParam: string;
|
|
250
|
+
notNull: true;
|
|
251
|
+
hasDefault: true;
|
|
252
|
+
isPrimaryKey: false;
|
|
253
|
+
isAutoincrement: false;
|
|
254
|
+
hasRuntimeDefault: false;
|
|
255
|
+
enumValues: undefined;
|
|
256
|
+
baseColumn: never;
|
|
257
|
+
identity: undefined;
|
|
258
|
+
generated: undefined;
|
|
259
|
+
}, {}, {}>;
|
|
260
|
+
completedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
261
|
+
name: "completed_at";
|
|
262
|
+
tableName: "media_job";
|
|
263
|
+
dataType: "date";
|
|
264
|
+
columnType: "PgTimestamp";
|
|
265
|
+
data: Date;
|
|
266
|
+
driverParam: string;
|
|
267
|
+
notNull: false;
|
|
268
|
+
hasDefault: false;
|
|
269
|
+
isPrimaryKey: false;
|
|
270
|
+
isAutoincrement: false;
|
|
271
|
+
hasRuntimeDefault: false;
|
|
272
|
+
enumValues: undefined;
|
|
273
|
+
baseColumn: never;
|
|
274
|
+
identity: undefined;
|
|
275
|
+
generated: undefined;
|
|
276
|
+
}, {}, {}>;
|
|
277
|
+
};
|
|
278
|
+
dialect: "pg";
|
|
279
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { integer, pgTable, text, timestamp, uniqueIndex, index, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
export const mediaJobTable = pgTable('media_job', {
|
|
3
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
4
|
+
type: text('type').notNull(), // 'image_style' (na start; rozszerzalne: video_poster, video_transcode)
|
|
5
|
+
mediaFileId: uuid('media_file_id').notNull(),
|
|
6
|
+
payload: text('payload').notNull(), // JSON.stringify(ImageFieldStyle)
|
|
7
|
+
dedupeKey: text('dedupe_key').notNull(),
|
|
8
|
+
status: text('status').notNull().default('pending'), // pending|processing|done|failed|skipped
|
|
9
|
+
priority: integer('priority').notNull().default(0), // wyżej = pilniejsze
|
|
10
|
+
attempts: integer('attempts').notNull().default(0),
|
|
11
|
+
maxAttempts: integer('max_attempts').notNull().default(3),
|
|
12
|
+
lastError: text('last_error'),
|
|
13
|
+
runAfter: timestamp('run_after').notNull().defaultNow(),
|
|
14
|
+
lockedAt: timestamp('locked_at'),
|
|
15
|
+
lockedBy: text('locked_by'),
|
|
16
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
17
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
18
|
+
completedAt: timestamp('completed_at')
|
|
19
|
+
}, (t) => [
|
|
20
|
+
uniqueIndex('media_job_dedupe_key').on(t.dedupeKey),
|
|
21
|
+
index('media_job_claim_idx').on(t.status, t.runAfter, t.priority),
|
|
22
|
+
index('media_job_media_idx').on(t.mediaFileId)
|
|
23
|
+
]);
|