import type { DatabaseSync } from 'node:sqlite'; import { InvalidPluginPackageAutomationPublicationError, MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES, PluginPackageAutomationPublicationConflictError, PluginPackageAutomationPublicationUnavailableError, assertPluginPackageAutomationPublicationSuccessor, assertPluginPackageAutomationPublicationRecoveryPageSize, normalizePluginPackageAutomationPublication, normalizePluginPackageAutomationPublicationRecoveryCursor, type PluginPackageAutomationPublication, type PluginPackageAutomationPublicationRecoveryPage, type PluginPackageAutomationPublicationRepository, type PluginPackageAutomationPublicationRecoverySource, type PluginPackageAutomationPublicationStartGuard, } from '@qinglong/runtime-core/plugin-package-automation-publication'; import { LocalSqliteOperationAuthority } from '../authority/operationAuthority'; type Row = Record; const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; const DIGEST = /^[0-9a-f]{64}$/; function invalid(message: string): never { throw new InvalidPluginPackageAutomationPublicationError(message); } function text(row: Row, key: string): string { const value = row[key]; if (typeof value !== 'string') { throw new PluginPackageAutomationPublicationUnavailableError(); } return value; } function integer(row: Row, key: string): number { const value = row[key]; if (!Number.isSafeInteger(value) || (value as number) < 0) { throw new PluginPackageAutomationPublicationUnavailableError(); } return value as number; } function targetIdentity( projectId: unknown, packageName: unknown, ): { readonly projectId: string; readonly packageName: string; } { if (typeof projectId !== 'string' || !IDENTIFIER.test(projectId)) { return invalid('projectId is invalid'); } if (typeof packageName !== 'string' || !PACKAGE_NAME.test(packageName)) { return invalid('packageName is invalid'); } return { projectId, packageName }; } function publicationDigest(value: unknown): string { if (typeof value !== 'string' || !DIGEST.test(value)) { return invalid('publicationDigest is invalid'); } return value; } function serialize( publication: Readonly, ): string { const value = JSON.stringify(publication); if ( Buffer.byteLength(value, 'utf8') > MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES ) { return invalid('publication exceeds the durable JSON budget'); } return value; } function mapStorageError(error: unknown): Error { if ( error instanceof InvalidPluginPackageAutomationPublicationError || error instanceof PluginPackageAutomationPublicationConflictError || error instanceof PluginPackageAutomationPublicationUnavailableError ) { return error; } if ( error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && error.code.startsWith('SQLITE_CONSTRAINT') ) { return new PluginPackageAutomationPublicationConflictError( 'durable publication chain changed', ); } return new PluginPackageAutomationPublicationUnavailableError({ cause: error instanceof Error ? error : undefined, }); } export class LocalSqlitePluginPackageAutomationPublicationRepository implements PluginPackageAutomationPublicationRepository, PluginPackageAutomationPublicationRecoverySource, PluginPackageAutomationPublicationStartGuard { readonly #authority: LocalSqliteOperationAuthority; constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) { this.#authority = authority instanceof LocalSqliteOperationAuthority ? authority : new LocalSqliteOperationAuthority(authority); } #parse(row: Row): Readonly { try { const publication = normalizePluginPackageAutomationPublication( JSON.parse( text(row, 'publicationJson'), ) as PluginPackageAutomationPublication, ); if ( publication.publicationDigest !== text(row, 'publicationDigest') || publication.target.projectId !== text(row, 'projectId') || publication.target.packageName !== text(row, 'packageName') || publication.target.installationId !== text(row, 'installationId') || publication.target.lockDigest !== text(row, 'lockDigest') || publication.target.generation !== integer(row, 'generation') || publication.target.generationDigest !== text(row, 'generationDigest') || publication.target.materializedRevisionDigest !== text(row, 'materializedRevisionDigest') || publication.state !== text(row, 'state') || publication.version !== integer(row, 'version') || publication.publishedAtMs !== integer(row, 'publishedAtMs') ) { throw new PluginPackageAutomationPublicationUnavailableError(); } return publication; } catch (error) { if (error instanceof PluginPackageAutomationPublicationUnavailableError) { throw error; } throw new PluginPackageAutomationPublicationUnavailableError(); } } #select( where: string, values: readonly (string | number | null)[], ): Row | undefined { return this.#authority.client .prepare( `SELECT publication_digest AS "publicationDigest", project_id AS "projectId", package_name AS "packageName", installation_id AS "installationId", lock_digest AS "lockDigest", generation, generation_digest AS "generationDigest", materialized_revision_digest AS "materializedRevisionDigest", state, version, published_at_ms AS "publishedAtMs", publication_json AS "publicationJson" FROM "QingLong3PluginPackageAutomationPublications" WHERE ${where}`, ) .get(...values) as Row | undefined; } #findByDigest( digest: string, ): Readonly | null { const row = this.#select('publication_digest = ?', [digest]); return row ? this.#parse(row) : null; } #findCurrent( projectId: string, packageName: string, ): Readonly | null { const row = this.#select( `publication_digest = ( SELECT publication_digest FROM "QingLong3PluginPackageAutomationPublicationHeads" WHERE project_id = ? AND package_name = ? )`, [projectId, packageName], ); return row ? this.#parse(row) : null; } #enqueue(work: () => T | Promise): Promise { return this.#authority.enqueue( async () => { try { return await work(); } catch (error) { throw mapStorageError(error); } }, () => new PluginPackageAutomationPublicationUnavailableError(), ); } findCurrent( projectIdValue: string, packageNameValue: string, ): Promise | null> { const { projectId, packageName } = targetIdentity( projectIdValue, packageNameValue, ); return this.#enqueue(() => this.#findCurrent(projectId, packageName)); } findByDigest( publicationDigestValue: string, ): Promise | null> { const digest = publicationDigest(publicationDigestValue); return this.#enqueue(() => this.#findByDigest(digest)); } isStartAllowed( projectIdValue: string, packageNameValue: string, publicationDigestValue: string, ): Promise { const { projectId, packageName } = targetIdentity( projectIdValue, packageNameValue, ); const digest = publicationDigest(publicationDigestValue); return this.#enqueue(() => { const row = this.#authority.client .prepare( `SELECT EXISTS ( SELECT 1 FROM "QingLong3PluginPackageAutomationPublicationHeads" AS head JOIN "QingLong3PluginPackageAutomationPublications" AS publication ON publication.publication_digest = head.publication_digest JOIN "QingLong3PluginPackageInstallHeads" AS install_head ON install_head.project_id = publication.project_id AND install_head.package_name = publication.package_name AND install_head.installation_id = publication.installation_id JOIN "QingLong3PluginPackageInstalls" AS install ON install.installation_id = install_head.installation_id AND install.lock_digest = publication.lock_digest LEFT JOIN "QingLong3PluginPackageLifecycleHeads" AS lifecycle ON lifecycle.project_id = publication.project_id AND lifecycle.package_name = publication.package_name WHERE head.project_id = ? AND head.package_name = ? AND head.publication_digest = ? AND publication.state = 'active' AND install.state = 'active' AND install.active_lock_digest = publication.lock_digest AND ( lifecycle.event_digest IS NULL OR lifecycle.disposition = 'active' ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine WHERE quarantine.project_id = publication.project_id AND quarantine.package_name = publication.package_name AND quarantine.installation_id = publication.installation_id AND quarantine.lock_digest = publication.lock_digest ) ) AS "allowed"`, ) .get(projectId, packageName, digest) as Row | undefined; if (!row || (row.allowed !== 0 && row.allowed !== 1)) { throw new PluginPackageAutomationPublicationUnavailableError(); } return row.allowed === 1; }); } listPendingPage(options: { readonly limit: number; readonly after?: Readonly<{ readonly projectId: string; readonly packageName: string; }>; }): Promise> { if (!options || typeof options !== 'object' || Array.isArray(options)) { return Promise.reject( new InvalidPluginPackageAutomationPublicationError( 'pending page options are invalid', ), ); } assertPluginPackageAutomationPublicationRecoveryPageSize(options.limit); const after = options.after === undefined ? undefined : normalizePluginPackageAutomationPublicationRecoveryCursor( options.after, ); return this.#enqueue(() => { const rows = this.#authority.client .prepare( `SELECT install.project_id AS "projectId", install.package_name AS "packageName" FROM "QingLong3PluginPackageInstallHeads" AS install_head JOIN "QingLong3PluginPackageInstalls" AS install ON install.installation_id = install_head.installation_id JOIN "QingLong3PluginPackageMaterializedRevisions" AS revision ON revision.project_id = install.project_id AND revision.package_name = install.package_name AND revision.generation = install.target_generation AND revision.lock_digest = install.lock_digest LEFT JOIN "QingLong3PluginPackageAutomationPublicationHeads" AS publication ON publication.project_id = install.project_id AND publication.package_name = install.package_name WHERE install.state = 'active' AND install.active_lock_digest = install.lock_digest AND NOT EXISTS ( SELECT 1 FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine WHERE quarantine.project_id = install.project_id AND quarantine.package_name = install.package_name AND quarantine.installation_id = install.installation_id AND quarantine.lock_digest = install.lock_digest ) AND ( publication.generation_digest IS NULL OR publication.generation_digest <> revision.generation_digest ) AND ( ? IS NULL OR install.project_id > ? OR (install.project_id = ? AND install.package_name > ?) ) ORDER BY install.project_id, install.package_name LIMIT ?`, ) .all( after?.projectId ?? null, after?.projectId ?? null, after?.projectId ?? null, after?.packageName ?? null, options.limit + 1, ) as Row[]; const truncated = rows.length > options.limit; const candidates = rows.slice(0, options.limit).map((row) => Object.freeze({ projectId: text(row, 'projectId'), packageName: text(row, 'packageName'), }), ); const last = candidates.at(-1); return Object.freeze({ candidates: Object.freeze(candidates), truncated, ...(truncated && last ? { next: Object.freeze({ projectId: last.projectId, packageName: last.packageName, }), } : {}), }); }); } findCurrentInTransaction( projectIdValue: string, packageNameValue: string, ): Readonly | null { const { projectId, packageName } = targetIdentity( projectIdValue, packageNameValue, ); if (!this.#authority.client.isTransaction) { throw new PluginPackageAutomationPublicationUnavailableError(); } return this.#findCurrent(projectId, packageName); } #publishInTransaction( value: Readonly, securityWithdrawal: boolean, ): Readonly<{ status: 'created' | 'existing'; publication: Readonly; }> { const client = this.#authority.client; if (!client.isTransaction) { throw new PluginPackageAutomationPublicationUnavailableError(); } const publication = normalizePluginPackageAutomationPublication(value); const publicationJson = serialize(publication); const existing = this.#findByDigest(publication.publicationDigest); if (existing) { if (serialize(existing) !== publicationJson) { throw new PluginPackageAutomationPublicationConflictError( 'publication digest is bound to another semantic publication', ); } return Object.freeze({ status: 'existing' as const, publication: existing, }); } const current = this.#findCurrent( publication.target.projectId, publication.target.packageName, ); if (publication.version === 1) { if (current) { throw new PluginPackageAutomationPublicationConflictError( 'Package already has an automation publication head', ); } } else { if (!current) { throw new PluginPackageAutomationPublicationConflictError( 'previous automation publication head is absent', ); } try { assertPluginPackageAutomationPublicationSuccessor(current, publication); } catch (error) { if (error instanceof InvalidPluginPackageAutomationPublicationError) { throw new PluginPackageAutomationPublicationConflictError( 'automation publication does not succeed the current head', ); } throw error; } } const revision = client .prepare( `SELECT revision_digest AS "revisionDigest", project_id AS "projectId", package_name AS "packageName", generation, lock_digest AS "lockDigest" FROM "QingLong3PluginPackageMaterializedRevisions" WHERE generation_digest = ?`, ) .get(publication.target.generationDigest) as Row | undefined; if ( !revision || text(revision, 'revisionDigest') !== publication.target.materializedRevisionDigest || text(revision, 'projectId') !== publication.target.projectId || text(revision, 'packageName') !== publication.target.packageName || integer(revision, 'generation') !== publication.target.generation || text(revision, 'lockDigest') !== publication.target.lockDigest ) { throw new PluginPackageAutomationPublicationConflictError( 'materialized revision fence does not match publication target', ); } if (!securityWithdrawal) { const securityFence = client .prepare( `SELECT EXISTS ( SELECT 1 FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine WHERE quarantine.project_id = ? AND quarantine.package_name = ? AND quarantine.installation_id = ? AND quarantine.lock_digest = ? ) AS "blocked"`, ) .get( publication.target.projectId, publication.target.packageName, publication.target.installationId, publication.target.lockDigest, ) as Row | undefined; if ( !securityFence || (securityFence.blocked !== 0 && securityFence.blocked !== 1) ) { throw new PluginPackageAutomationPublicationUnavailableError(); } if (securityFence.blocked === 1) { throw new PluginPackageAutomationPublicationConflictError( 'quarantined Package generation cannot publish automation', ); } } client .prepare( `INSERT INTO "QingLong3PluginPackageAutomationPublications" ( publication_digest, project_id, package_name, installation_id, lock_digest, generation, generation_digest, materialized_revision_digest, state, version, previous_publication_digest, lifecycle_event_digest, published_at_ms, publication_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( publication.publicationDigest, publication.target.projectId, publication.target.packageName, publication.target.installationId, publication.target.lockDigest, publication.target.generation, publication.target.generationDigest, publication.target.materializedRevisionDigest, publication.state, publication.version, publication.previousPublicationDigest, publication.lifecycleEventDigest, publication.publishedAtMs, publicationJson, ); if (!current) { client .prepare( `INSERT INTO "QingLong3PluginPackageAutomationPublicationHeads" ( project_id, package_name, publication_digest, generation_digest, state, version, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?)`, ) .run( publication.target.projectId, publication.target.packageName, publication.publicationDigest, publication.target.generationDigest, publication.state, publication.version, publication.publishedAtMs, ); } else { const updated = client .prepare( `UPDATE "QingLong3PluginPackageAutomationPublicationHeads" SET publication_digest = ?, generation_digest = ?, state = ?, version = ?, updated_at_ms = ? WHERE project_id = ? AND package_name = ? AND publication_digest = ? AND version = ?`, ) .run( publication.publicationDigest, publication.target.generationDigest, publication.state, publication.version, publication.publishedAtMs, publication.target.projectId, publication.target.packageName, current.publicationDigest, current.version, ); if (updated.changes !== 1) { throw new PluginPackageAutomationPublicationConflictError( 'automation publication head changed', ); } } return Object.freeze({ status: 'created' as const, publication, }); } publishInTransaction( value: Readonly, ): Readonly<{ status: 'created' | 'existing'; publication: Readonly; }> { return this.#publishInTransaction(value, false); } publishSecurityWithdrawalInTransaction( value: Readonly, ): Readonly<{ status: 'created' | 'existing'; publication: Readonly; }> { const publication = normalizePluginPackageAutomationPublication(value); if (publication.state !== 'withdrawn') { throw new PluginPackageAutomationPublicationConflictError( 'security withdrawal must narrow automation state', ); } return this.#publishInTransaction(publication, true); } publish(value: Readonly): Promise< Readonly<{ status: 'created' | 'existing'; publication: Readonly; }> > { return this.#enqueue(() => { const client = this.#authority.client; client.exec('BEGIN IMMEDIATE'); try { const result = this.publishInTransaction(value); client.exec('COMMIT'); return result; } catch (error) { if (client.isTransaction) client.exec('ROLLBACK'); throw error; } }); } }