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.
Files changed (66) hide show
  1. package/API.md +9 -2
  2. package/CHANGELOG.md +29 -0
  3. package/DOCS.md +1 -1
  4. package/dist/admin/api/handler.js +2 -0
  5. package/dist/admin/api/media-jobs.d.ts +5 -0
  6. package/dist/admin/api/media-jobs.js +33 -0
  7. package/dist/admin/client/collection/collection-entries.svelte +20 -1
  8. package/dist/admin/client/collection/row-actions.svelte +12 -4
  9. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  10. package/dist/admin/client/maintenance/maintenance-page.svelte +9 -0
  11. package/dist/admin/client/maintenance/media-jobs-panel.svelte +310 -0
  12. package/dist/admin/client/maintenance/media-jobs-panel.svelte.d.ts +3 -0
  13. package/dist/admin/remote/entry.remote.d.ts +3 -0
  14. package/dist/admin/remote/entry.remote.js +14 -3
  15. package/dist/core/cms.js +7 -0
  16. package/dist/core/fields/dotPath.d.ts +4 -0
  17. package/dist/core/fields/dotPath.js +18 -0
  18. package/dist/core/fields/slugify.d.ts +3 -0
  19. package/dist/core/fields/slugify.js +4 -0
  20. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  21. package/dist/core/server/entries/operations/duplicate.js +73 -0
  22. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  23. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  24. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  25. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  26. package/dist/core/server/fields/utils/imageStyles.js +41 -4
  27. package/dist/core/server/index.d.ts +3 -0
  28. package/dist/core/server/index.js +3 -0
  29. package/dist/core/server/media/jobs/enqueue.d.ts +6 -0
  30. package/dist/core/server/media/jobs/enqueue.js +20 -0
  31. package/dist/core/server/media/jobs/processImageStyleJob.d.ts +19 -0
  32. package/dist/core/server/media/jobs/processImageStyleJob.js +23 -0
  33. package/dist/core/server/media/jobs/startWorker.d.ts +2 -0
  34. package/dist/core/server/media/jobs/startWorker.js +29 -0
  35. package/dist/core/server/media/jobs/worker.d.ts +29 -0
  36. package/dist/core/server/media/jobs/worker.js +54 -0
  37. package/dist/core/server/media/operations/replaceFile.js +2 -2
  38. package/dist/core/server/media/operations/uploadFile.js +10 -3
  39. package/dist/core/server/media/styles/buildStyleSpecs.d.ts +12 -0
  40. package/dist/core/server/media/styles/buildStyleSpecs.js +36 -0
  41. package/dist/core/server/media/styles/imagesConfig.d.ts +3 -0
  42. package/dist/core/server/media/styles/imagesConfig.js +16 -0
  43. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +19 -41
  44. package/dist/core/server/media/styles/operations/generateFallbackVariant.d.ts +10 -0
  45. package/dist/core/server/media/styles/operations/generateFallbackVariant.js +21 -0
  46. package/dist/core/server/media/styles/operations/getImageStyle.js +12 -8
  47. package/dist/core/server/media/styles/sharp/sharpConfig.d.ts +7 -0
  48. package/dist/core/server/media/styles/sharp/sharpConfig.js +12 -0
  49. package/dist/db-postgres/index.js +113 -1
  50. package/dist/db-postgres/schema/imageStyle.js +1 -1
  51. package/dist/db-postgres/schema/index.d.ts +1 -0
  52. package/dist/db-postgres/schema/index.js +1 -0
  53. package/dist/db-postgres/schema/mediaFile.d.ts +17 -0
  54. package/dist/db-postgres/schema/mediaFile.js +1 -0
  55. package/dist/db-postgres/schema/mediaJob.d.ts +279 -0
  56. package/dist/db-postgres/schema/mediaJob.js +23 -0
  57. package/dist/sveltekit/components/image.svelte +1 -1
  58. package/dist/types/adapters/db.d.ts +51 -0
  59. package/dist/types/cms.d.ts +46 -0
  60. package/dist/types/media.d.ts +1 -0
  61. package/dist/updates/0.36.8/index.d.ts +2 -0
  62. package/dist/updates/0.36.8/index.js +20 -0
  63. package/dist/updates/0.36.9/index.d.ts +2 -0
  64. package/dist/updates/0.36.9/index.js +12 -0
  65. package/dist/updates/index.js +5 -1
  66. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { getCMS } from '../../../../cms.js';
2
- import { isProcessableImage, defaultStyles, expandStyleFormats, getOriginalFormat } from '../../../fields/utils/imageStyles.js';
3
- import { getImageStyle } from './getImageStyle.js';
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 origFormat = getOriginalFormat(file);
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
- for (const style of expanded) {
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
- else {
25
- await getImageStyle(file.id, style);
26
- created++;
27
- }
28
- if (style.srcset && file.width) {
29
- const widths = style.srcset.filter((w) => w <= file.width);
30
- for (const w of widths) {
31
- const widthStyle = {
32
- ...style,
33
- name: `${style.name}_${w}w`,
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 origFormat = getOriginalFormat(file);
103
- const expanded = expandStyleFormats(defaultStyles, origFormat);
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
- const key = imageStyleKey(mediaFileId, style);
26
- try {
27
- const created = await createImageStyle(mediaFileId, style);
28
- cache.set(key, created);
29
- }
30
- catch (e) {
31
- console.warn(`[CMS] Failed to generate missing image style "${style.name}" for media ${mediaFileId}:`, e);
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
  ]);
@@ -7,4 +7,5 @@ export * from './formSubmission.js';
7
7
  export * from './consentLog.js';
8
8
  export * from './imageStyle.js';
9
9
  export * from './videoStyle.js';
10
+ export * from './mediaJob.js';
10
11
  export * from './shop/index.js';
@@ -7,4 +7,5 @@ export * from './formSubmission.js';
7
7
  export * from './consentLog.js';
8
8
  export * from './imageStyle.js';
9
9
  export * from './videoStyle.js';
10
+ export * from './mediaJob.js';
10
11
  export * from './shop/index.js';
@@ -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
+ ]);