import { createHash, randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { basename, extname } from "node:path"; import { Readable } from "node:stream"; import type BetterSqlite3 from "better-sqlite3"; import sharp, { type Metadata } from "sharp"; import type { StaticStickerCatalogItem } from "@dada/static-sticker-catalog"; import { auditRetentionMilliseconds, isSafeAuditRef, isSafeAuditSummaryJson, serializeAuditSummary, } from "./audit-policy.js"; import { ManagedStorage, type StagedManagedFile } from "./managed-storage.js"; import { StickerReleaseError } from "./sticker-release-errors.js"; import { classifyCapacity } from "./storage-policy.js"; export { StickerReleaseError } from "./sticker-release-errors.js"; const require = createRequire(import.meta.url); const Database = require("better-sqlite3") as typeof BetterSqlite3; const stableIdPattern = /^STK([0-9]{4,})$/; const idempotencyPattern = /^[A-Za-z0-9_-]{32,200}$/; const sha256Pattern = /^[0-9a-f]{64}$/i; const maximumOriginalBytes = 20 * 1024 * 1024; const maximumDimension = 8_192; const bundledPartCounts = [203, 36, 27, 48, 38, 75, 37, 67, 48, 24, 40, 30, 27, 51, 62, 19, 36, 45, 92, 53, 69, 31, 36, 30, 183] as const; type StickerMime = "image/png" | "image/webp"; type StickerVariant = "original" | "thumbnail"; interface StickerItemRow { enabled: 0 | 1; height: number; mime_type: StickerMime; order_index: number; original_byte_size: number; original_file_id: string; original_filename: string; original_relative_path: string; original_sha256: string; part: number; release_version: string; stable_id: string; thumbnail_byte_size: number; thumbnail_file_id: string; thumbnail_relative_path: string; thumbnail_sha256: string; width: number; } export interface StickerUploadInput { actorId: string; content: Readable; enabled: boolean; expectedByteSize: number; expectedMimeType: StickerMime; expectedSha256: string; fileName: string; idempotencyKey: string; order: number; part: number; stableId: string; } function digest(value: string) { return createHash("sha256").update(value, "utf8").digest("hex"); } function stableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (value && typeof value === "object") { return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`; } return JSON.stringify(value); } function iso(timestamp: number) { return new Date(timestamp).toISOString(); } function itemView(row: StickerItemRow): StaticStickerCatalogItem { const originalReference = `/api/v1/assets/public/${encodeURIComponent(row.release_version)}/${encodeURIComponent(row.stable_id)}`; return { enabled: row.enabled === 1, height: row.height, mime: row.mime_type, mime_type: row.mime_type, order: row.order_index, original_filename: row.original_filename, original_reference: originalReference, origin: "admin_uploaded", part: row.part, relative_path: `static-stickers/${row.stable_id}${row.mime_type === "image/png" ? ".png" : ".webp"}`, resource_version: row.release_version, sha256: row.original_sha256, stable_id: row.stable_id, thumbnail_reference: { media: "thumbnail", resource_id: row.stable_id, resource_version: row.release_version, url: `${originalReference}?variant=thumbnail`, }, width: row.width, }; } export class StickerReleaseService { private readonly clock: () => number; private readonly database: BetterSqlite3.Database; private readonly storage: ManagedStorage; constructor(input: { clock?: () => number; databasePath: string; storage: ManagedStorage }) { this.clock = input.clock ?? Date.now; const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING; this.database = new Database(input.databasePath, nativeBinding ? { nativeBinding } : undefined); this.database.pragma("journal_mode = WAL"); this.database.pragma("foreign_keys = ON"); this.database.pragma("busy_timeout = 5000"); this.database.function("dada_audit_ref_is_safe", { deterministic: true }, isSafeAuditRef); this.database.function("dada_audit_summary_is_safe", { deterministic: true }, isSafeAuditSummaryJson); this.database.function("dada_allow_privacy_purge", { deterministic: false }, () => 0); this.database.function("dada_privacy_purge_subject", { deterministic: false }, () => ""); this.database.function("dada_allow_retention_purge", { deterministic: false }, () => 0); this.database.function("dada_retention_purge_now", { deterministic: false }, () => 0); this.storage = input.storage; this.migrate(); } close() { this.database.close(); } async upload(input: StickerUploadInput) { this.validateUpload(input); const requestHash = digest(stableJson({ enabled: input.enabled, expected_byte_size: input.expectedByteSize, expected_mime_type: input.expectedMimeType, expected_sha256: input.expectedSha256.toLowerCase(), order: input.order, part: input.part, stable_id: input.stableId, })); const keyDigest = digest(input.idempotencyKey); const receipt = this.database.prepare(` SELECT request_hash, release_version FROM sticker_upload_receipts WHERE actor_id = ? AND idempotency_key_digest = ? `).get(input.actorId, keyDigest) as { release_version: string; request_hash: string } | undefined; if (receipt) { input.content.destroy(); if (receipt.request_hash !== requestHash) throw new StickerReleaseError("sticker_idempotency_conflict", 409); return this.uploadResult(receipt.release_version, input.stableId, false); } this.assertNewPosition(input.stableId, input.part, input.order); const staged: StagedManagedFile[] = []; try { const original = await this.storage.stageManagedImage({ content: input.content, expectedMimeType: input.expectedMimeType, expectedSha256: input.expectedSha256, fileKind: "sticker_original", fileName: `${input.stableId}${input.expectedMimeType === "image/png" ? ".png" : ".webp"}`, maximumBytes: maximumOriginalBytes, operationId: randomUUID(), ownerRef: input.actorId, projectedWriteBytes: input.expectedByteSize, }); staged.push(original); if (original.bytes !== input.expectedByteSize) throw new StickerReleaseError("content_size_invalid"); let metadata: Metadata; let thumbnail: Buffer; const decoder = sharp(readFileSync(original.stagingPath), { failOn: "warning", limitInputPixels: maximumDimension * maximumDimension }); try { metadata = await decoder.metadata(); if (metadata.format !== (input.expectedMimeType === "image/png" ? "png" : "webp") || !metadata.width || !metadata.height || metadata.width > maximumDimension || metadata.height > maximumDimension) { throw new Error("content_decode_invalid"); } thumbnail = await decoder .rotate() .resize({ fit: "inside", height: 256, width: 256, withoutEnlargement: true }) .png({ adaptiveFiltering: true, compressionLevel: 9 }) .toBuffer(); } catch { throw new StickerReleaseError("content_decode_invalid"); } finally { decoder.destroy(); } const thumbnailStaged = await this.storage.stageManagedImage({ content: Readable.from(thumbnail), expectedMimeType: "image/png", fileKind: "sticker_thumbnail", fileName: `${input.stableId}-thumbnail.png`, maximumBytes: maximumOriginalBytes, operationId: randomUUID(), ownerRef: input.actorId, projectedWriteBytes: thumbnail.byteLength, }); staged.push(thumbnailStaged); const releaseVersion = this.immediate(() => this.commitUpload({ ...input, height: metadata.height!, keyDigest, original, requestHash, thumbnail: thumbnailStaged, width: metadata.width!, })); return this.uploadResult(releaseVersion, input.stableId, true); } catch (error) { for (const file of staged) this.storage.abandonStagedFile(file); if (!(error instanceof StickerReleaseError) && error instanceof Error && new Set(["content_hash_invalid", "content_mime_invalid", "content_size_invalid", "file_name_invalid"]).has(error.message)) { throw new StickerReleaseError("sticker_upload_invalid"); } throw error; } } update(input: { actorId: string; enabled?: boolean; order?: number; part?: number; stableId: string }) { const current = this.currentVersion(); if (!current) throw new StickerReleaseError("sticker_not_found", 404); const existing = this.readItem(current, input.stableId); if (!existing) throw new StickerReleaseError("sticker_not_found", 404); const part = input.part ?? existing.part; const order = input.order ?? existing.order_index; this.validatePosition(input.stableId, part, order); const releaseVersion = this.immediate(() => { const version = this.nextReleaseVersion(); this.copyRelease(current, version); const conflict = this.database.prepare(` SELECT stable_id FROM sticker_release_items WHERE release_version = ? AND part = ? AND order_index = ? AND stable_id <> ? `).get(version, part, order, input.stableId); if (conflict) throw new StickerReleaseError("sticker_order_conflict", 409); this.database.prepare(` UPDATE sticker_release_items SET enabled = ?, part = ?, order_index = ? WHERE release_version = ? AND stable_id = ? `).run((input.enabled ?? existing.enabled === 1) ? 1 : 0, part, order, version, input.stableId); this.finalizeRelease(version, current, input.actorId); this.insertReleaseAudit({ actorId: input.actorId, afterSummary: { enabled: input.enabled ?? existing.enabled === 1, order, part, stable_id: input.stableId }, beforeSummary: { enabled: existing.enabled === 1, order: existing.order_index, part: existing.part, stable_id: input.stableId }, operationType: "sticker_release_update", releaseVersion: version, }); return version; }); return { item: itemView(this.readItem(releaseVersion, input.stableId)!), release_version: releaseVersion }; } listPublic(releaseVersion = this.currentVersion()) { if (!releaseVersion) return { count: 0, items: [], release_version: null }; const exists = this.database.prepare("SELECT 1 FROM sticker_releases WHERE release_version = ?").get(releaseVersion); if (!exists) return { count: 0, items: [], release_version: null }; const items = (this.database.prepare(` SELECT * FROM sticker_release_items WHERE release_version = ? AND enabled = 1 ORDER BY part, order_index, stable_id `).all(releaseVersion) as StickerItemRow[]).map(itemView); return { count: items.length, items, release_version: releaseVersion }; } adminView() { const releaseVersion = this.currentVersion(); const items = releaseVersion ? (this.database.prepare("SELECT * FROM sticker_release_items WHERE release_version = ? ORDER BY part, order_index, stable_id").all(releaseVersion) as StickerItemRow[]) : []; return { count: items.length, items: items.map((row) => ({ ...itemView(row), file_state: "committed" as const, original_byte_size: row.original_byte_size, thumbnail_byte_size: row.thumbnail_byte_size, })), release_version: releaseVersion, storage: this.storage.getState(), }; } readPublicAsset(releaseVersion: string, stableId: string, variant: StickerVariant) { const row = this.readItem(releaseVersion, stableId); if (!row || row.enabled !== 1) return undefined; const fileId = variant === "thumbnail" ? row.thumbnail_file_id : row.original_file_id; const path = this.storage.resolveManagedFile(fileId); if (!path) return undefined; return { bytes: readFileSync(path), mimeType: variant === "thumbnail" ? "image/png" as const : row.mime_type, sha256: variant === "thumbnail" ? row.thumbnail_sha256 : row.original_sha256, }; } inspectCounts() { const count = (table: string) => (this.database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count; return { items: count("sticker_release_items"), releases: count("sticker_releases"), upload_receipts: count("sticker_upload_receipts") }; } private uploadResult(releaseVersion: string, stableId: string, created: boolean) { const row = this.readItem(releaseVersion, stableId); if (!row) throw new StickerReleaseError("sticker_not_found", 404); return { created, item: itemView(row), original: { byte_size: row.original_byte_size, file_id: row.original_file_id, sha256: row.original_sha256 }, release_version: releaseVersion, thumbnail: { byte_size: row.thumbnail_byte_size, file_id: row.thumbnail_file_id, sha256: row.thumbnail_sha256 }, }; } private commitUpload(input: StickerUploadInput & { height: number; keyDigest: string; original: StagedManagedFile; requestHash: string; thumbnail: StagedManagedFile; width: number; }) { this.assertNewPosition(input.stableId, input.part, input.order); const previous = this.currentVersion(); const releaseVersion = this.nextReleaseVersion(); if (previous) this.copyRelease(previous, releaseVersion); for (const file of [input.original, input.thumbnail]) { this.storage.moveStagedFile(file); this.database.prepare(` INSERT INTO managed_files (file_id, file_kind, owner_ref, relative_path, byte_size, mime_type, sha256, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'committed', ?) `).run(file.fileId, file.fileKind, file.ownerRef, file.relativePath, file.bytes, file.mimeType, file.sha256, iso(this.clock())); } this.database.prepare(` INSERT INTO sticker_release_items ( release_version, stable_id, part, order_index, original_filename, original_relative_path, width, height, mime_type, original_sha256, original_file_id, original_byte_size, thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( releaseVersion, input.stableId, input.part, input.order, input.fileName, input.original.relativePath, input.width, input.height, input.expectedMimeType, input.original.sha256, input.original.fileId, input.original.bytes, input.thumbnail.fileId, input.thumbnail.relativePath, input.thumbnail.sha256, input.thumbnail.bytes, input.enabled ? 1 : 0, ); this.database.prepare(` INSERT OR IGNORE INTO sticker_managed_file_history ( managed_file_id, stable_id, resource_version, file_kind, created_at ) VALUES (?, ?, ?, 'original', ?), (?, ?, ?, 'thumbnail', ?) `).run( input.original.fileId, input.stableId, releaseVersion, this.clock(), input.thumbnail.fileId, input.stableId, releaseVersion, this.clock(), ); this.consumeStagedStorage([input.original, input.thumbnail]); this.database.prepare(` INSERT INTO sticker_upload_receipts (actor_id, idempotency_key_digest, request_hash, release_version, stable_id, created_at) VALUES (?, ?, ?, ?, ?, ?) `).run(input.actorId, input.keyDigest, input.requestHash, releaseVersion, input.stableId, iso(this.clock())); this.finalizeRelease(releaseVersion, previous, input.actorId); this.insertReleaseAudit({ actorId: input.actorId, afterSummary: { enabled: input.enabled, order: input.order, part: input.part, stable_id: input.stableId }, beforeSummary: previous ? { release_version: previous } : null, operationType: "sticker_release_publish", releaseVersion, }); return releaseVersion; } private insertReleaseAudit(input: { actorId: string; afterSummary: Record; beforeSummary: Record | null; operationType: "sticker_release_publish" | "sticker_release_update"; releaseVersion: string; }) { const occurredAt = this.clock(); this.database.prepare(` INSERT INTO admin_operation_logs ( log_id, actor_type, actor_ref, operation_type, target_type, target_ref, result, before_summary, after_summary, occurred_at, expires_at ) VALUES (?, 'super_admin', ?, ?, 'sticker_release', ?, 'succeeded', ?, ?, ?, ?) `).run( randomUUID(), input.actorId, input.operationType, input.releaseVersion, serializeAuditSummary(input.beforeSummary), serializeAuditSummary(input.afterSummary), occurredAt, occurredAt + auditRetentionMilliseconds, ); } private finalizeRelease(releaseVersion: string, previous: string | null, actorId: string) { const rows = this.database.prepare(` SELECT stable_id, part, order_index, original_sha256, thumbnail_sha256, enabled FROM sticker_release_items WHERE release_version = ? ORDER BY stable_id `).all(releaseVersion); const manifestSha256 = digest(stableJson(rows)); this.database.prepare(` INSERT INTO sticker_releases (release_version, previous_release_version, manifest_sha256, published_at, published_by) VALUES (?, ?, ?, ?, ?) `).run(releaseVersion, previous, manifestSha256, iso(this.clock()), actorId); this.database.prepare(` INSERT INTO current_sticker_release (singleton, release_version) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET release_version = excluded.release_version `).run(releaseVersion); const files = this.database.prepare(` SELECT original_file_id AS file_id FROM sticker_release_items WHERE release_version = ? UNION SELECT thumbnail_file_id AS file_id FROM sticker_release_items WHERE release_version = ? `).all(releaseVersion, releaseVersion) as Array<{ file_id: string }>; for (const file of files) { this.database.prepare(` INSERT INTO project_asset_refs (reference_id, managed_file_id, reference_type, created_at) VALUES (?, ?, 'release', ?) `).run(`release:${releaseVersion}:${file.file_id}`, file.file_id, iso(this.clock())); } } private copyRelease(from: string, to: string) { this.database.prepare(` INSERT INTO sticker_release_items ( release_version, stable_id, part, order_index, original_filename, original_relative_path, width, height, mime_type, original_sha256, original_file_id, original_byte_size, thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled ) SELECT ?, stable_id, part, order_index, original_filename, original_relative_path, width, height, mime_type, original_sha256, original_file_id, original_byte_size, thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled FROM sticker_release_items WHERE release_version = ? `).run(to, from); } private consumeStagedStorage(files: StagedManagedFile[]) { const timestamp = iso(this.clock()); for (const file of files) { this.database.prepare(` UPDATE storage_reservations SET status = 'consumed', resolved_at = ? WHERE operation_id = ? AND status = 'active' `).run(timestamp, file.operationId); } const total = files.reduce((sum, file) => sum + file.bytes, 0); const state = this.database.prepare("SELECT managed_content_bytes FROM local_backend_storage_state WHERE singleton = 1").get() as { managed_content_bytes: number }; const active = this.database.prepare("SELECT COALESCE(SUM(projected_bytes), 0) AS bytes FROM storage_reservations WHERE status = 'active'").get() as { bytes: number }; const nextBytes = state.managed_content_bytes + total; const classification = classifyCapacity(nextBytes, active.bytes); this.database.prepare(` UPDATE local_backend_storage_state SET managed_content_bytes = ?, capacity_notice_level = ?, storage_status = ?, measured_at = ?, version = version + 1 WHERE singleton = 1 `).run(nextBytes, classification.capacity_notice_level, classification.storage_status, timestamp); } private currentVersion() { return (this.database.prepare("SELECT release_version FROM current_sticker_release WHERE singleton = 1").get() as { release_version: string } | undefined)?.release_version ?? null; } private nextReleaseVersion() { const date = new Date(this.clock()).toISOString().slice(0, 10).replaceAll("-", ""); const row = this.database.prepare("SELECT next_sequence FROM sticker_release_sequences WHERE release_date = ?").get(date) as { next_sequence: number } | undefined; const sequence = row?.next_sequence ?? 1; this.database.prepare(` INSERT INTO sticker_release_sequences (release_date, next_sequence) VALUES (?, ?) ON CONFLICT(release_date) DO UPDATE SET next_sequence = excluded.next_sequence `).run(date, sequence + 1); return `asset-${date}.${sequence}`; } private readItem(releaseVersion: string, stableId: string) { return this.database.prepare("SELECT * FROM sticker_release_items WHERE release_version = ? AND stable_id = ?") .get(releaseVersion, stableId) as StickerItemRow | undefined; } private assertNewPosition(stableId: string, part: number, order: number) { this.validatePosition(stableId, part, order); const current = this.currentVersion(); if (!current) return; if (this.readItem(current, stableId)) throw new StickerReleaseError("sticker_stable_id_conflict", 409); const conflict = this.database.prepare(` SELECT stable_id FROM sticker_release_items WHERE release_version = ? AND part = ? AND order_index = ? `).get(current, part, order); if (conflict) throw new StickerReleaseError("sticker_order_conflict", 409); } private validatePosition(stableId: string, part: number, order: number) { const matched = stableId.match(stableIdPattern); const numericId = matched ? Number(matched[1]) : Number.NaN; if (!matched || !Number.isSafeInteger(numericId) || numericId <= 1_407) throw new StickerReleaseError("sticker_stable_id_invalid"); if (!Number.isSafeInteger(part) || part < 1 || part > bundledPartCounts.length || !Number.isSafeInteger(order) || order <= bundledPartCounts[part - 1]!) { throw new StickerReleaseError("sticker_part_order_invalid"); } } private validateUpload(input: StickerUploadInput) { this.validatePosition(input.stableId, input.part, input.order); if (!/^[0-9a-f-]{36}$/i.test(input.actorId) || !idempotencyPattern.test(input.idempotencyKey) || !sha256Pattern.test(input.expectedSha256) || !Number.isSafeInteger(input.expectedByteSize) || input.expectedByteSize <= 0 || input.expectedByteSize > maximumOriginalBytes || !new Set(["image/png", "image/webp"]).has(input.expectedMimeType)) { throw new StickerReleaseError("sticker_upload_invalid"); } const expectedExtension = input.expectedMimeType === "image/png" ? ".png" : ".webp"; if (input.fileName.length > 255 || basename(input.fileName) !== input.fileName || /[\u0000-\u001f]/.test(input.fileName) || extname(input.fileName).toLowerCase() !== expectedExtension) throw new StickerReleaseError("sticker_upload_invalid"); } private immediate(action: () => T) { this.database.exec("BEGIN IMMEDIATE"); try { const result = action(); this.database.exec("COMMIT"); return result; } catch (error) { if (this.database.inTransaction) this.database.exec("ROLLBACK"); throw error; } } private migrate() { this.database.exec(` CREATE TABLE IF NOT EXISTS sticker_release_sequences ( release_date TEXT PRIMARY KEY, next_sequence INTEGER NOT NULL CHECK (next_sequence >= 1) ); CREATE TABLE IF NOT EXISTS sticker_releases ( release_version TEXT PRIMARY KEY, previous_release_version TEXT, manifest_sha256 TEXT NOT NULL CHECK (length(manifest_sha256) = 64), published_at TEXT NOT NULL, published_by TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sticker_release_items ( release_version TEXT NOT NULL, stable_id TEXT NOT NULL, part INTEGER NOT NULL CHECK (part BETWEEN 1 AND 25), order_index INTEGER NOT NULL CHECK (order_index > 0), original_filename TEXT NOT NULL, original_relative_path TEXT NOT NULL, width INTEGER NOT NULL CHECK (width > 0), height INTEGER NOT NULL CHECK (height > 0), mime_type TEXT NOT NULL CHECK (mime_type IN ('image/png', 'image/webp')), original_sha256 TEXT NOT NULL CHECK (length(original_sha256) = 64), original_file_id TEXT NOT NULL REFERENCES managed_files(file_id), original_byte_size INTEGER NOT NULL CHECK (original_byte_size > 0), thumbnail_file_id TEXT NOT NULL REFERENCES managed_files(file_id), thumbnail_relative_path TEXT NOT NULL, thumbnail_sha256 TEXT NOT NULL CHECK (length(thumbnail_sha256) = 64), thumbnail_byte_size INTEGER NOT NULL CHECK (thumbnail_byte_size > 0), enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)), PRIMARY KEY (release_version, stable_id), UNIQUE (release_version, part, order_index) ); CREATE TABLE IF NOT EXISTS current_sticker_release ( singleton INTEGER PRIMARY KEY CHECK (singleton = 1), release_version TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sticker_upload_receipts ( actor_id TEXT NOT NULL, idempotency_key_digest TEXT NOT NULL CHECK (length(idempotency_key_digest) = 64), request_hash TEXT NOT NULL CHECK (length(request_hash) = 64), release_version TEXT NOT NULL, stable_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (actor_id, idempotency_key_digest) ); CREATE TABLE IF NOT EXISTS sticker_managed_file_history ( managed_file_id TEXT NOT NULL, stable_id TEXT NOT NULL, resource_version TEXT NOT NULL, file_kind TEXT NOT NULL CHECK (file_kind IN ('original', 'thumbnail')), created_at INTEGER NOT NULL, PRIMARY KEY (managed_file_id, file_kind), FOREIGN KEY (managed_file_id) REFERENCES managed_files(file_id) ); CREATE TRIGGER IF NOT EXISTS sticker_releases_no_update BEFORE UPDATE ON sticker_releases BEGIN SELECT RAISE(ABORT, 'sticker_releases_immutable'); END; CREATE TRIGGER IF NOT EXISTS sticker_releases_no_delete BEFORE DELETE ON sticker_releases BEGIN SELECT RAISE(ABORT, 'sticker_releases_immutable'); END; CREATE TRIGGER IF NOT EXISTS sticker_release_items_no_update BEFORE UPDATE ON sticker_release_items WHEN EXISTS (SELECT 1 FROM sticker_releases WHERE release_version = OLD.release_version) BEGIN SELECT RAISE(ABORT, 'sticker_release_items_immutable'); END; CREATE TRIGGER IF NOT EXISTS sticker_release_items_no_delete BEFORE DELETE ON sticker_release_items WHEN EXISTS (SELECT 1 FROM sticker_releases WHERE release_version = OLD.release_version) BEGIN SELECT RAISE(ABORT, 'sticker_release_items_immutable'); END; `); this.database.exec(` INSERT OR IGNORE INTO sticker_managed_file_history ( managed_file_id, stable_id, resource_version, file_kind, created_at ) SELECT original_file_id, stable_id, release_version, 'original', strftime('%s', 'now') * 1000 FROM sticker_release_items; INSERT OR IGNORE INTO sticker_managed_file_history ( managed_file_id, stable_id, resource_version, file_kind, created_at ) SELECT thumbnail_file_id, stable_id, release_version, 'thumbnail', strftime('%s', 'now') * 1000 FROM sticker_release_items; `); } }