feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+84
View File
@@ -0,0 +1,84 @@
import { createHash } from 'crypto';
import { DataTypes } from 'sequelize';
import type { Migration } from './types';
type ColumnType = 'string' | 'integer' | 'text' | 'json';
interface LegacyColumn {
table: string;
column: string;
type: ColumnType;
}
const columns: LegacyColumn[] = [
{ table: 'CrontabViews', column: 'filterRelation', type: 'string' },
{ table: 'Subscriptions', column: 'proxy', type: 'string' },
{ table: 'CrontabViews', column: 'type', type: 'integer' },
{ table: 'Subscriptions', column: 'autoAddCron', type: 'integer' },
{ table: 'Subscriptions', column: 'autoDelCron', type: 'integer' },
{ table: 'Crontabs', column: 'sub_id', type: 'integer' },
{ table: 'Crontabs', column: 'extra_schedules', type: 'json' },
{ table: 'Crontabs', column: 'task_before', type: 'text' },
{ table: 'Crontabs', column: 'task_after', type: 'text' },
{ table: 'Crontabs', column: 'log_name', type: 'string' },
{
table: 'Crontabs',
column: 'allow_multiple_instances',
type: 'integer',
},
{ table: 'Crontabs', column: 'work_dir', type: 'string' },
{ table: 'Envs', column: 'isPinned', type: 'integer' },
{ table: 'Envs', column: 'labels', type: 'json' },
];
export const legacyColumnOwnership: readonly {
table: string;
column: string;
type: ColumnType;
}[] = columns;
function dataType(type: ColumnType) {
switch (type) {
case 'integer':
return DataTypes.INTEGER;
case 'text':
return DataTypes.TEXT;
case 'json':
return DataTypes.JSON;
default:
return DataTypes.STRING;
}
}
export const legacyColumnsMigration: Migration = {
id: '0001-legacy-columns',
checksum: createHash('sha256').update(JSON.stringify(columns)).digest('hex'),
async up({ queryInterface, transaction }) {
const descriptions = new Map<string, Record<string, unknown>>();
for (const definition of columns) {
let description = descriptions.get(definition.table);
if (!description) {
description = await queryInterface.describeTable(definition.table);
descriptions.set(definition.table, description);
}
if (
Object.prototype.hasOwnProperty.call(description, definition.column)
) {
continue;
}
await queryInterface.addColumn(
definition.table,
definition.column,
{
type: dataType(definition.type),
allowNull: true,
},
{ transaction },
);
description[definition.column] = {};
}
},
};
+319
View File
@@ -0,0 +1,319 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const RUN_TABLE = 'Runs';
export const RUN_ATTEMPT_TABLE = 'RunAttempts';
export const RUN_EVENT_TABLE = 'RunEvents';
const schemaManifest = {
version: 1,
tables: {
Runs: [
'id',
'project_id',
'task_id',
'task_revision',
'task_name',
'task_snapshot_ref',
'legacy_cron_id',
'parent_run_id',
'retry_of_run_id',
'trigger_id',
'trigger_type',
'execution_origin',
'execution_owner',
'triggered_by',
'request_id',
'scheduled_for_ms',
'status',
'version',
'event_sequence',
'priority',
'idempotency_key',
'input_ref',
'output_ref',
'created_at_ms',
'queued_at_ms',
'started_at_ms',
'finished_at_ms',
'error_code',
'error_summary',
],
RunAttempts: [
'id',
'run_id',
'step_run_id',
'attempt',
'status',
'executor_type',
'worker_id',
'executor_handle',
'pid',
'log_artifact_id',
'lease_token',
'lease_expires_at_ms',
'callback_token_hash',
'callback_sequence',
'created_at_ms',
'started_at_ms',
'finished_at_ms',
'exit_code',
'error_code',
'error_summary',
],
RunEvents: [
'id',
'run_id',
'sequence',
'type',
'dedupe_key',
'actor_type',
'actor_id',
'attempt_id',
'step_run_id',
'payload',
'created_at_ms',
],
},
indexes: [
'runs_project_created_idx',
'runs_task_created_idx',
'runs_status_queued_idx',
'runs_legacy_cron_created_idx',
'runs_project_idempotency_uidx',
'run_attempts_run_attempt_uidx',
'run_attempts_run_status_idx',
'run_attempts_status_created_idx',
'run_attempts_lease_idx',
'run_events_run_sequence_uidx',
'run_events_run_dedupe_uidx',
'run_events_run_created_idx',
],
constraints: [
'runs_version_nonnegative_check',
'runs_event_sequence_nonnegative_check',
'run_attempts_attempt_positive_check',
'run_attempts_callback_sequence_nonnegative_check',
'run_events_sequence_positive_check',
],
};
export const runSchemaManifest = schemaManifest;
export const runSchemaMigration: Migration = {
id: '0002-run-schema',
checksum: createHash('sha256')
.update(JSON.stringify(schemaManifest))
.digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
RUN_TABLE,
{
id: { type: DataTypes.STRING(36), allowNull: false, primaryKey: true },
project_id: { type: DataTypes.STRING(128), allowNull: false },
task_id: { type: DataTypes.STRING(255), allowNull: false },
task_revision: { type: DataTypes.STRING(128), allowNull: false },
task_name: { type: DataTypes.STRING(255), allowNull: true },
task_snapshot_ref: { type: DataTypes.STRING(512), allowNull: true },
legacy_cron_id: { type: DataTypes.INTEGER, allowNull: true },
parent_run_id: { type: DataTypes.STRING(36), allowNull: true },
retry_of_run_id: { type: DataTypes.STRING(36), allowNull: true },
trigger_id: { type: DataTypes.STRING(36), allowNull: true },
trigger_type: { type: DataTypes.STRING(64), allowNull: false },
execution_origin: { type: DataTypes.STRING(64), allowNull: false },
execution_owner: { type: DataTypes.STRING(16), allowNull: false },
triggered_by: { type: DataTypes.STRING(255), allowNull: true },
request_id: { type: DataTypes.STRING(128), allowNull: true },
scheduled_for_ms: { type: DataTypes.BIGINT, allowNull: true },
status: { type: DataTypes.STRING(32), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
event_sequence: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
priority: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
idempotency_key: { type: DataTypes.STRING(255), allowNull: true },
input_ref: { type: DataTypes.STRING(512), allowNull: true },
output_ref: { type: DataTypes.STRING(512), allowNull: true },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
queued_at_ms: { type: DataTypes.BIGINT, allowNull: true },
started_at_ms: { type: DataTypes.BIGINT, allowNull: true },
finished_at_ms: { type: DataTypes.BIGINT, allowNull: true },
error_code: { type: DataTypes.STRING(128), allowNull: true },
error_summary: { type: DataTypes.STRING(1024), allowNull: true },
},
{ transaction },
);
await queryInterface.createTable(
RUN_ATTEMPT_TABLE,
{
id: { type: DataTypes.STRING(36), allowNull: false, primaryKey: true },
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
step_run_id: { type: DataTypes.STRING(36), allowNull: true },
attempt: { type: DataTypes.INTEGER, allowNull: false },
status: { type: DataTypes.STRING(32), allowNull: false },
executor_type: { type: DataTypes.STRING(64), allowNull: false },
worker_id: { type: DataTypes.STRING(128), allowNull: true },
executor_handle: { type: DataTypes.TEXT, allowNull: true },
pid: { type: DataTypes.INTEGER, allowNull: true },
log_artifact_id: { type: DataTypes.STRING(36), allowNull: true },
lease_token: { type: DataTypes.STRING(128), allowNull: true },
lease_expires_at_ms: { type: DataTypes.BIGINT, allowNull: true },
callback_token_hash: { type: DataTypes.STRING(128), allowNull: true },
callback_sequence: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
started_at_ms: { type: DataTypes.BIGINT, allowNull: true },
finished_at_ms: { type: DataTypes.BIGINT, allowNull: true },
exit_code: { type: DataTypes.INTEGER, allowNull: true },
error_code: { type: DataTypes.STRING(128), allowNull: true },
error_summary: { type: DataTypes.STRING(1024), allowNull: true },
},
{ transaction },
);
await queryInterface.createTable(
RUN_EVENT_TABLE,
{
id: { type: DataTypes.STRING(36), allowNull: false, primaryKey: true },
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
sequence: { type: DataTypes.INTEGER, allowNull: false },
type: { type: DataTypes.STRING(128), allowNull: false },
dedupe_key: { type: DataTypes.STRING(255), allowNull: true },
actor_type: { type: DataTypes.STRING(64), allowNull: false },
actor_id: { type: DataTypes.STRING(255), allowNull: true },
attempt_id: {
type: DataTypes.STRING(36),
allowNull: true,
references: { model: RUN_ATTEMPT_TABLE, key: 'id' },
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
step_run_id: { type: DataTypes.STRING(36), allowNull: true },
payload: { type: DataTypes.JSON, allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(RUN_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 0 } },
name: 'runs_version_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(RUN_TABLE, {
fields: ['event_sequence'],
type: 'check',
where: { event_sequence: { [Op.gte]: 0 } },
name: 'runs_event_sequence_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(RUN_ATTEMPT_TABLE, {
fields: ['attempt'],
type: 'check',
where: { attempt: { [Op.gte]: 1 } },
name: 'run_attempts_attempt_positive_check',
transaction,
});
await queryInterface.addConstraint(RUN_ATTEMPT_TABLE, {
fields: ['callback_sequence'],
type: 'check',
where: { callback_sequence: { [Op.gte]: 0 } },
name: 'run_attempts_callback_sequence_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(RUN_EVENT_TABLE, {
fields: ['sequence'],
type: 'check',
where: { sequence: { [Op.gte]: 1 } },
name: 'run_events_sequence_positive_check',
transaction,
});
await queryInterface.addIndex(RUN_TABLE, ['project_id', 'created_at_ms'], {
name: 'runs_project_created_idx',
transaction,
});
await queryInterface.addIndex(RUN_TABLE, ['task_id', 'created_at_ms'], {
name: 'runs_task_created_idx',
transaction,
});
await queryInterface.addIndex(RUN_TABLE, ['status', 'queued_at_ms'], {
name: 'runs_status_queued_idx',
transaction,
});
await queryInterface.addIndex(
RUN_TABLE,
['legacy_cron_id', 'created_at_ms'],
{ name: 'runs_legacy_cron_created_idx', transaction },
);
await queryInterface.addIndex(
RUN_TABLE,
['project_id', 'idempotency_key'],
{
name: 'runs_project_idempotency_uidx',
unique: true,
transaction,
},
);
await queryInterface.addIndex(RUN_ATTEMPT_TABLE, ['run_id', 'attempt'], {
name: 'run_attempts_run_attempt_uidx',
unique: true,
transaction,
});
await queryInterface.addIndex(RUN_ATTEMPT_TABLE, ['run_id', 'status'], {
name: 'run_attempts_run_status_idx',
transaction,
});
await queryInterface.addIndex(
RUN_ATTEMPT_TABLE,
['status', 'created_at_ms'],
{ name: 'run_attempts_status_created_idx', transaction },
);
await queryInterface.addIndex(RUN_ATTEMPT_TABLE, ['lease_expires_at_ms'], {
name: 'run_attempts_lease_idx',
transaction,
});
await queryInterface.addIndex(RUN_EVENT_TABLE, ['run_id', 'sequence'], {
name: 'run_events_run_sequence_uidx',
unique: true,
transaction,
});
await queryInterface.addIndex(RUN_EVENT_TABLE, ['run_id', 'dedupe_key'], {
name: 'run_events_run_dedupe_uidx',
unique: true,
transaction,
});
await queryInterface.addIndex(
RUN_EVENT_TABLE,
['run_id', 'created_at_ms'],
{ name: 'run_events_run_created_idx', transaction },
);
},
};
@@ -0,0 +1,71 @@
import { createHash } from 'crypto';
import { DataTypes } from 'sequelize';
import type { Migration } from './types';
export const RUNNING_INSTANCE_TABLE = 'RunningInstances';
export const RUNNING_INSTANCE_RUN_INDEX = 'running_instances_run_started_idx';
export const RUNNING_INSTANCE_ATTEMPT_INDEX = 'running_instances_attempt_uidx';
const manifest = {
table: RUNNING_INSTANCE_TABLE,
columns: {
run_id: 'varchar(36) null',
attempt_id: 'varchar(36) null',
},
indexes: [
`${RUNNING_INSTANCE_RUN_INDEX}(run_id,started_at)`,
`${RUNNING_INSTANCE_ATTEMPT_INDEX}(attempt_id) unique`,
],
};
export const runningInstanceRunReferenceManifest = manifest;
export const runningInstanceRunReferenceMigration: Migration = {
id: '0003-running-instance-run-reference',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
const tables = new Set(await queryInterface.showAllTables());
if (!tables.has(RUNNING_INSTANCE_TABLE)) return;
const columns = await queryInterface.describeTable(RUNNING_INSTANCE_TABLE);
if (!columns.run_id) {
await queryInterface.addColumn(
RUNNING_INSTANCE_TABLE,
'run_id',
{ type: DataTypes.STRING(36), allowNull: true },
{ transaction },
);
}
if (!columns.attempt_id) {
await queryInterface.addColumn(
RUNNING_INSTANCE_TABLE,
'attempt_id',
{ type: DataTypes.STRING(36), allowNull: true },
{ transaction },
);
}
const currentIndexes = (await queryInterface.showIndex(
RUNNING_INSTANCE_TABLE,
{ transaction },
)) as Array<{ name: string }>;
const indexes = new Set(currentIndexes.map((index) => index.name));
if (!indexes.has(RUNNING_INSTANCE_RUN_INDEX)) {
await queryInterface.addIndex(
RUNNING_INSTANCE_TABLE,
['run_id', 'started_at'],
{
name: RUNNING_INSTANCE_RUN_INDEX,
transaction,
},
);
}
if (!indexes.has(RUNNING_INSTANCE_ATTEMPT_INDEX)) {
await queryInterface.addIndex(RUNNING_INSTANCE_TABLE, ['attempt_id'], {
name: RUNNING_INSTANCE_ATTEMPT_INDEX,
unique: true,
transaction,
});
}
},
};
@@ -0,0 +1,60 @@
import { createHash } from 'crypto';
import { DataTypes } from 'sequelize';
import { RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const RUN_CANCELLATION_REQUEST_INDEX =
'runs_status_cancel_requested_idx';
const manifest = {
table: RUN_TABLE,
columns: {
cancel_requested_at_ms: 'bigint null',
cancel_reason: 'varchar(32) null',
},
indexes: [`${RUN_CANCELLATION_REQUEST_INDEX}(status,cancel_requested_at_ms)`],
};
export const runCancellationRequestManifest = manifest;
export const runCancellationRequestMigration: Migration = {
id: '0004-run-cancellation-request',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
const tables = new Set(await queryInterface.showAllTables());
if (!tables.has(RUN_TABLE)) return;
const columns = await queryInterface.describeTable(RUN_TABLE);
if (!columns.cancel_requested_at_ms) {
await queryInterface.addColumn(
RUN_TABLE,
'cancel_requested_at_ms',
{ type: DataTypes.BIGINT, allowNull: true },
{ transaction },
);
}
if (!columns.cancel_reason) {
await queryInterface.addColumn(
RUN_TABLE,
'cancel_reason',
{ type: DataTypes.STRING(32), allowNull: true },
{ transaction },
);
}
const currentIndexes = (await queryInterface.showIndex(RUN_TABLE, {
transaction,
})) as Array<{ name: string }>;
if (
!currentIndexes.some(
(index) => index.name === RUN_CANCELLATION_REQUEST_INDEX,
)
) {
await queryInterface.addIndex(
RUN_TABLE,
['status', 'cancel_requested_at_ms'],
{ name: RUN_CANCELLATION_REQUEST_INDEX, transaction },
);
}
},
};
@@ -0,0 +1,106 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { RUN_ATTEMPT_TABLE, RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const RUN_CANCELLATION_DISPATCH_TABLE = 'RunCancellationDispatches';
export const RUN_CANCELLATION_DISPATCH_DUE_INDEX =
'run_cancel_dispatch_due_idx';
export const RUN_CANCELLATION_DISPATCH_LEASE_INDEX =
'run_cancel_dispatch_lease_idx';
const manifest = {
table: RUN_CANCELLATION_DISPATCH_TABLE,
columns: [
'run_id',
'attempt_id',
'status',
'version',
'dispatch_count',
'next_attempt_at_ms',
'lease_owner',
'lease_token',
'lease_expires_at_ms',
'last_result',
'last_dispatched_at_ms',
'created_at_ms',
'updated_at_ms',
],
indexes: [
`${RUN_CANCELLATION_DISPATCH_DUE_INDEX}(status,next_attempt_at_ms)`,
`${RUN_CANCELLATION_DISPATCH_LEASE_INDEX}(lease_expires_at_ms)`,
],
constraints: [
'run_cancel_dispatch_version_nonnegative_check',
'run_cancel_dispatch_count_nonnegative_check',
],
};
export const runCancellationDispatchManifest = manifest;
export const runCancellationDispatchMigration: Migration = {
id: '0005-run-cancellation-dispatch',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
RUN_CANCELLATION_DISPATCH_TABLE,
{
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
primaryKey: true,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
attempt_id: {
type: DataTypes.STRING(36),
allowNull: false,
references: { model: RUN_ATTEMPT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
status: { type: DataTypes.STRING(32), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
dispatch_count: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
next_attempt_at_ms: { type: DataTypes.BIGINT, allowNull: true },
lease_owner: { type: DataTypes.STRING(128), allowNull: true },
lease_token: { type: DataTypes.STRING(128), allowNull: true },
lease_expires_at_ms: { type: DataTypes.BIGINT, allowNull: true },
last_result: { type: DataTypes.STRING(64), allowNull: true },
last_dispatched_at_ms: { type: DataTypes.BIGINT, allowNull: true },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(RUN_CANCELLATION_DISPATCH_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 0 } },
name: 'run_cancel_dispatch_version_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(RUN_CANCELLATION_DISPATCH_TABLE, {
fields: ['dispatch_count'],
type: 'check',
where: { dispatch_count: { [Op.gte]: 0 } },
name: 'run_cancel_dispatch_count_nonnegative_check',
transaction,
});
await queryInterface.addIndex(
RUN_CANCELLATION_DISPATCH_TABLE,
['status', 'next_attempt_at_ms'],
{ name: RUN_CANCELLATION_DISPATCH_DUE_INDEX, transaction },
);
await queryInterface.addIndex(
RUN_CANCELLATION_DISPATCH_TABLE,
['lease_expires_at_ms'],
{ name: RUN_CANCELLATION_DISPATCH_LEASE_INDEX, transaction },
);
},
};
@@ -0,0 +1,48 @@
import { createHash } from 'crypto';
import { DataTypes } from 'sequelize';
import { RUN_ATTEMPT_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const RUN_ATTEMPT_DEADLINE_INDEX = 'run_attempt_status_deadline_idx';
const manifest = {
table: RUN_ATTEMPT_TABLE,
columns: {
deadline_at_ms: 'bigint null',
},
indexes: [`${RUN_ATTEMPT_DEADLINE_INDEX}(status,deadline_at_ms,id)`],
};
export const runAttemptDeadlineManifest = manifest;
export const runAttemptDeadlineMigration: Migration = {
id: '0006-run-attempt-deadline',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
const tables = new Set(await queryInterface.showAllTables());
if (!tables.has(RUN_ATTEMPT_TABLE)) return;
const columns = await queryInterface.describeTable(RUN_ATTEMPT_TABLE);
if (!columns.deadline_at_ms) {
await queryInterface.addColumn(
RUN_ATTEMPT_TABLE,
'deadline_at_ms',
{ type: DataTypes.BIGINT, allowNull: true },
{ transaction },
);
}
const currentIndexes = (await queryInterface.showIndex(RUN_ATTEMPT_TABLE, {
transaction,
})) as Array<{ name: string }>;
if (
!currentIndexes.some((index) => index.name === RUN_ATTEMPT_DEADLINE_INDEX)
) {
await queryInterface.addIndex(
RUN_ATTEMPT_TABLE,
['status', 'deadline_at_ms', 'id'],
{ name: RUN_ATTEMPT_DEADLINE_INDEX, transaction },
);
}
},
};
@@ -0,0 +1,102 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { RUN_ATTEMPT_TABLE, RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const COMPLETION_RECEIPT_JOURNAL_TABLE = 'CompletionReceiptJournals';
export const COMPLETION_RECEIPT_JOURNAL_SCAN_INDEX =
'completion_receipt_journal_scan_idx';
export const COMPLETION_RECEIPT_JOURNAL_PURGE_INDEX =
'completion_receipt_journal_purge_idx';
const manifest = {
table: COMPLETION_RECEIPT_JOURNAL_TABLE,
columns: [
'attempt_id',
'run_id',
'state',
'quarantine_ref',
'purge_after_ms',
'registered_at_ms',
'updated_at_ms',
],
indexes: [
`${COMPLETION_RECEIPT_JOURNAL_SCAN_INDEX}(state,updated_at_ms,attempt_id)`,
`${COMPLETION_RECEIPT_JOURNAL_PURGE_INDEX}(state,purge_after_ms,attempt_id)`,
],
constraints: [
'completion_receipt_journal_state_check',
'completion_receipt_journal_registered_nonnegative_check',
'completion_receipt_journal_updated_nonnegative_check',
],
};
export const completionReceiptJournalManifest = manifest;
export const completionReceiptJournalMigration: Migration = {
id: '0007-completion-receipt-journal',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
COMPLETION_RECEIPT_JOURNAL_TABLE,
{
attempt_id: {
type: DataTypes.STRING(36),
allowNull: false,
primaryKey: true,
references: { model: RUN_ATTEMPT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
state: {
type: DataTypes.STRING(16),
allowNull: false,
defaultValue: 'pending',
},
quarantine_ref: { type: DataTypes.STRING(255), allowNull: true },
purge_after_ms: { type: DataTypes.BIGINT, allowNull: true },
registered_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(COMPLETION_RECEIPT_JOURNAL_TABLE, {
fields: ['state'],
type: 'check',
where: { state: { [Op.in]: ['pending', 'quarantined'] } },
name: 'completion_receipt_journal_state_check',
transaction,
});
await queryInterface.addConstraint(COMPLETION_RECEIPT_JOURNAL_TABLE, {
fields: ['registered_at_ms'],
type: 'check',
where: { registered_at_ms: { [Op.gte]: 0 } },
name: 'completion_receipt_journal_registered_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(COMPLETION_RECEIPT_JOURNAL_TABLE, {
fields: ['updated_at_ms'],
type: 'check',
where: { updated_at_ms: { [Op.gte]: 0 } },
name: 'completion_receipt_journal_updated_nonnegative_check',
transaction,
});
await queryInterface.addIndex(
COMPLETION_RECEIPT_JOURNAL_TABLE,
['state', 'updated_at_ms', 'attempt_id'],
{ name: COMPLETION_RECEIPT_JOURNAL_SCAN_INDEX, transaction },
);
await queryInterface.addIndex(
COMPLETION_RECEIPT_JOURNAL_TABLE,
['state', 'purge_after_ms', 'attempt_id'],
{ name: COMPLETION_RECEIPT_JOURNAL_PURGE_INDEX, transaction },
);
},
};
+110
View File
@@ -0,0 +1,110 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const WORKER_REGISTRY_TABLE = 'Workers';
export const WORKER_REGISTRY_LEASE_INDEX = 'workers_lease_idx';
export const WORKER_REGISTRY_CAPACITY_INDEX = 'workers_capacity_idx';
const manifest = {
table: WORKER_REGISTRY_TABLE,
columns: [
'id',
'session_id',
'generation',
'status',
'version',
'capabilities_json',
'capabilities_hash',
'max_concurrent_runs',
'available_slots',
'registered_at_ms',
'last_heartbeat_at_ms',
'lease_expires_at_ms',
'updated_at_ms',
],
indexes: [
`${WORKER_REGISTRY_LEASE_INDEX}(status,lease_expires_at_ms,id)`,
`${WORKER_REGISTRY_CAPACITY_INDEX}(status,available_slots,lease_expires_at_ms,id)`,
],
constraints: [
'workers_generation_positive_check',
'workers_version_nonnegative_check',
'workers_max_concurrency_positive_check',
'workers_available_slots_nonnegative_check',
'workers_status_check',
],
};
export const workerRegistryManifest = manifest;
export const workerRegistryMigration: Migration = {
id: '0008-worker-registry',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
WORKER_REGISTRY_TABLE,
{
id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
session_id: { type: DataTypes.STRING(36), allowNull: false },
generation: { type: DataTypes.INTEGER, allowNull: false },
status: { type: DataTypes.STRING(16), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
capabilities_json: { type: DataTypes.TEXT, allowNull: false },
capabilities_hash: { type: DataTypes.STRING(64), allowNull: false },
max_concurrent_runs: { type: DataTypes.INTEGER, allowNull: false },
available_slots: { type: DataTypes.INTEGER, allowNull: false },
registered_at_ms: { type: DataTypes.BIGINT, allowNull: false },
last_heartbeat_at_ms: { type: DataTypes.BIGINT, allowNull: false },
lease_expires_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(WORKER_REGISTRY_TABLE, {
fields: ['generation'],
type: 'check',
where: { generation: { [Op.gt]: 0 } },
name: 'workers_generation_positive_check',
transaction,
});
await queryInterface.addConstraint(WORKER_REGISTRY_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 0 } },
name: 'workers_version_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(WORKER_REGISTRY_TABLE, {
fields: ['max_concurrent_runs'],
type: 'check',
where: { max_concurrent_runs: { [Op.gt]: 0 } },
name: 'workers_max_concurrency_positive_check',
transaction,
});
await queryInterface.addConstraint(WORKER_REGISTRY_TABLE, {
fields: ['available_slots'],
type: 'check',
where: { available_slots: { [Op.gte]: 0 } },
name: 'workers_available_slots_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(WORKER_REGISTRY_TABLE, {
fields: ['status'],
type: 'check',
where: { status: { [Op.in]: ['online', 'draining', 'offline'] } },
name: 'workers_status_check',
transaction,
});
await queryInterface.addIndex(
WORKER_REGISTRY_TABLE,
['status', 'lease_expires_at_ms', 'id'],
{ name: WORKER_REGISTRY_LEASE_INDEX, transaction },
);
await queryInterface.addIndex(
WORKER_REGISTRY_TABLE,
['status', 'available_slots', 'lease_expires_at_ms', 'id'],
{ name: WORKER_REGISTRY_CAPACITY_INDEX, transaction },
);
},
};
+143
View File
@@ -0,0 +1,143 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { RUN_ATTEMPT_TABLE, RUN_TABLE } from './0002-run-schema';
import { WORKER_REGISTRY_TABLE } from './0008-worker-registry';
import type { Migration } from './types';
export const RUN_DISPATCH_LEASE_TABLE = 'RunDispatchLeases';
export const RUN_DISPATCH_LEASE_EXPIRY_INDEX = 'run_dispatch_leases_expiry_idx';
export const RUN_DISPATCH_LEASE_WORKER_INDEX = 'run_dispatch_leases_worker_idx';
export const RUN_DISPATCH_LEASE_TOKEN_INDEX = 'run_dispatch_leases_token_uidx';
const manifest = {
table: RUN_DISPATCH_LEASE_TABLE,
columns: [
'attempt_id',
'run_id',
'status',
'version',
'lease_generation',
'worker_id',
'worker_session_id',
'worker_generation',
'lease_token',
'acquired_at_ms',
'renewed_at_ms',
'expires_at_ms',
'released_at_ms',
'release_reason',
'completed_at_ms',
'updated_at_ms',
],
indexes: [
`${RUN_DISPATCH_LEASE_EXPIRY_INDEX}(status,expires_at_ms,attempt_id)`,
`${RUN_DISPATCH_LEASE_WORKER_INDEX}(worker_id,worker_session_id,worker_generation,status,expires_at_ms,attempt_id)`,
`${RUN_DISPATCH_LEASE_TOKEN_INDEX}(lease_token)`,
],
constraints: [
'run_dispatch_leases_status_check',
'run_dispatch_leases_version_nonnegative_check',
'run_dispatch_leases_generation_positive_check',
'run_dispatch_leases_worker_generation_positive_check',
],
};
export const runDispatchLeaseManifest = manifest;
export const runDispatchLeaseMigration: Migration = {
id: '0009-run-dispatch-lease',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
RUN_DISPATCH_LEASE_TABLE,
{
attempt_id: {
type: DataTypes.STRING(36),
allowNull: false,
primaryKey: true,
references: { model: RUN_ATTEMPT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
status: { type: DataTypes.STRING(16), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
lease_generation: { type: DataTypes.INTEGER, allowNull: false },
worker_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: WORKER_REGISTRY_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
worker_session_id: { type: DataTypes.STRING(36), allowNull: false },
worker_generation: { type: DataTypes.INTEGER, allowNull: false },
lease_token: { type: DataTypes.STRING(128), allowNull: false },
acquired_at_ms: { type: DataTypes.BIGINT, allowNull: false },
renewed_at_ms: { type: DataTypes.BIGINT, allowNull: false },
expires_at_ms: { type: DataTypes.BIGINT, allowNull: false },
released_at_ms: { type: DataTypes.BIGINT, allowNull: true },
release_reason: { type: DataTypes.STRING(32), allowNull: true },
completed_at_ms: { type: DataTypes.BIGINT, allowNull: true },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(RUN_DISPATCH_LEASE_TABLE, {
fields: ['status'],
type: 'check',
where: { status: { [Op.in]: ['leased', 'released', 'completed'] } },
name: 'run_dispatch_leases_status_check',
transaction,
});
await queryInterface.addConstraint(RUN_DISPATCH_LEASE_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 0 } },
name: 'run_dispatch_leases_version_nonnegative_check',
transaction,
});
await queryInterface.addConstraint(RUN_DISPATCH_LEASE_TABLE, {
fields: ['lease_generation'],
type: 'check',
where: { lease_generation: { [Op.gt]: 0 } },
name: 'run_dispatch_leases_generation_positive_check',
transaction,
});
await queryInterface.addConstraint(RUN_DISPATCH_LEASE_TABLE, {
fields: ['worker_generation'],
type: 'check',
where: { worker_generation: { [Op.gt]: 0 } },
name: 'run_dispatch_leases_worker_generation_positive_check',
transaction,
});
await queryInterface.addIndex(
RUN_DISPATCH_LEASE_TABLE,
['status', 'expires_at_ms', 'attempt_id'],
{ name: RUN_DISPATCH_LEASE_EXPIRY_INDEX, transaction },
);
await queryInterface.addIndex(
RUN_DISPATCH_LEASE_TABLE,
[
'worker_id',
'worker_session_id',
'worker_generation',
'status',
'expires_at_ms',
'attempt_id',
],
{ name: RUN_DISPATCH_LEASE_WORKER_INDEX, transaction },
);
await queryInterface.addIndex(RUN_DISPATCH_LEASE_TABLE, ['lease_token'], {
name: RUN_DISPATCH_LEASE_TOKEN_INDEX,
unique: true,
transaction,
});
},
};
@@ -0,0 +1,40 @@
import { createHash } from 'crypto';
import { Op } from 'sequelize';
import { RUN_ATTEMPT_TABLE, RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const RUN_DISPATCH_CANDIDATE_RUN_INDEX = 'runs_dispatch_candidates_idx';
export const RUN_DISPATCH_CANDIDATE_ATTEMPT_INDEX =
'run_attempts_dispatch_candidates_idx';
const manifest = {
indexes: [
`${RUN_DISPATCH_CANDIDATE_RUN_INDEX}(priority DESC,queued_at_ms,id) WHERE runtime queued/dispatching uncancelled`,
`${RUN_DISPATCH_CANDIDATE_ATTEMPT_INDEX}(status,run_id,created_at_ms,id)`,
],
};
export const runDispatchCandidateManifest = manifest;
export const runDispatchCandidateMigration: Migration = {
id: '0010-run-dispatch-candidates',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.addIndex(RUN_TABLE, {
fields: [{ name: 'priority', order: 'DESC' }, 'queued_at_ms', 'id'],
name: RUN_DISPATCH_CANDIDATE_RUN_INDEX,
where: {
execution_owner: 'runtime',
status: { [Op.in]: ['queued', 'dispatching'] },
cancel_requested_at_ms: null,
queued_at_ms: { [Op.ne]: null },
},
transaction,
});
await queryInterface.addIndex(
RUN_ATTEMPT_TABLE,
['status', 'run_id', 'created_at_ms', 'id'],
{ name: RUN_DISPATCH_CANDIDATE_ATTEMPT_INDEX, transaction },
);
},
};
+115
View File
@@ -0,0 +1,115 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const RUN_RETRY_POLICY_TABLE = 'RunRetryPolicies';
export const RUN_RETRY_POLICY_DUE_INDEX = 'run_retry_policies_due_idx';
export const RUN_LOST_RETRY_INDEX = 'runs_lost_retry_idx';
const manifest = {
table: RUN_RETRY_POLICY_TABLE,
columns: [
'run_id',
'max_attempts',
'retry_on_lost',
'safety',
'backoff_base_ms',
'backoff_max_ms',
'next_attempt_at_ms',
'version',
'created_at_ms',
'updated_at_ms',
],
indexes: [
`${RUN_LOST_RETRY_INDEX}(execution_owner,status,id)`,
`${RUN_RETRY_POLICY_DUE_INDEX}(next_attempt_at_ms,run_id) WHERE next_attempt_at_ms IS NOT NULL`,
],
constraints: [
'run_retry_policies_max_attempts_check',
'run_retry_policies_backoff_base_check',
'run_retry_policies_backoff_max_check',
'run_retry_policies_version_check',
],
};
export const runRetryPolicyManifest = manifest;
export const runRetryPolicyMigration: Migration = {
id: '0011-run-retry-policy',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
RUN_RETRY_POLICY_TABLE,
{
run_id: {
type: DataTypes.STRING(36),
allowNull: false,
primaryKey: true,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
max_attempts: { type: DataTypes.INTEGER, allowNull: false },
retry_on_lost: { type: DataTypes.BOOLEAN, allowNull: false },
safety: { type: DataTypes.STRING(16), allowNull: false },
backoff_base_ms: { type: DataTypes.BIGINT, allowNull: false },
backoff_max_ms: { type: DataTypes.BIGINT, allowNull: false },
next_attempt_at_ms: { type: DataTypes.BIGINT, allowNull: true },
version: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(RUN_RETRY_POLICY_TABLE, {
fields: ['max_attempts'],
type: 'check',
where: { max_attempts: { [Op.between]: [1, 16] } },
name: 'run_retry_policies_max_attempts_check',
transaction,
});
await queryInterface.addConstraint(RUN_RETRY_POLICY_TABLE, {
fields: ['backoff_base_ms'],
type: 'check',
where: { backoff_base_ms: { [Op.gte]: 0 } },
name: 'run_retry_policies_backoff_base_check',
transaction,
});
await queryInterface.addConstraint(RUN_RETRY_POLICY_TABLE, {
fields: ['backoff_max_ms'],
type: 'check',
where: { backoff_max_ms: { [Op.gte]: 0 } },
name: 'run_retry_policies_backoff_max_check',
transaction,
});
await queryInterface.addConstraint(RUN_RETRY_POLICY_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 0 } },
name: 'run_retry_policies_version_check',
transaction,
});
await queryInterface.addIndex(
RUN_TABLE,
['execution_owner', 'status', 'id'],
{
name: RUN_LOST_RETRY_INDEX,
transaction,
},
);
await queryInterface.addIndex(
RUN_RETRY_POLICY_TABLE,
['next_attempt_at_ms', 'run_id'],
{
name: RUN_RETRY_POLICY_DUE_INDEX,
where: { next_attempt_at_ms: { [Op.ne]: null } },
transaction,
},
);
},
};
@@ -0,0 +1,72 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const TASK_EXECUTION_REVISION_TABLE = 'TaskExecutionRevisions';
export const TASK_EXECUTION_REVISION_CREATED_INDEX =
'task_execution_revisions_created_idx';
const manifest = {
table: TASK_EXECUTION_REVISION_TABLE,
columns: [
'project_id',
'task_id',
'task_revision',
'executor_type',
'execution_template',
'context_ref',
'content_digest',
'created_at_ms',
],
indexes: [
`${TASK_EXECUTION_REVISION_CREATED_INDEX}(project_id,created_at_ms,task_id,task_revision)`,
],
constraints: ['task_execution_revisions_created_at_check'],
};
export const taskExecutionRevisionManifest = manifest;
export const taskExecutionRevisionMigration: Migration = {
id: '0012-task-execution-revisions',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
TASK_EXECUTION_REVISION_TABLE,
{
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
},
task_id: {
type: DataTypes.STRING(255),
allowNull: false,
primaryKey: true,
},
task_revision: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
},
executor_type: { type: DataTypes.STRING(64), allowNull: false },
execution_template: { type: DataTypes.TEXT, allowNull: false },
context_ref: { type: DataTypes.STRING(512), allowNull: false },
content_digest: { type: DataTypes.STRING(64), allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(TASK_EXECUTION_REVISION_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'task_execution_revisions_created_at_check',
transaction,
});
await queryInterface.addIndex(
TASK_EXECUTION_REVISION_TABLE,
['project_id', 'created_at_ms', 'task_id', 'task_revision'],
{ name: TASK_EXECUTION_REVISION_CREATED_INDEX, transaction },
);
},
};
@@ -0,0 +1,57 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE =
'LocalExecutionContextRecipes';
export const LOCAL_EXECUTION_CONTEXT_RECIPE_CREATED_INDEX =
'local_execution_context_recipes_created_idx';
const manifest = {
table: LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
columns: [
'context_ref',
'environment_recipe',
'content_digest',
'created_at_ms',
],
indexes: [
`${LOCAL_EXECUTION_CONTEXT_RECIPE_CREATED_INDEX}(created_at_ms,context_ref)`,
],
constraints: ['local_execution_context_recipes_created_at_check'],
};
export const localExecutionContextRecipeManifest = manifest;
export const localExecutionContextRecipeMigration: Migration = {
id: '0013-local-execution-context-recipes',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
{
context_ref: {
type: DataTypes.STRING(512),
allowNull: false,
primaryKey: true,
},
environment_recipe: { type: DataTypes.TEXT, allowNull: false },
content_digest: { type: DataTypes.STRING(64), allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'local_execution_context_recipes_created_at_check',
transaction,
});
await queryInterface.addIndex(
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
['created_at_ms', 'context_ref'],
{ name: LOCAL_EXECUTION_CONTEXT_RECIPE_CREATED_INDEX, transaction },
);
},
};
@@ -0,0 +1,99 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const LOCAL_SECRET_ENVELOPE_TABLE = 'LocalSecretEnvelopes';
export const LOCAL_SECRET_MUTATION_INDEX = 'local_secret_mutation_idx';
export const LOCAL_SECRET_CURRENT_INDEX = 'local_secret_current_idx';
export const LOCAL_SECRET_KEY_INDEX = 'local_secret_key_idx';
const manifest = {
table: LOCAL_SECRET_ENVELOPE_TABLE,
columns: [
'project_id',
'secret_name',
'version',
'mutation_id',
'key_id',
'algorithm',
'nonce',
'ciphertext',
'auth_tag',
'created_at_ms',
],
indexes: [
`${LOCAL_SECRET_MUTATION_INDEX}(project_id,secret_name,mutation_id) UNIQUE`,
`${LOCAL_SECRET_CURRENT_INDEX}(project_id,secret_name,version DESC)`,
`${LOCAL_SECRET_KEY_INDEX}(key_id,project_id,secret_name,version)`,
],
constraints: [
'local_secret_envelopes_version_check',
'local_secret_envelopes_created_at_check',
],
};
export const localSecretEnvelopeManifest = manifest;
export const localSecretEnvelopeMigration: Migration = {
id: '0014-local-secret-envelopes',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
LOCAL_SECRET_ENVELOPE_TABLE,
{
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
},
secret_name: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
},
mutation_id: { type: DataTypes.STRING(64), allowNull: false },
key_id: { type: DataTypes.STRING(128), allowNull: false },
algorithm: { type: DataTypes.STRING(32), allowNull: false },
nonce: { type: DataTypes.BLOB, allowNull: false },
ciphertext: { type: DataTypes.BLOB, allowNull: false },
auth_tag: { type: DataTypes.BLOB, allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(LOCAL_SECRET_ENVELOPE_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 1 } },
name: 'local_secret_envelopes_version_check',
transaction,
});
await queryInterface.addConstraint(LOCAL_SECRET_ENVELOPE_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'local_secret_envelopes_created_at_check',
transaction,
});
await queryInterface.addIndex(
LOCAL_SECRET_ENVELOPE_TABLE,
['project_id', 'secret_name', 'mutation_id'],
{ name: LOCAL_SECRET_MUTATION_INDEX, unique: true, transaction },
);
await queryInterface.addIndex(LOCAL_SECRET_ENVELOPE_TABLE, {
fields: ['project_id', 'secret_name', { name: 'version', order: 'DESC' }],
name: LOCAL_SECRET_CURRENT_INDEX,
transaction,
});
await queryInterface.addIndex(
LOCAL_SECRET_ENVELOPE_TABLE,
['key_id', 'project_id', 'secret_name', 'version'],
{ name: LOCAL_SECRET_KEY_INDEX, transaction },
);
},
};
@@ -0,0 +1,94 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { RUN_ATTEMPT_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const LOCAL_ARTIFACT_RETENTION_TABLE = 'LocalArtifactRetentions';
export const LOCAL_ARTIFACT_RETENTION_RECORDED_INDEX =
'local_artifact_retention_recorded_idx';
export const RUN_ATTEMPT_ARTIFACT_RETENTION_INDEX =
'run_attempts_artifact_retention_idx';
const manifest = {
table: LOCAL_ARTIFACT_RETENTION_TABLE,
columns: [
'attempt_id',
'log_artifact_id',
'finished_at_ms',
'eligible_at_ms',
'disposition',
'bytes_reclaimed',
'recorded_at_ms',
],
indexes: [
`${LOCAL_ARTIFACT_RETENTION_RECORDED_INDEX}(recorded_at_ms,attempt_id)`,
`${RUN_ATTEMPT_ARTIFACT_RETENTION_INDEX}(status,finished_at_ms,id)`,
],
constraints: [
'local_artifact_retention_disposition_check',
'local_artifact_retention_finished_check',
'local_artifact_retention_eligible_check',
'local_artifact_retention_bytes_check',
'local_artifact_retention_recorded_check',
],
};
export const localArtifactRetentionManifest = manifest;
export const localArtifactRetentionMigration: Migration = {
id: '0015-local-artifact-retention',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
LOCAL_ARTIFACT_RETENTION_TABLE,
{
attempt_id: {
type: DataTypes.STRING(36),
allowNull: false,
primaryKey: true,
references: { model: RUN_ATTEMPT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
log_artifact_id: { type: DataTypes.STRING(36), allowNull: false },
finished_at_ms: { type: DataTypes.BIGINT, allowNull: false },
eligible_at_ms: { type: DataTypes.BIGINT, allowNull: false },
disposition: { type: DataTypes.STRING(16), allowNull: false },
bytes_reclaimed: { type: DataTypes.BIGINT, allowNull: false },
recorded_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(LOCAL_ARTIFACT_RETENTION_TABLE, {
fields: ['disposition'],
type: 'check',
where: { disposition: { [Op.in]: ['deleted', 'already_absent'] } },
name: 'local_artifact_retention_disposition_check',
transaction,
});
for (const [field, name] of [
['finished_at_ms', 'local_artifact_retention_finished_check'],
['eligible_at_ms', 'local_artifact_retention_eligible_check'],
['bytes_reclaimed', 'local_artifact_retention_bytes_check'],
['recorded_at_ms', 'local_artifact_retention_recorded_check'],
] as const) {
await queryInterface.addConstraint(LOCAL_ARTIFACT_RETENTION_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.gte]: 0 } },
name,
transaction,
});
}
await queryInterface.addIndex(
LOCAL_ARTIFACT_RETENTION_TABLE,
['recorded_at_ms', 'attempt_id'],
{ name: LOCAL_ARTIFACT_RETENTION_RECORDED_INDEX, transaction },
);
await queryInterface.addIndex(
RUN_ATTEMPT_TABLE,
['status', 'finished_at_ms', 'id'],
{ name: RUN_ATTEMPT_ARTIFACT_RETENTION_INDEX, transaction },
);
},
};
@@ -0,0 +1,96 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import type { Migration } from './types';
export const LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE =
'LocalArtifactMaintenanceCursors';
const manifest = {
table: LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
columns: [
'scope',
'cursor_finished_at_ms',
'cursor_attempt_id',
'version',
'updated_at_ms',
],
indexes: [] as string[],
constraints: [
'local_artifact_cursor_scope_check',
'local_artifact_cursor_pair_check',
'local_artifact_cursor_version_check',
'local_artifact_cursor_updated_check',
],
};
export const localArtifactMaintenanceCursorManifest = manifest;
export const localArtifactMaintenanceCursorMigration: Migration = {
id: '0016-local-artifact-maintenance-cursor',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{
scope: {
type: DataTypes.STRING(32),
allowNull: false,
primaryKey: true,
},
cursor_finished_at_ms: { type: DataTypes.BIGINT, allowNull: true },
cursor_attempt_id: { type: DataTypes.STRING(36), allowNull: true },
version: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{
fields: ['scope'],
type: 'check',
where: { scope: 'retention' },
name: 'local_artifact_cursor_scope_check',
transaction,
},
);
await queryInterface.addConstraint(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{
fields: ['cursor_finished_at_ms', 'cursor_attempt_id'],
type: 'check',
where: {
[Op.or]: [
{ cursor_finished_at_ms: null, cursor_attempt_id: null },
{
cursor_finished_at_ms: { [Op.not]: null },
cursor_attempt_id: { [Op.not]: null },
},
],
},
name: 'local_artifact_cursor_pair_check',
transaction,
},
);
await queryInterface.addConstraint(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 1 } },
name: 'local_artifact_cursor_version_check',
transaction,
},
);
await queryInterface.addConstraint(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{
fields: ['updated_at_ms'],
type: 'check',
where: { updated_at_ms: { [Op.gte]: 0 } },
name: 'local_artifact_cursor_updated_check',
transaction,
},
);
},
};
+227
View File
@@ -0,0 +1,227 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
POLICY_SUBJECT_TYPES,
PROJECT_ROLES,
PROJECT_ROLE_BINDING_STATES,
PROJECT_STATUSES,
} from '../runtime/domain/projectPolicy';
import type { Migration } from './types';
export const PROJECT_TABLE = 'Projects';
export const PROJECT_ROLE_BINDING_TABLE = 'ProjectRoleBindings';
export const PROJECT_SLUG_INDEX = 'projects_slug_uidx';
export const PROJECT_ROLE_BINDING_CURRENT_INDEX =
'project_role_binding_current_idx';
export const PROJECT_ROLE_BINDING_MUTATION_INDEX =
'project_role_binding_mutation_uidx';
export const PROJECT_ROLE_BINDING_SUBJECT_INDEX =
'project_role_binding_subject_idx';
const manifest = {
tables: {
Projects: [
'id',
'name',
'slug',
'status',
'version',
'created_at_ms',
'updated_at_ms',
],
ProjectRoleBindings: [
'project_id',
'subject_type',
'subject_id',
'version',
'state',
'role',
'mutation_id',
'changed_by_type',
'changed_by_id',
'created_at_ms',
],
},
indexes: [
`${PROJECT_SLUG_INDEX}(slug) UNIQUE`,
`${PROJECT_ROLE_BINDING_CURRENT_INDEX}(project_id,subject_type,subject_id,version DESC)`,
`${PROJECT_ROLE_BINDING_MUTATION_INDEX}(project_id,mutation_id) UNIQUE`,
`${PROJECT_ROLE_BINDING_SUBJECT_INDEX}(subject_type,subject_id,project_id,version DESC)`,
],
constraints: [
'projects_status_check',
'projects_version_check',
'projects_created_at_check',
'projects_updated_at_check',
'project_role_bindings_subject_type_check',
'project_role_bindings_version_check',
'project_role_bindings_state_check',
'project_role_bindings_role_check',
'project_role_bindings_changed_by_type_check',
'project_role_bindings_created_at_check',
],
baseline: {
id: 'default',
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
created_at_ms: 0,
updated_at_ms: 0,
},
};
export const projectPolicyManifest = manifest;
export const projectPolicyMigration: Migration = {
id: '0017-project-policy',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
PROJECT_TABLE,
{
id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
name: { type: DataTypes.STRING(255), allowNull: false },
slug: { type: DataTypes.STRING(128), allowNull: false },
status: { type: DataTypes.STRING(16), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(PROJECT_TABLE, {
fields: ['status'],
type: 'check',
where: { status: { [Op.in]: PROJECT_STATUSES } },
name: 'projects_status_check',
transaction,
});
for (const [field, name, minimum] of [
['version', 'projects_version_check', 1],
['created_at_ms', 'projects_created_at_check', 0],
['updated_at_ms', 'projects_updated_at_check', 0],
] as const) {
await queryInterface.addConstraint(PROJECT_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.gte]: minimum } },
name,
transaction,
});
}
await queryInterface.addIndex(PROJECT_TABLE, ['slug'], {
name: PROJECT_SLUG_INDEX,
unique: true,
transaction,
});
await queryInterface.bulkInsert(PROJECT_TABLE, [manifest.baseline], {
transaction,
});
await queryInterface.createTable(
PROJECT_ROLE_BINDING_TABLE,
{
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
subject_type: {
type: DataTypes.STRING(32),
allowNull: false,
primaryKey: true,
},
subject_id: {
type: DataTypes.STRING(255),
allowNull: false,
primaryKey: true,
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
},
state: { type: DataTypes.STRING(16), allowNull: false },
role: { type: DataTypes.STRING(16), allowNull: true },
mutation_id: { type: DataTypes.STRING(64), allowNull: false },
changed_by_type: { type: DataTypes.STRING(32), allowNull: false },
changed_by_id: { type: DataTypes.STRING(255), allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
for (const [field, values, name] of [
[
'subject_type',
POLICY_SUBJECT_TYPES,
'project_role_bindings_subject_type_check',
],
[
'state',
PROJECT_ROLE_BINDING_STATES,
'project_role_bindings_state_check',
],
['role', PROJECT_ROLES, 'project_role_bindings_role_check'],
[
'changed_by_type',
POLICY_SUBJECT_TYPES,
'project_role_bindings_changed_by_type_check',
],
] as const) {
await queryInterface.addConstraint(PROJECT_ROLE_BINDING_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.in]: values } },
name,
transaction,
});
}
await queryInterface.addConstraint(PROJECT_ROLE_BINDING_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 1 } },
name: 'project_role_bindings_version_check',
transaction,
});
await queryInterface.addConstraint(PROJECT_ROLE_BINDING_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'project_role_bindings_created_at_check',
transaction,
});
await queryInterface.addIndex(PROJECT_ROLE_BINDING_TABLE, {
fields: [
'project_id',
'subject_type',
'subject_id',
{ name: 'version', order: 'DESC' },
],
name: PROJECT_ROLE_BINDING_CURRENT_INDEX,
transaction,
});
await queryInterface.addIndex(
PROJECT_ROLE_BINDING_TABLE,
['project_id', 'mutation_id'],
{
name: PROJECT_ROLE_BINDING_MUTATION_INDEX,
unique: true,
transaction,
},
);
await queryInterface.addIndex(PROJECT_ROLE_BINDING_TABLE, {
fields: [
'subject_type',
'subject_id',
'project_id',
{ name: 'version', order: 'DESC' },
],
name: PROJECT_ROLE_BINDING_SUBJECT_INDEX,
transaction,
});
},
};
@@ -0,0 +1,154 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import { POLICY_SUBJECT_TYPES } from '../runtime/domain/projectPolicy';
import { PROJECT_TABLE } from './0017-project-policy';
import type { Migration } from './types';
export const PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE =
'ProjectOwnerBootstrapChallenges';
export const PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX =
'project_owner_bootstrap_challenge_id_uidx';
export const PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX =
'project_owner_bootstrap_current_idx';
const manifest = {
table: PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
columns: [
'project_id',
'version',
'challenge_id',
'token_digest',
'issued_at_ms',
'expires_at_ms',
'consumed_at_ms',
'claimed_subject_type',
'claimed_subject_id',
],
indexes: [
`${PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX}(challenge_id) UNIQUE`,
`${PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX}(project_id,version DESC)`,
],
constraints: [
'project_owner_bootstrap_version_check',
'project_owner_bootstrap_issued_at_check',
'project_owner_bootstrap_lifetime_check',
'project_owner_bootstrap_claim_tuple_check',
'project_owner_bootstrap_claimed_subject_type_check',
],
};
export const projectOwnerBootstrapManifest = manifest;
export const projectOwnerBootstrapMigration: Migration = {
id: '0018-project-owner-bootstrap',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
},
challenge_id: { type: DataTypes.STRING(22), allowNull: false },
token_digest: { type: DataTypes.STRING(64), allowNull: false },
issued_at_ms: { type: DataTypes.BIGINT, allowNull: false },
expires_at_ms: { type: DataTypes.BIGINT, allowNull: false },
consumed_at_ms: { type: DataTypes.BIGINT, allowNull: true },
claimed_subject_type: { type: DataTypes.STRING(32), allowNull: true },
claimed_subject_id: { type: DataTypes.STRING(255), allowNull: true },
},
{ transaction },
);
await queryInterface.addConstraint(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 1 } },
name: 'project_owner_bootstrap_version_check',
transaction,
},
);
await queryInterface.addConstraint(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
fields: ['issued_at_ms'],
type: 'check',
where: { issued_at_ms: { [Op.gte]: 0 } },
name: 'project_owner_bootstrap_issued_at_check',
transaction,
},
);
await queryInterface.addConstraint(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
fields: ['expires_at_ms', 'issued_at_ms'],
type: 'check',
where: { expires_at_ms: { [Op.gt]: { [Op.col]: 'issued_at_ms' } } },
name: 'project_owner_bootstrap_lifetime_check',
transaction,
},
);
await queryInterface.addConstraint(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
fields: [
'consumed_at_ms',
'claimed_subject_type',
'claimed_subject_id',
],
type: 'check',
where: {
[Op.or]: [
{
consumed_at_ms: null,
claimed_subject_type: null,
claimed_subject_id: null,
},
{
consumed_at_ms: { [Op.not]: null },
claimed_subject_type: { [Op.not]: null },
claimed_subject_id: { [Op.not]: null },
},
],
},
name: 'project_owner_bootstrap_claim_tuple_check',
transaction,
},
);
await queryInterface.addConstraint(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
{
fields: ['claimed_subject_type'],
type: 'check',
where: { claimed_subject_type: { [Op.in]: POLICY_SUBJECT_TYPES } },
name: 'project_owner_bootstrap_claimed_subject_type_check',
transaction,
},
);
await queryInterface.addIndex(
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
['challenge_id'],
{
name: PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX,
unique: true,
transaction,
},
);
await queryInterface.addIndex(PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE, {
fields: ['project_id', { name: 'version', order: 'DESC' }],
name: PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX,
transaction,
});
},
};
+208
View File
@@ -0,0 +1,208 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
IDENTITY_AUTHENTICATION_BINDING_STATES,
IDENTITY_SUBJECT_STATUSES,
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
LEGACY_PRIMARY_USER_SUBJECT_ID,
} from '../runtime/domain/identityDirectory';
import { POLICY_SUBJECT_TYPES } from '../runtime/domain/projectPolicy';
import type { Migration } from './types';
export const IDENTITY_SUBJECT_TABLE = 'IdentitySubjects';
export const IDENTITY_AUTHENTICATION_BINDING_TABLE =
'IdentityAuthenticationBindings';
export const IDENTITY_SUBJECT_STATUS_INDEX = 'identity_subject_status_idx';
export const IDENTITY_AUTHENTICATION_BINDING_CURRENT_INDEX =
'identity_auth_binding_current_idx';
export const IDENTITY_AUTHENTICATION_BINDING_SUBJECT_INDEX =
'identity_auth_binding_subject_idx';
const manifest = {
tables: {
IdentitySubjects: [
'id',
'type',
'status',
'version',
'created_at_ms',
'updated_at_ms',
],
IdentityAuthenticationBindings: [
'provider',
'provider_subject',
'version',
'state',
'subject_id',
'created_at_ms',
],
},
indexes: [
`${IDENTITY_SUBJECT_STATUS_INDEX}(type,status,id)`,
`${IDENTITY_AUTHENTICATION_BINDING_CURRENT_INDEX}(provider,provider_subject,version DESC)`,
`${IDENTITY_AUTHENTICATION_BINDING_SUBJECT_INDEX}(subject_id,provider,provider_subject,version DESC)`,
],
constraints: [
'identity_subject_type_check',
'identity_subject_status_check',
'identity_subject_version_check',
'identity_subject_created_at_check',
'identity_subject_updated_at_check',
'identity_auth_binding_version_check',
'identity_auth_binding_state_check',
'identity_auth_binding_created_at_check',
],
baseline: {
subject: {
id: LEGACY_PRIMARY_USER_SUBJECT_ID,
type: 'user',
status: 'active',
version: 1,
created_at_ms: 0,
updated_at_ms: 0,
},
binding: {
provider: LEGACY_PANEL_IDENTITY_PROVIDER,
provider_subject: LEGACY_PANEL_PROVIDER_SUBJECT,
version: 1,
state: 'active',
subject_id: LEGACY_PRIMARY_USER_SUBJECT_ID,
created_at_ms: 0,
},
},
};
export const identityDirectoryManifest = manifest;
export const identityDirectoryMigration: Migration = {
id: '0019-identity-directory',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
IDENTITY_SUBJECT_TABLE,
{
id: { type: DataTypes.STRING(255), allowNull: false, primaryKey: true },
type: { type: DataTypes.STRING(32), allowNull: false },
status: { type: DataTypes.STRING(16), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
for (const [field, values, name] of [
['type', POLICY_SUBJECT_TYPES, 'identity_subject_type_check'],
['status', IDENTITY_SUBJECT_STATUSES, 'identity_subject_status_check'],
] as const) {
await queryInterface.addConstraint(IDENTITY_SUBJECT_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.in]: values } },
name,
transaction,
});
}
for (const [field, name, minimum] of [
['version', 'identity_subject_version_check', 1],
['created_at_ms', 'identity_subject_created_at_check', 0],
['updated_at_ms', 'identity_subject_updated_at_check', 0],
] as const) {
await queryInterface.addConstraint(IDENTITY_SUBJECT_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.gte]: minimum } },
name,
transaction,
});
}
await queryInterface.addIndex(IDENTITY_SUBJECT_TABLE, {
fields: ['type', 'status', 'id'],
name: IDENTITY_SUBJECT_STATUS_INDEX,
transaction,
});
await queryInterface.bulkInsert(
IDENTITY_SUBJECT_TABLE,
[manifest.baseline.subject],
{ transaction },
);
await queryInterface.createTable(
IDENTITY_AUTHENTICATION_BINDING_TABLE,
{
provider: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
},
provider_subject: {
type: DataTypes.STRING(128),
allowNull: false,
primaryKey: true,
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
},
state: { type: DataTypes.STRING(16), allowNull: false },
subject_id: {
type: DataTypes.STRING(255),
allowNull: false,
references: { model: IDENTITY_SUBJECT_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(IDENTITY_AUTHENTICATION_BINDING_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.gte]: 1 } },
name: 'identity_auth_binding_version_check',
transaction,
});
await queryInterface.addConstraint(IDENTITY_AUTHENTICATION_BINDING_TABLE, {
fields: ['state'],
type: 'check',
where: {
state: { [Op.in]: IDENTITY_AUTHENTICATION_BINDING_STATES },
},
name: 'identity_auth_binding_state_check',
transaction,
});
await queryInterface.addConstraint(IDENTITY_AUTHENTICATION_BINDING_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'identity_auth_binding_created_at_check',
transaction,
});
await queryInterface.addIndex(IDENTITY_AUTHENTICATION_BINDING_TABLE, {
fields: [
'provider',
'provider_subject',
{ name: 'version', order: 'DESC' },
],
name: IDENTITY_AUTHENTICATION_BINDING_CURRENT_INDEX,
transaction,
});
await queryInterface.addIndex(IDENTITY_AUTHENTICATION_BINDING_TABLE, {
fields: [
'subject_id',
'provider',
'provider_subject',
{ name: 'version', order: 'DESC' },
],
name: IDENTITY_AUTHENTICATION_BINDING_SUBJECT_INDEX,
transaction,
});
await queryInterface.bulkInsert(
IDENTITY_AUTHENTICATION_BINDING_TABLE,
[manifest.baseline.binding],
{ transaction },
);
},
};
+436
View File
@@ -0,0 +1,436 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
APPROVAL_DECISIONS,
APPROVAL_REQUEST_STATES,
APPROVAL_RISKS,
APPROVED_ACTION_DISPATCH_STATES,
} from '../runtime/domain/approvalRequest';
import { POLICY_SUBJECT_TYPES } from '../runtime/domain/projectPolicy';
import { PROJECT_TABLE } from './0017-project-policy';
import type { Migration } from './types';
export const APPROVAL_REQUEST_TABLE = 'ApprovalRequests';
export const APPROVED_ACTION_DISPATCH_TABLE = 'ApprovedActionDispatches';
export const APPROVAL_REQUEST_DECISION_ID_INDEX =
'approval_request_decision_id_uidx';
export const APPROVAL_REQUEST_CONSUMPTION_ID_INDEX =
'approval_request_consumption_id_uidx';
export const APPROVAL_REQUEST_PENDING_INDEX = 'approval_request_pending_idx';
export const APPROVAL_REQUEST_REQUESTER_INDEX =
'approval_request_requester_idx';
export const APPROVED_ACTION_DISPATCH_REQUEST_INDEX =
'approved_action_dispatch_request_uidx';
export const APPROVED_ACTION_DISPATCH_PENDING_INDEX =
'approved_action_dispatch_pending_idx';
const approvalRequestColumns = [
'id',
'project_id',
'version',
'state',
'permission',
'action_type',
'action_ref',
'action_digest',
'preview_digest',
'risk',
'requested_by_type',
'requested_by_id',
'requested_at_ms',
'expires_at_ms',
'decision_id',
'decision',
'decision_reason_code',
'decided_by_type',
'decided_by_id',
'decided_at_ms',
'consumption_id',
'dispatch_id',
'consumed_by_type',
'consumed_by_id',
'consumed_at_ms',
];
const approvedActionDispatchColumns = [
'id',
'approval_request_id',
'approval_request_version',
'project_id',
'state',
'permission',
'action_type',
'action_ref',
'action_digest',
'preview_digest',
'requested_by_type',
'requested_by_id',
'consumed_by_type',
'consumed_by_id',
'created_at_ms',
];
const constraints = [
'approval_request_version_check',
'approval_request_state_check',
'approval_request_risk_check',
'approval_request_requester_type_check',
'approval_request_lifetime_check',
'approval_request_decision_tuple_check',
'approval_request_decision_value_check',
'approval_request_decided_by_type_check',
'approval_request_decision_time_check',
'approval_request_consumption_tuple_check',
'approval_request_consumed_by_type_check',
'approval_request_consumption_time_check',
'approval_request_state_tuple_check',
'approved_action_dispatch_version_check',
'approved_action_dispatch_state_check',
'approved_action_dispatch_requester_type_check',
'approved_action_dispatch_consumer_type_check',
'approved_action_dispatch_created_at_check',
];
const manifest = {
tables: {
ApprovalRequests: approvalRequestColumns,
ApprovedActionDispatches: approvedActionDispatchColumns,
},
indexes: [
`${APPROVAL_REQUEST_DECISION_ID_INDEX}(decision_id) UNIQUE`,
`${APPROVAL_REQUEST_CONSUMPTION_ID_INDEX}(consumption_id) UNIQUE`,
`${APPROVAL_REQUEST_PENDING_INDEX}(project_id,state,expires_at_ms,id)`,
`${APPROVAL_REQUEST_REQUESTER_INDEX}(project_id,requested_by_type,requested_by_id,requested_at_ms DESC,id)`,
`${APPROVED_ACTION_DISPATCH_REQUEST_INDEX}(approval_request_id) UNIQUE`,
`${APPROVED_ACTION_DISPATCH_PENDING_INDEX}(project_id,state,created_at_ms,id)`,
],
constraints,
};
export const approvalRequestManifest = manifest;
function nullableTuple(...fields: string[]) {
return {
[Op.or]: [
Object.fromEntries(fields.map((field) => [field, null])),
Object.fromEntries(fields.map((field) => [field, { [Op.not]: null }])),
],
};
}
export const approvalRequestMigration: Migration = {
id: '0020-approval-requests',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
APPROVAL_REQUEST_TABLE,
{
id: { type: DataTypes.STRING(64), allowNull: false, primaryKey: true },
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
version: { type: DataTypes.INTEGER, allowNull: false },
state: { type: DataTypes.STRING(16), allowNull: false },
permission: { type: DataTypes.STRING(255), allowNull: false },
action_type: { type: DataTypes.STRING(64), allowNull: false },
action_ref: { type: DataTypes.STRING(255), allowNull: false },
action_digest: { type: DataTypes.STRING(64), allowNull: false },
preview_digest: { type: DataTypes.STRING(64), allowNull: false },
risk: { type: DataTypes.STRING(16), allowNull: false },
requested_by_type: { type: DataTypes.STRING(32), allowNull: false },
requested_by_id: { type: DataTypes.STRING(255), allowNull: false },
requested_at_ms: { type: DataTypes.BIGINT, allowNull: false },
expires_at_ms: { type: DataTypes.BIGINT, allowNull: false },
decision_id: { type: DataTypes.STRING(64), allowNull: true },
decision: { type: DataTypes.STRING(16), allowNull: true },
decision_reason_code: { type: DataTypes.STRING(64), allowNull: true },
decided_by_type: { type: DataTypes.STRING(32), allowNull: true },
decided_by_id: { type: DataTypes.STRING(255), allowNull: true },
decided_at_ms: { type: DataTypes.BIGINT, allowNull: true },
consumption_id: { type: DataTypes.STRING(64), allowNull: true },
dispatch_id: { type: DataTypes.STRING(64), allowNull: true },
consumed_by_type: { type: DataTypes.STRING(32), allowNull: true },
consumed_by_id: { type: DataTypes.STRING(255), allowNull: true },
consumed_at_ms: { type: DataTypes.BIGINT, allowNull: true },
},
{ transaction },
);
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['version'],
type: 'check',
where: { version: { [Op.between]: [1, 3] } },
name: 'approval_request_version_check',
transaction,
});
for (const [field, values, name] of [
['state', APPROVAL_REQUEST_STATES, 'approval_request_state_check'],
['risk', APPROVAL_RISKS, 'approval_request_risk_check'],
[
'requested_by_type',
POLICY_SUBJECT_TYPES,
'approval_request_requester_type_check',
],
[
'decided_by_type',
POLICY_SUBJECT_TYPES,
'approval_request_decided_by_type_check',
],
[
'consumed_by_type',
POLICY_SUBJECT_TYPES,
'approval_request_consumed_by_type_check',
],
] as const) {
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.in]: values } },
name,
transaction,
});
}
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['requested_at_ms', 'expires_at_ms'],
type: 'check',
where: {
requested_at_ms: { [Op.gte]: 0 },
expires_at_ms: { [Op.gt]: { [Op.col]: 'requested_at_ms' } },
},
name: 'approval_request_lifetime_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: [
'decision_id',
'decision',
'decision_reason_code',
'decided_by_type',
'decided_by_id',
'decided_at_ms',
],
type: 'check',
where: nullableTuple(
'decision_id',
'decision',
'decision_reason_code',
'decided_by_type',
'decided_by_id',
'decided_at_ms',
),
name: 'approval_request_decision_tuple_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['decision'],
type: 'check',
where: { decision: { [Op.in]: APPROVAL_DECISIONS } },
name: 'approval_request_decision_value_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['decided_at_ms', 'requested_at_ms', 'expires_at_ms'],
type: 'check',
where: {
[Op.or]: [
{ decided_at_ms: null },
{
decided_at_ms: {
[Op.gte]: { [Op.col]: 'requested_at_ms' },
[Op.lt]: { [Op.col]: 'expires_at_ms' },
},
},
],
},
name: 'approval_request_decision_time_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: [
'consumption_id',
'dispatch_id',
'consumed_by_type',
'consumed_by_id',
'consumed_at_ms',
],
type: 'check',
where: nullableTuple(
'consumption_id',
'dispatch_id',
'consumed_by_type',
'consumed_by_id',
'consumed_at_ms',
),
name: 'approval_request_consumption_tuple_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['consumed_at_ms', 'decided_at_ms', 'expires_at_ms'],
type: 'check',
where: {
[Op.or]: [
{ consumed_at_ms: null },
{
consumed_at_ms: {
[Op.gte]: { [Op.col]: 'decided_at_ms' },
[Op.lt]: { [Op.col]: 'expires_at_ms' },
},
},
],
},
name: 'approval_request_consumption_time_check',
transaction,
});
await queryInterface.addConstraint(APPROVAL_REQUEST_TABLE, {
fields: ['state', 'version', 'decision', 'decision_id', 'consumption_id'],
type: 'check',
where: {
[Op.or]: [
{
state: 'pending',
version: 1,
decision_id: null,
consumption_id: null,
},
{
state: 'approved',
version: 2,
decision: 'approved',
consumption_id: null,
},
{
state: 'rejected',
version: 2,
decision: 'rejected',
consumption_id: null,
},
{
state: 'consumed',
version: 3,
decision: 'approved',
consumption_id: { [Op.not]: null },
},
],
},
name: 'approval_request_state_tuple_check',
transaction,
});
await queryInterface.addIndex(APPROVAL_REQUEST_TABLE, ['decision_id'], {
name: APPROVAL_REQUEST_DECISION_ID_INDEX,
unique: true,
transaction,
});
await queryInterface.addIndex(APPROVAL_REQUEST_TABLE, ['consumption_id'], {
name: APPROVAL_REQUEST_CONSUMPTION_ID_INDEX,
unique: true,
transaction,
});
await queryInterface.addIndex(APPROVAL_REQUEST_TABLE, {
fields: ['project_id', 'state', 'expires_at_ms', 'id'],
name: APPROVAL_REQUEST_PENDING_INDEX,
transaction,
});
await queryInterface.addIndex(APPROVAL_REQUEST_TABLE, {
fields: [
'project_id',
'requested_by_type',
'requested_by_id',
{ name: 'requested_at_ms', order: 'DESC' },
'id',
],
name: APPROVAL_REQUEST_REQUESTER_INDEX,
transaction,
});
await queryInterface.createTable(
APPROVED_ACTION_DISPATCH_TABLE,
{
id: { type: DataTypes.STRING(64), allowNull: false, primaryKey: true },
approval_request_id: {
type: DataTypes.STRING(64),
allowNull: false,
references: { model: APPROVAL_REQUEST_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
approval_request_version: { type: DataTypes.INTEGER, allowNull: false },
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
state: { type: DataTypes.STRING(16), allowNull: false },
permission: { type: DataTypes.STRING(255), allowNull: false },
action_type: { type: DataTypes.STRING(64), allowNull: false },
action_ref: { type: DataTypes.STRING(255), allowNull: false },
action_digest: { type: DataTypes.STRING(64), allowNull: false },
preview_digest: { type: DataTypes.STRING(64), allowNull: false },
requested_by_type: { type: DataTypes.STRING(32), allowNull: false },
requested_by_id: { type: DataTypes.STRING(255), allowNull: false },
consumed_by_type: { type: DataTypes.STRING(32), allowNull: false },
consumed_by_id: { type: DataTypes.STRING(255), allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(APPROVED_ACTION_DISPATCH_TABLE, {
fields: ['approval_request_version'],
type: 'check',
where: { approval_request_version: 3 },
name: 'approved_action_dispatch_version_check',
transaction,
});
for (const [field, values, name] of [
[
'state',
APPROVED_ACTION_DISPATCH_STATES,
'approved_action_dispatch_state_check',
],
[
'requested_by_type',
POLICY_SUBJECT_TYPES,
'approved_action_dispatch_requester_type_check',
],
[
'consumed_by_type',
POLICY_SUBJECT_TYPES,
'approved_action_dispatch_consumer_type_check',
],
] as const) {
await queryInterface.addConstraint(APPROVED_ACTION_DISPATCH_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.in]: values } },
name,
transaction,
});
}
await queryInterface.addConstraint(APPROVED_ACTION_DISPATCH_TABLE, {
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'approved_action_dispatch_created_at_check',
transaction,
});
await queryInterface.addIndex(
APPROVED_ACTION_DISPATCH_TABLE,
['approval_request_id'],
{
name: APPROVED_ACTION_DISPATCH_REQUEST_INDEX,
unique: true,
transaction,
},
);
await queryInterface.addIndex(APPROVED_ACTION_DISPATCH_TABLE, {
fields: ['project_id', 'state', 'created_at_ms', 'id'],
name: APPROVED_ACTION_DISPATCH_PENDING_INDEX,
transaction,
});
},
};
@@ -0,0 +1,227 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
APPROVED_ACTION_EXECUTION_STATUSES,
DEFAULT_APPROVED_ACTION_MAX_ATTEMPTS,
MAX_APPROVED_ACTION_ATTEMPTS,
} from '../runtime/domain/approvedActionDispatchExecution';
import { PROJECT_TABLE } from './0017-project-policy';
import { APPROVED_ACTION_DISPATCH_TABLE } from './0020-approval-requests';
import type { Migration } from './types';
export const APPROVED_ACTION_DISPATCH_EXECUTION_TABLE =
'ApprovedActionDispatchExecutions';
export const APPROVED_ACTION_DISPATCH_EXECUTION_DUE_INDEX =
'approved_action_execution_due_idx';
export const APPROVED_ACTION_DISPATCH_EXECUTION_PROJECT_INDEX =
'approved_action_execution_project_idx';
export const APPROVED_ACTION_DISPATCH_EXECUTION_LEASE_INDEX =
'approved_action_execution_lease_idx';
const columns = [
'dispatch_id',
'project_id',
'status',
'version',
'attempt_count',
'max_attempts',
'eligible_at_ms',
'next_attempt_at_ms',
'lease_owner',
'lease_token',
'lease_expires_at_ms',
'started_at_ms',
'result_mutation_id',
'last_result_code',
'completed_at_ms',
'created_at_ms',
'updated_at_ms',
];
const manifest = {
table: APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
columns,
indexes: [
`${APPROVED_ACTION_DISPATCH_EXECUTION_DUE_INDEX}(eligible_at_ms,dispatch_id)`,
`${APPROVED_ACTION_DISPATCH_EXECUTION_PROJECT_INDEX}(project_id,status,created_at_ms,dispatch_id)`,
`${APPROVED_ACTION_DISPATCH_EXECUTION_LEASE_INDEX}(status,lease_expires_at_ms,dispatch_id)`,
],
constraints: [
'approved_action_execution_status_check',
'approved_action_execution_version_check',
'approved_action_execution_attempt_count_check',
'approved_action_execution_max_attempts_check',
'approved_action_execution_attempt_budget_check',
'approved_action_execution_lease_tuple_check',
'approved_action_execution_created_at_check',
'approved_action_execution_updated_at_check',
],
baselineMaxAttempts: DEFAULT_APPROVED_ACTION_MAX_ATTEMPTS,
};
export const approvedActionDispatchExecutionManifest = manifest;
export const approvedActionDispatchExecutionMigration: Migration = {
id: '0021-approved-action-dispatch-executions',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
dispatch_id: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
references: { model: APPROVED_ACTION_DISPATCH_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
status: { type: DataTypes.STRING(16), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
attempt_count: { type: DataTypes.INTEGER, allowNull: false },
max_attempts: { type: DataTypes.INTEGER, allowNull: false },
eligible_at_ms: { type: DataTypes.BIGINT, allowNull: true },
next_attempt_at_ms: { type: DataTypes.BIGINT, allowNull: true },
lease_owner: { type: DataTypes.STRING(128), allowNull: true },
lease_token: { type: DataTypes.STRING(128), allowNull: true },
lease_expires_at_ms: { type: DataTypes.BIGINT, allowNull: true },
started_at_ms: { type: DataTypes.BIGINT, allowNull: true },
result_mutation_id: { type: DataTypes.STRING(64), allowNull: true },
last_result_code: { type: DataTypes.STRING(64), allowNull: true },
completed_at_ms: { type: DataTypes.BIGINT, allowNull: true },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: ['status'],
type: 'check',
where: { status: { [Op.in]: APPROVED_ACTION_EXECUTION_STATUSES } },
name: 'approved_action_execution_status_check',
transaction,
},
);
for (const [field, minimum, maximum, name] of [
['version', 0, 2_147_483_647, 'approved_action_execution_version_check'],
[
'attempt_count',
0,
MAX_APPROVED_ACTION_ATTEMPTS,
'approved_action_execution_attempt_count_check',
],
[
'max_attempts',
1,
MAX_APPROVED_ACTION_ATTEMPTS,
'approved_action_execution_max_attempts_check',
],
] as const) {
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: [field],
type: 'check',
where: { [field]: { [Op.between]: [minimum, maximum] } },
name,
transaction,
},
);
}
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: ['attempt_count', 'max_attempts'],
type: 'check',
where: { attempt_count: { [Op.lte]: { [Op.col]: 'max_attempts' } } },
name: 'approved_action_execution_attempt_budget_check',
transaction,
},
);
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: ['lease_owner', 'lease_token', 'lease_expires_at_ms'],
type: 'check',
where: {
[Op.or]: [
{
lease_owner: null,
lease_token: null,
lease_expires_at_ms: null,
},
{
lease_owner: { [Op.not]: null },
lease_token: { [Op.not]: null },
lease_expires_at_ms: { [Op.not]: null },
},
],
},
name: 'approved_action_execution_lease_tuple_check',
transaction,
},
);
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: ['created_at_ms'],
type: 'check',
where: { created_at_ms: { [Op.gte]: 0 } },
name: 'approved_action_execution_created_at_check',
transaction,
},
);
await queryInterface.addConstraint(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
{
fields: ['updated_at_ms', 'created_at_ms'],
type: 'check',
where: { updated_at_ms: { [Op.gte]: { [Op.col]: 'created_at_ms' } } },
name: 'approved_action_execution_updated_at_check',
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
['eligible_at_ms', 'dispatch_id'],
{
name: APPROVED_ACTION_DISPATCH_EXECUTION_DUE_INDEX,
transaction,
},
);
await queryInterface.addIndex(APPROVED_ACTION_DISPATCH_EXECUTION_TABLE, {
fields: ['project_id', 'status', 'created_at_ms', 'dispatch_id'],
name: APPROVED_ACTION_DISPATCH_EXECUTION_PROJECT_INDEX,
transaction,
});
await queryInterface.addIndex(APPROVED_ACTION_DISPATCH_EXECUTION_TABLE, {
fields: ['status', 'lease_expires_at_ms', 'dispatch_id'],
name: APPROVED_ACTION_DISPATCH_EXECUTION_LEASE_INDEX,
transaction,
});
await queryInterface.sequelize.query(
`INSERT INTO "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
(dispatch_id, project_id, status, version, attempt_count, max_attempts,
eligible_at_ms, next_attempt_at_ms, lease_owner, lease_token,
lease_expires_at_ms, started_at_ms, result_mutation_id,
last_result_code, completed_at_ms, created_at_ms, updated_at_ms)
SELECT id, project_id, 'pending', 0, 0, :maxAttempts,
created_at_ms, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
created_at_ms, created_at_ms
FROM "${APPROVED_ACTION_DISPATCH_TABLE}"`,
{
replacements: { maxAttempts: DEFAULT_APPROVED_ACTION_MAX_ATTEMPTS },
transaction,
},
);
},
};
@@ -0,0 +1,447 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
APPROVED_ACTION_RECOVERY_CONTROL_STATUSES,
APPROVED_ACTION_RECOVERY_DECISIONS,
APPROVED_ACTION_RECOVERY_FINDINGS,
APPROVED_ACTION_RECOVERY_SOURCES,
MAX_APPROVED_ACTION_RECOVERY_FINDINGS,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
} from '../runtime/domain/approvedActionRecovery';
import { PROJECT_TABLE } from './0017-project-policy';
import { APPROVED_ACTION_DISPATCH_EXECUTION_TABLE } from './0021-approved-action-dispatch-executions';
import type { Migration } from './types';
export const APPROVED_ACTION_RECOVERY_CONTROL_TABLE =
'ApprovedActionRecoveryControls';
export const APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE =
'ApprovedActionRecoveryResolutions';
export const APPROVED_ACTION_RECOVERY_DUE_INDEX =
'approved_action_recovery_due_idx';
export const APPROVED_ACTION_RECOVERY_PROJECT_INDEX =
'approved_action_recovery_project_idx';
export const APPROVED_ACTION_RECOVERY_LEASE_INDEX =
'approved_action_recovery_lease_idx';
export const APPROVED_ACTION_RECOVERY_RESOLUTION_MUTATION_INDEX =
'approved_action_recovery_resolution_mutation_uidx';
export const APPROVED_ACTION_RECOVERY_RESOLUTION_PROJECT_INDEX =
'approved_action_recovery_resolution_project_idx';
const controlColumns = [
'dispatch_id',
'project_id',
'execution_version',
'status',
'version',
'next_scan_at_ms',
'lease_owner',
'lease_token',
'lease_expires_at_ms',
'finding_count',
'last_finding_mutation_id',
'last_finding',
'last_result_code',
'last_evidence_digest',
'resolution_mutation_id',
'created_at_ms',
'updated_at_ms',
];
const resolutionColumns = [
'dispatch_id',
'project_id',
'execution_version',
'mutation_id',
'source',
'decision',
'evidence_digest',
'reason_code',
'resolved_by_type',
'resolved_by_id',
'resolved_at_ms',
];
const constraints = [
'approved_action_recovery_status_check',
'approved_action_recovery_execution_version_check',
'approved_action_recovery_version_check',
'approved_action_recovery_finding_count_check',
'approved_action_recovery_lease_tuple_check',
'approved_action_recovery_finding_tuple_check',
'approved_action_recovery_finding_value_check',
'approved_action_recovery_evidence_digest_check',
'approved_action_recovery_resolution_mutation_tuple_check',
'approved_action_recovery_timestamps_check',
'approved_action_recovery_resolution_execution_version_check',
'approved_action_recovery_resolution_source_check',
'approved_action_recovery_resolution_decision_check',
'approved_action_recovery_resolution_actor_tuple_check',
'approved_action_recovery_resolution_source_tuple_check',
'approved_action_recovery_resolution_evidence_digest_check',
'approved_action_recovery_resolution_time_check',
];
const manifest = {
tables: {
ApprovedActionRecoveryControls: controlColumns,
ApprovedActionRecoveryResolutions: resolutionColumns,
},
indexes: [
`${APPROVED_ACTION_RECOVERY_DUE_INDEX}(status,next_scan_at_ms,dispatch_id)`,
`${APPROVED_ACTION_RECOVERY_PROJECT_INDEX}(project_id,status,created_at_ms,dispatch_id)`,
`${APPROVED_ACTION_RECOVERY_LEASE_INDEX}(status,lease_expires_at_ms,dispatch_id)`,
`${APPROVED_ACTION_RECOVERY_RESOLUTION_MUTATION_INDEX}(mutation_id) UNIQUE`,
`${APPROVED_ACTION_RECOVERY_RESOLUTION_PROJECT_INDEX}(project_id,resolved_at_ms,dispatch_id)`,
],
constraints,
};
export const approvedActionRecoveryManifest = manifest;
export const approvedActionRecoveryMigration: Migration = {
id: '0022-approved-action-recovery',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
{
dispatch_id: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
references: {
model: APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
key: 'dispatch_id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
execution_version: { type: DataTypes.INTEGER, allowNull: false },
status: { type: DataTypes.STRING(24), allowNull: false },
version: { type: DataTypes.INTEGER, allowNull: false },
next_scan_at_ms: { type: DataTypes.BIGINT, allowNull: true },
lease_owner: { type: DataTypes.STRING(128), allowNull: true },
lease_token: { type: DataTypes.STRING(128), allowNull: true },
lease_expires_at_ms: { type: DataTypes.BIGINT, allowNull: true },
finding_count: { type: DataTypes.INTEGER, allowNull: false },
last_finding_mutation_id: {
type: DataTypes.STRING(64),
allowNull: true,
},
last_finding: { type: DataTypes.STRING(24), allowNull: true },
last_result_code: { type: DataTypes.STRING(64), allowNull: true },
last_evidence_digest: { type: DataTypes.STRING(64), allowNull: true },
resolution_mutation_id: {
type: DataTypes.STRING(64),
allowNull: true,
},
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
updated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
await queryInterface.createTable(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
{
dispatch_id: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
references: {
model: APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
key: 'dispatch_id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
execution_version: { type: DataTypes.INTEGER, allowNull: false },
mutation_id: { type: DataTypes.STRING(64), allowNull: false },
source: { type: DataTypes.STRING(24), allowNull: false },
decision: { type: DataTypes.STRING(24), allowNull: false },
evidence_digest: { type: DataTypes.STRING(64), allowNull: true },
reason_code: { type: DataTypes.STRING(64), allowNull: false },
resolved_by_type: { type: DataTypes.STRING(32), allowNull: true },
resolved_by_id: { type: DataTypes.STRING(255), allowNull: true },
resolved_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
for (const [table, field, minimum, maximum, name] of [
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'execution_version',
1,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
'approved_action_recovery_execution_version_check',
],
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'version',
0,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
'approved_action_recovery_version_check',
],
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'finding_count',
0,
MAX_APPROVED_ACTION_RECOVERY_FINDINGS,
'approved_action_recovery_finding_count_check',
],
[
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
'execution_version',
1,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
'approved_action_recovery_resolution_execution_version_check',
],
] as const) {
await queryInterface.addConstraint(table, {
fields: [field],
type: 'check',
where: { [field]: { [Op.between]: [minimum, maximum] } },
name,
transaction,
});
}
for (const [table, field, values, name] of [
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'status',
APPROVED_ACTION_RECOVERY_CONTROL_STATUSES,
'approved_action_recovery_status_check',
],
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'last_finding',
APPROVED_ACTION_RECOVERY_FINDINGS,
'approved_action_recovery_finding_value_check',
],
[
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
'source',
APPROVED_ACTION_RECOVERY_SOURCES,
'approved_action_recovery_resolution_source_check',
],
[
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
'decision',
APPROVED_ACTION_RECOVERY_DECISIONS,
'approved_action_recovery_resolution_decision_check',
],
] as const) {
await queryInterface.addConstraint(table, {
fields: [field],
type: 'check',
where: { [field]: { [Op.in]: values } },
name,
transaction,
});
}
await queryInterface.addConstraint(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['lease_owner', 'lease_token', 'lease_expires_at_ms'],
type: 'check',
where: {
[Op.or]: [
{ lease_owner: null, lease_token: null, lease_expires_at_ms: null },
{
lease_owner: { [Op.not]: null },
lease_token: { [Op.not]: null },
lease_expires_at_ms: { [Op.not]: null },
},
],
},
name: 'approved_action_recovery_lease_tuple_check',
transaction,
});
await queryInterface.addConstraint(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['last_finding_mutation_id', 'last_finding', 'last_result_code'],
type: 'check',
where: {
[Op.or]: [
{
last_finding_mutation_id: null,
last_finding: null,
last_result_code: null,
},
{
last_finding_mutation_id: { [Op.not]: null },
last_finding: { [Op.not]: null },
last_result_code: { [Op.not]: null },
},
],
},
name: 'approved_action_recovery_finding_tuple_check',
transaction,
});
for (const [table, field, name] of [
[
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
'last_evidence_digest',
'approved_action_recovery_evidence_digest_check',
],
[
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
'evidence_digest',
'approved_action_recovery_resolution_evidence_digest_check',
],
] as const) {
await queryInterface.addConstraint(table, {
fields: [field],
type: 'check',
where: {
[Op.or]: [
{ [field]: null },
queryInterface.sequelize.where(
queryInterface.sequelize.fn(
'length',
queryInterface.sequelize.col(field),
),
64,
),
],
},
name,
transaction,
});
}
await queryInterface.addConstraint(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['status', 'resolution_mutation_id'],
type: 'check',
where: {
[Op.or]: [
{ status: 'resolved', resolution_mutation_id: { [Op.not]: null } },
{
status: { [Op.not]: 'resolved' },
resolution_mutation_id: null,
},
],
},
name: 'approved_action_recovery_resolution_mutation_tuple_check',
transaction,
});
await queryInterface.addConstraint(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['created_at_ms', 'updated_at_ms'],
type: 'check',
where: {
created_at_ms: { [Op.gte]: 0 },
updated_at_ms: { [Op.gte]: { [Op.col]: 'created_at_ms' } },
},
name: 'approved_action_recovery_timestamps_check',
transaction,
});
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
{
fields: ['resolved_by_type', 'resolved_by_id'],
type: 'check',
where: {
[Op.or]: [
{ resolved_by_type: null, resolved_by_id: null },
{
resolved_by_type: 'user',
resolved_by_id: { [Op.not]: null },
},
],
},
name: 'approved_action_recovery_resolution_actor_tuple_check',
transaction,
},
);
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
{
fields: ['source', 'decision', 'evidence_digest', 'resolved_by_type'],
type: 'check',
where: {
[Op.or]: [
{
source: 'automatic_evidence',
decision: {
[Op.in]: ['confirm_succeeded', 'confirm_failed'],
},
evidence_digest: { [Op.not]: null },
resolved_by_type: null,
},
{ source: 'human', resolved_by_type: 'user' },
],
},
name: 'approved_action_recovery_resolution_source_tuple_check',
transaction,
},
);
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
{
fields: ['resolved_at_ms'],
type: 'check',
where: { resolved_at_ms: { [Op.gte]: 0 } },
name: 'approved_action_recovery_resolution_time_check',
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
['status', 'next_scan_at_ms', 'dispatch_id'],
{ name: APPROVED_ACTION_RECOVERY_DUE_INDEX, transaction },
);
await queryInterface.addIndex(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['project_id', 'status', 'created_at_ms', 'dispatch_id'],
name: APPROVED_ACTION_RECOVERY_PROJECT_INDEX,
transaction,
});
await queryInterface.addIndex(APPROVED_ACTION_RECOVERY_CONTROL_TABLE, {
fields: ['status', 'lease_expires_at_ms', 'dispatch_id'],
name: APPROVED_ACTION_RECOVERY_LEASE_INDEX,
transaction,
});
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
['mutation_id'],
{
name: APPROVED_ACTION_RECOVERY_RESOLUTION_MUTATION_INDEX,
unique: true,
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
['project_id', 'resolved_at_ms', 'dispatch_id'],
{
name: APPROVED_ACTION_RECOVERY_RESOLUTION_PROJECT_INDEX,
transaction,
},
);
await queryInterface.sequelize.query(
`INSERT INTO "${APPROVED_ACTION_RECOVERY_CONTROL_TABLE}"
(dispatch_id, project_id, execution_version, status, version,
next_scan_at_ms, lease_owner, lease_token, lease_expires_at_ms,
finding_count, last_finding_mutation_id, last_finding,
last_result_code, last_evidence_digest,
resolution_mutation_id, created_at_ms, updated_at_ms)
SELECT dispatch_id, project_id, version, 'armed', 0,
lease_expires_at_ms, NULL, NULL, NULL,
0, NULL, NULL, NULL, NULL, NULL, started_at_ms, started_at_ms
FROM "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
WHERE status = 'executing'`,
{ transaction },
);
},
};
@@ -0,0 +1,188 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
APPROVED_RUN_ACTION_TYPE,
APPROVED_RUN_RECEIPT_RESULT_CODE,
APPROVED_RUN_RECEIPT_SCHEMA_VERSION,
} from '../runtime/domain/approvedRunAction';
import { PROJECT_TABLE } from './0017-project-policy';
import {
APPROVAL_REQUEST_TABLE,
APPROVED_ACTION_DISPATCH_TABLE,
} from './0020-approval-requests';
import { RUN_TABLE } from './0002-run-schema';
import type { Migration } from './types';
export const APPROVED_RUN_ACTION_RECEIPT_TABLE = 'ApprovedRunActionReceipts';
export const APPROVED_RUN_ACTION_RECEIPT_PROJECT_INDEX =
'approved_run_receipt_project_idx';
export const APPROVED_RUN_ACTION_RECEIPT_RESOURCE_UNIQUE_INDEX =
'approved_run_receipt_resource_uidx';
const columns = [
'dispatch_id',
'approval_request_id',
'project_id',
'schema_version',
'action_type',
'action_digest',
'execution_attempt',
'execution_version',
'started_at_ms',
'idempotency_key',
'outcome',
'result_code',
'resource_type',
'resource_id',
'finished_at_ms',
'evidence_digest',
'created_at_ms',
];
const manifest = {
table: APPROVED_RUN_ACTION_RECEIPT_TABLE,
columns,
indexes: [
`${APPROVED_RUN_ACTION_RECEIPT_PROJECT_INDEX}(project_id,created_at_ms,dispatch_id)`,
`${APPROVED_RUN_ACTION_RECEIPT_RESOURCE_UNIQUE_INDEX}(resource_type,resource_id)`,
],
constraints: [
'approved_run_receipt_schema_version_check',
'approved_run_receipt_action_type_check',
'approved_run_receipt_execution_attempt_check',
'approved_run_receipt_execution_version_check',
'approved_run_receipt_idempotency_check',
'approved_run_receipt_outcome_check',
'approved_run_receipt_result_code_check',
'approved_run_receipt_resource_type_check',
'approved_run_receipt_timestamps_check',
],
};
export const approvedRunActionReceiptManifest = manifest;
export const approvedRunActionReceiptMigration: Migration = {
id: '0023-approved-run-action-receipts',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
APPROVED_RUN_ACTION_RECEIPT_TABLE,
{
dispatch_id: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
references: { model: APPROVED_ACTION_DISPATCH_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
approval_request_id: {
type: DataTypes.STRING(64),
allowNull: false,
references: { model: APPROVAL_REQUEST_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
schema_version: { type: DataTypes.INTEGER, allowNull: false },
action_type: { type: DataTypes.STRING(64), allowNull: false },
action_digest: { type: DataTypes.STRING(64), allowNull: false },
execution_attempt: { type: DataTypes.INTEGER, allowNull: false },
execution_version: { type: DataTypes.INTEGER, allowNull: false },
started_at_ms: { type: DataTypes.BIGINT, allowNull: false },
idempotency_key: { type: DataTypes.STRING(64), allowNull: false },
outcome: { type: DataTypes.STRING(16), allowNull: false },
result_code: { type: DataTypes.STRING(64), allowNull: false },
resource_type: { type: DataTypes.STRING(16), allowNull: false },
resource_id: {
type: DataTypes.STRING(64),
allowNull: false,
references: { model: RUN_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
finished_at_ms: { type: DataTypes.BIGINT, allowNull: false },
evidence_digest: { type: DataTypes.STRING(64), allowNull: false },
created_at_ms: { type: DataTypes.BIGINT, allowNull: false },
},
{ transaction },
);
for (const [field, value, name] of [
[
'schema_version',
APPROVED_RUN_RECEIPT_SCHEMA_VERSION,
'approved_run_receipt_schema_version_check',
],
[
'action_type',
APPROVED_RUN_ACTION_TYPE,
'approved_run_receipt_action_type_check',
],
['outcome', 'succeeded', 'approved_run_receipt_outcome_check'],
[
'result_code',
APPROVED_RUN_RECEIPT_RESULT_CODE,
'approved_run_receipt_result_code_check',
],
['resource_type', 'run', 'approved_run_receipt_resource_type_check'],
] as const) {
await queryInterface.addConstraint(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: [field],
type: 'check',
where: { [field]: value },
name,
transaction,
});
}
for (const [field, maximum, name] of [
['execution_attempt', 16, 'approved_run_receipt_execution_attempt_check'],
[
'execution_version',
2_147_483_647,
'approved_run_receipt_execution_version_check',
],
] as const) {
await queryInterface.addConstraint(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: [field],
type: 'check',
where: { [field]: { [Op.between]: [1, maximum] } },
name,
transaction,
});
}
await queryInterface.addConstraint(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: ['dispatch_id', 'idempotency_key'],
type: 'check',
where: { idempotency_key: { [Op.eq]: { [Op.col]: 'dispatch_id' } } },
name: 'approved_run_receipt_idempotency_check',
transaction,
});
await queryInterface.addConstraint(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: ['started_at_ms', 'finished_at_ms', 'created_at_ms'],
type: 'check',
where: {
finished_at_ms: { [Op.gte]: { [Op.col]: 'started_at_ms' } },
created_at_ms: { [Op.eq]: { [Op.col]: 'finished_at_ms' } },
},
name: 'approved_run_receipt_timestamps_check',
transaction,
});
await queryInterface.addIndex(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: ['project_id', 'created_at_ms', 'dispatch_id'],
name: APPROVED_RUN_ACTION_RECEIPT_PROJECT_INDEX,
transaction,
});
await queryInterface.addIndex(APPROVED_RUN_ACTION_RECEIPT_TABLE, {
fields: ['resource_type', 'resource_id'],
unique: true,
name: APPROVED_RUN_ACTION_RECEIPT_RESOURCE_UNIQUE_INDEX,
transaction,
});
},
};
@@ -0,0 +1,166 @@
import { createHash } from 'crypto';
import { DataTypes, Op } from 'sequelize';
import {
APPROVED_ACTION_RECOVERY_STRONG_ASSURANCES,
MAX_APPROVED_ACTION_RECOVERY_AUTH_AGE_MS,
} from '../runtime/domain/approvedActionRecoveryAuthorization';
import { PROJECT_TABLE } from './0017-project-policy';
import { APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE } from './0022-approved-action-recovery';
import type { Migration } from './types';
export const APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE =
'ApprovedActionRecoveryAuthorizationFacts';
export const APPROVED_ACTION_RECOVERY_AUTHORIZATION_MUTATION_INDEX =
'approved_action_recovery_authorization_mutation_uidx';
export const APPROVED_ACTION_RECOVERY_AUTHORIZATION_PROJECT_INDEX =
'approved_action_recovery_authorization_project_idx';
export const APPROVED_ACTION_RECOVERY_AUTHORIZATION_AUTH_INDEX =
'approved_action_recovery_authorization_auth_idx';
const columns = [
'dispatch_id',
'project_id',
'mutation_id',
'resolved_by_id',
'authentication_id',
'assurance',
'authenticated_at_ms',
'project_version',
'binding_version',
'authorized_at_ms',
'fact_digest',
];
const manifest = {
table: APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
columns,
indexes: [
`${APPROVED_ACTION_RECOVERY_AUTHORIZATION_MUTATION_INDEX}(mutation_id) UNIQUE`,
`${APPROVED_ACTION_RECOVERY_AUTHORIZATION_PROJECT_INDEX}(project_id,authorized_at_ms,dispatch_id)`,
`${APPROVED_ACTION_RECOVERY_AUTHORIZATION_AUTH_INDEX}(authentication_id,authorized_at_ms,dispatch_id)`,
],
constraints: [
'approved_action_recovery_authorization_assurance_check',
'approved_action_recovery_authorization_project_version_check',
'approved_action_recovery_authorization_binding_version_check',
'approved_action_recovery_authorization_recency_check',
],
maxAuthenticationAgeMs: MAX_APPROVED_ACTION_RECOVERY_AUTH_AGE_MS,
};
export const approvedActionRecoveryAuthorizationManifest = manifest;
export const approvedActionRecoveryAuthorizationMigration: Migration = {
id: '0024-approved-action-recovery-authorization',
checksum: createHash('sha256').update(JSON.stringify(manifest)).digest('hex'),
async up({ queryInterface, transaction }) {
await queryInterface.createTable(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
{
dispatch_id: {
type: DataTypes.STRING(64),
allowNull: false,
primaryKey: true,
references: {
model: APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
key: 'dispatch_id',
},
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
project_id: {
type: DataTypes.STRING(128),
allowNull: false,
references: { model: PROJECT_TABLE, key: 'id' },
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
mutation_id: { type: DataTypes.STRING(64), allowNull: false },
resolved_by_id: { type: DataTypes.STRING(255), allowNull: false },
authentication_id: { type: DataTypes.STRING(128), allowNull: false },
assurance: { type: DataTypes.STRING(32), allowNull: false },
authenticated_at_ms: { type: DataTypes.BIGINT, allowNull: false },
project_version: { type: DataTypes.INTEGER, allowNull: false },
binding_version: { type: DataTypes.INTEGER, allowNull: false },
authorized_at_ms: { type: DataTypes.BIGINT, allowNull: false },
fact_digest: { type: DataTypes.STRING(64), allowNull: false },
},
{ transaction },
);
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
{
fields: ['assurance'],
type: 'check',
where: {
assurance: { [Op.in]: APPROVED_ACTION_RECOVERY_STRONG_ASSURANCES },
},
name: 'approved_action_recovery_authorization_assurance_check',
transaction,
},
);
for (const [field, name] of [
[
'project_version',
'approved_action_recovery_authorization_project_version_check',
],
[
'binding_version',
'approved_action_recovery_authorization_binding_version_check',
],
] as const) {
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
{
fields: [field],
type: 'check',
where: { [field]: { [Op.between]: [1, 2_147_483_647] } },
name,
transaction,
},
);
}
await queryInterface.addConstraint(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
{
fields: ['authenticated_at_ms', 'authorized_at_ms'],
type: 'check',
where: {
authorized_at_ms: {
[Op.gte]: { [Op.col]: 'authenticated_at_ms' },
[Op.lte]: queryInterface.sequelize.literal(
`authenticated_at_ms + ${MAX_APPROVED_ACTION_RECOVERY_AUTH_AGE_MS}`,
),
},
},
name: 'approved_action_recovery_authorization_recency_check',
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
['mutation_id'],
{
name: APPROVED_ACTION_RECOVERY_AUTHORIZATION_MUTATION_INDEX,
unique: true,
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
['project_id', 'authorized_at_ms', 'dispatch_id'],
{
name: APPROVED_ACTION_RECOVERY_AUTHORIZATION_PROJECT_INDEX,
transaction,
},
);
await queryInterface.addIndex(
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
['authentication_id', 'authorized_at_ms', 'dispatch_id'],
{
name: APPROVED_ACTION_RECOVERY_AUTHORIZATION_AUTH_INDEX,
transaction,
},
);
},
};
@@ -0,0 +1,262 @@
import {
MigrationStreamHistoryCorruptionError,
type MigrationStreamRecord,
type MigrationStreamStore,
type MigrationStreamTransaction,
} from '../core/migrationStream';
import type {
PostgresClient as PostgresMigrationClient,
PostgresPool as PostgresMigrationPool,
PostgresQueryable as PostgresMigrationQueryable,
PostgresQueryResult as PostgresMigrationQueryResult,
} from '@qinglong/runtime-core';
export type {
PostgresClient as PostgresMigrationClient,
PostgresPool as PostgresMigrationPool,
PostgresQueryable as PostgresMigrationQueryable,
PostgresQueryResult as PostgresMigrationQueryResult,
} from '@qinglong/runtime-core';
export const POSTGRESQL_MAIN_MIGRATION_STREAM_ID = 'postgresql-main';
export const POSTGRESQL_MIGRATION_SCHEMA = 'ql3';
export const POSTGRESQL_MIGRATION_HISTORY_TABLE = 'schema_migrations';
const POSTGRESQL_MIGRATION_LOCK_KEY = [0x514c, 0x0300] as const;
const POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS = 15_000;
const POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS = 5_000;
const POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS = 15_000;
export type PostgresMigrationContext = PostgresMigrationQueryable;
export class PostgresMigrationLeaderUnavailableError extends Error {
constructor() {
super('PostgreSQL migration leader lock is unavailable');
this.name = 'PostgresMigrationLeaderUnavailableError';
}
}
interface HistoryRow extends Record<string, unknown> {
streamId: unknown;
dialect: unknown;
migrationId: unknown;
checksum: unknown;
appliedAtMs: unknown;
}
interface AdvisoryLockRow extends Record<string, unknown> {
acquired: unknown;
}
const CREATE_HISTORY_SQL = `
CREATE TABLE IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
migration_id varchar(128) PRIMARY KEY,
stream_id varchar(64) NOT NULL,
dialect varchar(16) NOT NULL
CONSTRAINT ql3_schema_migrations_dialect_check
CHECK (dialect = 'postgresql'),
checksum char(64) NOT NULL
CONSTRAINT ql3_schema_migrations_checksum_check
CHECK (checksum ~ '^[0-9a-f]{64}$'),
applied_at_ms bigint NOT NULL
CONSTRAINT ql3_schema_migrations_applied_at_check
CHECK (applied_at_ms >= 0)
)`.trim();
const SELECT_HISTORY_SQL = `
SELECT
stream_id AS "streamId",
dialect,
migration_id AS "migrationId",
checksum,
applied_at_ms AS "appliedAtMs"
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
WHERE migration_id = $1
`.trim();
const SELECT_ALL_HISTORY_SQL = `
SELECT
stream_id AS "streamId",
dialect,
migration_id AS "migrationId",
checksum,
applied_at_ms AS "appliedAtMs"
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
ORDER BY applied_at_ms, migration_id
`.trim();
const INSERT_HISTORY_SQL = `
INSERT INTO "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
migration_id,
stream_id,
dialect,
checksum,
applied_at_ms
)
VALUES ($1, $2, $3, $4, $5)
`.trim();
function parseAppliedAtMs(value: unknown, migrationId: string): number {
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
return value;
}
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
async function findHistoryRecord(
queryable: PostgresMigrationQueryable,
migrationId: string,
): Promise<MigrationStreamRecord | null> {
const result = await queryable.query<HistoryRow>(SELECT_HISTORY_SQL, [
migrationId,
]);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) {
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
const row = result.rows[0];
if (!row) {
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
return {
streamId: row.streamId as string,
dialect: row.dialect as 'postgresql',
migrationId: row.migrationId as string,
checksum: row.checksum as string,
appliedAtMs: parseAppliedAtMs(row.appliedAtMs, migrationId),
};
}
export async function readPostgresMigrationHistory(
queryable: PostgresMigrationQueryable,
): Promise<readonly MigrationStreamRecord[]> {
const result = await queryable.query<HistoryRow>(SELECT_ALL_HISTORY_SQL);
return result.rows.map((row) => ({
streamId: row.streamId as string,
dialect: row.dialect as 'postgresql',
migrationId: row.migrationId as string,
checksum: row.checksum as string,
appliedAtMs: parseAppliedAtMs(
row.appliedAtMs,
typeof row.migrationId === 'string' ? row.migrationId : 'unknown',
),
}));
}
async function configureTransaction(
client: PostgresMigrationClient,
): Promise<void> {
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
`${POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS}ms`,
]);
await client.query(`SELECT set_config('lock_timeout', $1, true)`, [
`${POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS}ms`,
]);
await client.query(
`SELECT set_config('idle_in_transaction_session_timeout', $1, true)`,
[`${POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS}ms`],
);
}
async function acquireLeaderLock(
client: PostgresMigrationClient,
): Promise<void> {
const result = await client.query<AdvisoryLockRow>(
'SELECT pg_try_advisory_xact_lock($1, $2) AS acquired',
POSTGRESQL_MIGRATION_LOCK_KEY,
);
if (result.rows.length !== 1 || result.rows[0]?.acquired !== true) {
throw new PostgresMigrationLeaderUnavailableError();
}
}
/**
* PostgreSQL migration history adapter without a runtime dependency on `pg`.
* The cluster-only package owns the concrete Pool binding; edge and standalone
* builds can compile this contract without installing or importing the driver.
*/
export class PostgresMigrationStreamStore
implements MigrationStreamStore<PostgresMigrationContext>
{
constructor(private readonly pool: PostgresMigrationPool) {}
async ensureHistory(): Promise<void> {
await this.withLeaderTransaction(async (client) => {
await client.query(
`CREATE SCHEMA IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"`,
);
await client.query(CREATE_HISTORY_SQL);
});
}
async findById(migrationId: string): Promise<MigrationStreamRecord | null> {
return findHistoryRecord(this.pool, migrationId);
}
async listAll(): Promise<readonly MigrationStreamRecord[]> {
return readPostgresMigrationHistory(this.pool);
}
async transaction<T>(
work: (
transaction: MigrationStreamTransaction<PostgresMigrationContext>,
) => Promise<T>,
): Promise<T> {
return this.withLeaderTransaction(async (client) =>
work({
context: client,
findById: (migrationId) => findHistoryRecord(client, migrationId),
insert: async (record) => {
if (
record.streamId !== POSTGRESQL_MAIN_MIGRATION_STREAM_ID ||
record.dialect !== 'postgresql'
) {
throw new TypeError(
'PostgreSQL migration history record has the wrong stream or dialect',
);
}
await client.query(INSERT_HISTORY_SQL, [
record.migrationId,
record.streamId,
record.dialect,
record.checksum,
record.appliedAtMs,
]);
},
}),
);
}
private async withLeaderTransaction<T>(
work: (client: PostgresMigrationClient) => Promise<T>,
): Promise<T> {
const client = await this.pool.connect();
let began = false;
try {
await client.query('BEGIN');
began = true;
await configureTransaction(client);
await acquireLeaderLock(client);
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) {
try {
await client.query('ROLLBACK');
} catch {
// Preserve the migration error. A broken connection is discarded by
// the concrete driver when release() runs.
}
}
throw error;
} finally {
client.release();
}
}
}
@@ -0,0 +1,97 @@
import type { ModelStatic, Sequelize, Transaction } from 'sequelize';
import type {
SchemaMigrationInstance,
SchemaMigrationAttributes,
} from '../../data/schemaMigration';
import type {
MigrationStreamRecord,
MigrationStreamStore,
MigrationStreamTransaction,
} from '../core/migrationStream';
import type { MigrationContext } from '../types';
export const SQLITE_MAIN_MIGRATION_STREAM_ID = 'sqlite-main';
function toRecord(instance: SchemaMigrationInstance): MigrationStreamRecord {
return {
streamId: SQLITE_MAIN_MIGRATION_STREAM_ID,
dialect: 'sqlite',
migrationId: instance.id,
checksum: instance.checksum,
appliedAtMs: Number(instance.applied_at),
};
}
/**
* Compatibility adapter for the existing Sequelize-owned SQLite connection.
* It preserves the legacy SchemaMigrations row shape while the generic core
* owns ordering, replay and checksum semantics.
*/
export class SequelizeSqliteMigrationStreamStore
implements MigrationStreamStore<MigrationContext>
{
constructor(
private readonly database: Sequelize,
private readonly migrationModel: ModelStatic<SchemaMigrationInstance>,
) {
if (database.getDialect() !== 'sqlite') {
throw new TypeError(
'SequelizeSqliteMigrationStreamStore requires SQLite',
);
}
}
async ensureHistory(): Promise<void> {
await this.migrationModel.sync();
}
async listAll(): Promise<readonly MigrationStreamRecord[]> {
return (await this.migrationModel.findAll()).map(toRecord);
}
async findById(migrationId: string): Promise<MigrationStreamRecord | null> {
const applied = await this.migrationModel.findByPk(migrationId);
return applied ? toRecord(applied) : null;
}
async transaction<T>(
work: (
transaction: MigrationStreamTransaction<MigrationContext>,
) => Promise<T>,
): Promise<T> {
return this.database.transaction(async (transaction) =>
work(this.createTransaction(transaction)),
);
}
private createTransaction(
transaction: Transaction,
): MigrationStreamTransaction<MigrationContext> {
return {
context: {
queryInterface: this.database.getQueryInterface(),
transaction,
},
findById: async (migrationId) => {
const applied = await this.migrationModel.findByPk(migrationId, {
transaction,
});
return applied ? toRecord(applied) : null;
},
insert: async (record) => {
if (
record.streamId !== SQLITE_MAIN_MIGRATION_STREAM_ID ||
record.dialect !== 'sqlite'
) {
throw new TypeError('SQLite migration record identity is invalid');
}
const attributes: SchemaMigrationAttributes = {
id: record.migrationId,
checksum: record.checksum,
applied_at: record.appliedAtMs,
};
await this.migrationModel.create(attributes, { transaction });
},
};
}
}
+361
View File
@@ -0,0 +1,361 @@
export const MIGRATION_STREAM_DIALECTS = ['sqlite', 'postgresql'] as const;
export type MigrationStreamDialect = (typeof MIGRATION_STREAM_DIALECTS)[number];
export const MIGRATION_ID_SCHEMES = [
'sqlite-numbered',
'postgres-prefixed',
] as const;
export type MigrationIdScheme = (typeof MIGRATION_ID_SCHEMES)[number];
export const MIGRATION_CHECKSUM_SCHEMES = ['sha256', 'legacy-opaque'] as const;
export type MigrationChecksumScheme =
(typeof MIGRATION_CHECKSUM_SCHEMES)[number];
export interface MigrationStreamRecord {
streamId: string;
dialect: MigrationStreamDialect;
migrationId: string;
checksum: string;
appliedAtMs: number;
}
export interface MigrationStreamStep<TContext> {
id: string;
checksum: string;
up(context: TContext): Promise<void>;
}
export interface MigrationStreamTransaction<TContext> {
readonly context: TContext;
findById(migrationId: string): Promise<MigrationStreamRecord | null>;
insert(record: MigrationStreamRecord): Promise<void>;
}
export interface MigrationStreamStore<TContext> {
ensureHistory(): Promise<void>;
listAll(): Promise<readonly MigrationStreamRecord[]>;
findById(migrationId: string): Promise<MigrationStreamRecord | null>;
transaction<T>(
work: (transaction: MigrationStreamTransaction<TContext>) => Promise<T>,
): Promise<T>;
}
export interface MigrationStreamDefinition<TContext> {
id: string;
dialect: MigrationStreamDialect;
migrationIdScheme: MigrationIdScheme;
checksumScheme: MigrationChecksumScheme;
migrations: readonly MigrationStreamStep<TContext>[];
}
export interface RunMigrationStreamOptions<TContext> {
stream: MigrationStreamDefinition<TContext>;
store: MigrationStreamStore<TContext>;
clock?: () => number;
logger?: { info(message: string): unknown };
}
const STREAM_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
const SQLITE_MIGRATION_ID_PATTERN = /^\d{4}-[a-z0-9][a-z0-9-]{0,122}$/;
const POSTGRES_MIGRATION_ID_PATTERN = /^pg-\d{4}-[a-z0-9][a-z0-9-]{0,119}$/;
const CHECKSUM_PATTERN = /^[0-9a-f]{64}$/;
const MAX_LEGACY_CHECKSUM_LENGTH = 255;
export class InvalidMigrationStreamError extends TypeError {
constructor(message: string) {
super(`Migration stream is invalid: ${message}`);
this.name = 'InvalidMigrationStreamError';
}
}
export class MigrationStreamChecksumMismatchError extends Error {
constructor(
readonly migrationId: string,
readonly databaseChecksum: string,
readonly codeChecksum: string,
) {
super(
`Migration checksum mismatch: ${migrationId} ` +
`(database=${databaseChecksum}, code=${codeChecksum})`,
);
this.name = 'MigrationStreamChecksumMismatchError';
}
}
export class MigrationStreamHistoryCorruptionError extends Error {
constructor(readonly migrationId: string) {
super(`Migration history is corrupt: ${migrationId}`);
this.name = 'MigrationStreamHistoryCorruptionError';
}
}
export class MigrationStreamAheadOfCodeError extends Error {
constructor(readonly migrationId: string) {
super(`Migration history is ahead of this code: ${migrationId}`);
this.name = 'MigrationStreamAheadOfCodeError';
}
}
function exactKeys(value: object, expected: readonly string[]): boolean {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
return (
keys.length === canonical.length &&
keys.every((key, index) => key === canonical[index])
);
}
function checksumIsValid(
scheme: MigrationChecksumScheme,
value: unknown,
): value is string {
if (typeof value !== 'string') return false;
return scheme === 'sha256'
? CHECKSUM_PATTERN.test(value)
: value.length <= MAX_LEGACY_CHECKSUM_LENGTH;
}
function validateDefinition<TContext>(
stream: MigrationStreamDefinition<TContext>,
): void {
if (!stream || typeof stream !== 'object' || Array.isArray(stream)) {
throw new InvalidMigrationStreamError('definition must be an object');
}
if (
!exactKeys(stream, [
'id',
'dialect',
'migrationIdScheme',
'checksumScheme',
'migrations',
])
) {
throw new InvalidMigrationStreamError('definition shape is invalid');
}
if (!STREAM_ID_PATTERN.test(stream.id)) {
throw new InvalidMigrationStreamError('id is invalid');
}
if (!MIGRATION_STREAM_DIALECTS.includes(stream.dialect)) {
throw new InvalidMigrationStreamError('dialect is invalid');
}
if (!MIGRATION_ID_SCHEMES.includes(stream.migrationIdScheme)) {
throw new InvalidMigrationStreamError('migrationIdScheme is invalid');
}
if (!MIGRATION_CHECKSUM_SCHEMES.includes(stream.checksumScheme)) {
throw new InvalidMigrationStreamError('checksumScheme is invalid');
}
if (
(stream.dialect === 'sqlite' &&
stream.migrationIdScheme !== 'sqlite-numbered') ||
(stream.dialect === 'postgresql' &&
stream.migrationIdScheme !== 'postgres-prefixed')
) {
throw new InvalidMigrationStreamError(
'migrationIdScheme does not match dialect',
);
}
if (!Array.isArray(stream.migrations)) {
throw new InvalidMigrationStreamError('migrations must be an array');
}
const ids = new Set<string>();
for (const migration of stream.migrations) {
if (
!migration ||
typeof migration !== 'object' ||
Array.isArray(migration) ||
!exactKeys(migration, ['id', 'checksum', 'up']) ||
!(
stream.migrationIdScheme === 'sqlite-numbered'
? SQLITE_MIGRATION_ID_PATTERN
: POSTGRES_MIGRATION_ID_PATTERN
).test(migration.id) ||
!checksumIsValid(stream.checksumScheme, migration.checksum) ||
typeof migration.up !== 'function'
) {
throw new InvalidMigrationStreamError('migration is invalid');
}
if (ids.has(migration.id)) {
throw new InvalidMigrationStreamError(
`duplicate migration id: ${migration.id}`,
);
}
ids.add(migration.id);
}
}
function validateRecord(
value: MigrationStreamRecord,
stream: MigrationStreamDefinition<unknown>,
migrationId: string,
): void {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exactKeys(value, [
'streamId',
'dialect',
'migrationId',
'checksum',
'appliedAtMs',
]) ||
value.streamId !== stream.id ||
value.dialect !== stream.dialect ||
value.migrationId !== migrationId ||
!checksumIsValid(stream.checksumScheme, value.checksum) ||
!Number.isSafeInteger(value.appliedAtMs) ||
value.appliedAtMs < 0
) {
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
}
function assertChecksum(
record: MigrationStreamRecord,
migration: MigrationStreamStep<unknown>,
): void {
if (record.checksum !== migration.checksum) {
throw new MigrationStreamChecksumMismatchError(
migration.id,
record.checksum,
migration.checksum,
);
}
}
export function auditMigrationStreamHistory<TContext>(
history: readonly MigrationStreamRecord[],
stream: MigrationStreamDefinition<TContext>,
): ReadonlySet<string> {
if (!Array.isArray(history)) {
throw new MigrationStreamHistoryCorruptionError('history');
}
const migrationsById = new Map(
stream.migrations.map((migration, index) => [
migration.id,
{ migration, index },
]),
);
const appliedIds = new Set<string>();
const appliedIndexes = new Set<number>();
for (const record of history) {
const expected = migrationsById.get(record?.migrationId);
if (!expected) {
throw new MigrationStreamAheadOfCodeError(
record?.migrationId ?? 'unknown',
);
}
if (appliedIds.has(record.migrationId)) {
throw new MigrationStreamHistoryCorruptionError(record.migrationId);
}
validateRecord(
record,
stream as MigrationStreamDefinition<unknown>,
expected.migration.id,
);
assertChecksum(record, expected.migration as MigrationStreamStep<unknown>);
appliedIds.add(record.migrationId);
appliedIndexes.add(expected.index);
}
for (let index = 0; index < appliedIndexes.size; index += 1) {
if (!appliedIndexes.has(index)) {
throw new MigrationStreamHistoryCorruptionError(
stream.migrations[index].id,
);
}
}
return appliedIds;
}
/**
* Dialect-neutral migration ordering and history semantics. Concrete stores own
* SQL, leader election and transaction APIs; this core never imports a driver.
*/
export async function runMigrationStream<TContext>(
options: RunMigrationStreamOptions<TContext>,
): Promise<void> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidMigrationStreamError('options must be an object');
}
const expectedKeys = [
'stream',
'store',
...(options.clock === undefined ? [] : ['clock']),
...(options.logger === undefined ? [] : ['logger']),
];
if (!exactKeys(options, expectedKeys)) {
throw new InvalidMigrationStreamError('options shape is invalid');
}
validateDefinition(options.stream);
if (
!options.store ||
typeof options.store !== 'object' ||
typeof options.store.ensureHistory !== 'function' ||
typeof options.store.listAll !== 'function' ||
typeof options.store.findById !== 'function' ||
typeof options.store.transaction !== 'function'
) {
throw new InvalidMigrationStreamError('store is invalid');
}
const clock = options.clock ?? Date.now;
if (typeof clock !== 'function') {
throw new InvalidMigrationStreamError('clock is invalid');
}
await options.store.ensureHistory();
const appliedAtStart = auditMigrationStreamHistory(
await options.store.listAll(),
options.stream,
);
for (const migration of options.stream.migrations) {
if (appliedAtStart.has(migration.id)) continue;
const applied = await options.store.findById(migration.id);
if (applied) {
validateRecord(
applied,
options.stream as MigrationStreamDefinition<unknown>,
migration.id,
);
assertChecksum(applied, migration as MigrationStreamStep<unknown>);
continue;
}
let appliedNow = false;
await options.store.transaction(async (transaction) => {
const appliedInsideTransaction = await transaction.findById(migration.id);
if (appliedInsideTransaction) {
validateRecord(
appliedInsideTransaction,
options.stream as MigrationStreamDefinition<unknown>,
migration.id,
);
assertChecksum(
appliedInsideTransaction,
migration as MigrationStreamStep<unknown>,
);
return;
}
await migration.up(transaction.context);
const appliedAtMs = clock();
if (!Number.isSafeInteger(appliedAtMs) || appliedAtMs < 0) {
throw new InvalidMigrationStreamError(
'clock must return a non-negative safe integer',
);
}
await transaction.insert({
streamId: options.stream.id,
dialect: options.stream.dialect,
migrationId: migration.id,
checksum: migration.checksum,
appliedAtMs,
});
appliedNow = true;
});
if (appliedNow) {
options.logger?.info(
`[migration:${options.stream.id}] Applied ${migration.id}`,
);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import { runSchemaMigration } from './0002-run-schema';
import { runningInstanceRunReferenceMigration } from './0003-running-instance-run-reference';
import { runCancellationRequestMigration } from './0004-run-cancellation-request';
import { runCancellationDispatchMigration } from './0005-run-cancellation-dispatch';
import { runAttemptDeadlineMigration } from './0006-run-attempt-deadline';
import { completionReceiptJournalMigration } from './0007-completion-receipt-journal';
import { workerRegistryMigration } from './0008-worker-registry';
import { runDispatchLeaseMigration } from './0009-run-dispatch-lease';
import { runDispatchCandidateMigration } from './0010-run-dispatch-candidates';
import { runRetryPolicyMigration } from './0011-run-retry-policy';
import { taskExecutionRevisionMigration } from './0012-task-execution-revisions';
import { localExecutionContextRecipeMigration } from './0013-local-execution-context-recipes';
import { localSecretEnvelopeMigration } from './0014-local-secret-envelopes';
import { localArtifactRetentionMigration } from './0015-local-artifact-retention';
import { localArtifactMaintenanceCursorMigration } from './0016-local-artifact-maintenance-cursor';
import { projectPolicyMigration } from './0017-project-policy';
import { projectOwnerBootstrapMigration } from './0018-project-owner-bootstrap';
import { identityDirectoryMigration } from './0019-identity-directory';
import { approvalRequestMigration } from './0020-approval-requests';
import { approvedActionDispatchExecutionMigration } from './0021-approved-action-dispatch-executions';
import { approvedActionRecoveryMigration } from './0022-approved-action-recovery';
import { approvedRunActionReceiptMigration } from './0023-approved-run-action-receipts';
import { approvedActionRecoveryAuthorizationMigration } from './0024-approved-action-recovery-authorization';
import { legacyColumnsMigration } from './0001-legacy-columns';
import type { Migration } from './types';
export const migrations: Migration[] = [
legacyColumnsMigration,
runSchemaMigration,
runningInstanceRunReferenceMigration,
runCancellationRequestMigration,
runCancellationDispatchMigration,
runAttemptDeadlineMigration,
completionReceiptJournalMigration,
workerRegistryMigration,
runDispatchLeaseMigration,
runDispatchCandidateMigration,
runRetryPolicyMigration,
taskExecutionRevisionMigration,
localExecutionContextRecipeMigration,
localSecretEnvelopeMigration,
localArtifactRetentionMigration,
localArtifactMaintenanceCursorMigration,
projectPolicyMigration,
projectOwnerBootstrapMigration,
identityDirectoryMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
approvedActionRecoveryMigration,
approvedRunActionReceiptMigration,
approvedActionRecoveryAuthorizationMigration,
];
+21
View File
@@ -0,0 +1,21 @@
import {
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
type PostgresMigrationContext,
} from '../adapters/postgresMigrationStreamStore';
import type { MigrationStreamDefinition } from '../core/migrationStream';
import { pg0001SchemaCapabilityMigration } from './pg-0001-schema-capability';
import { pg0002RunCoreMigration } from './pg-0002-run-core';
import { pg0003RunRetryPolicyMigration } from './pg-0003-run-retry-policy';
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
Object.freeze({
id: POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
dialect: 'postgresql',
migrationIdScheme: 'postgres-prefixed',
checksumScheme: 'sha256',
migrations: Object.freeze([
pg0001SchemaCapabilityMigration,
pg0002RunCoreMigration,
pg0003RunRetryPolicyMigration,
]),
});
@@ -0,0 +1,28 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_SCHEMA_CAPABILITY_TABLE = 'schema_capabilities';
export const pg0001SchemaCapabilityMigration = definePostgresSqlMigration({
id: 'pg-0001-schema-capability',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_SCHEMA_CAPABILITY_TABLE}" (
contract_name varchar(64) PRIMARY KEY,
contract_version integer NOT NULL
CONSTRAINT ql3_schema_capabilities_version_check
CHECK (contract_version >= 0),
migration_id varchar(128) NOT NULL,
capabilities jsonb NOT NULL
CONSTRAINT ql3_schema_capabilities_payload_check
CHECK (jsonb_typeof(capabilities) = 'object'),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_schema_capabilities_updated_at_check
CHECK (updated_at_ms >= 0),
CONSTRAINT ql3_schema_capabilities_migration_fk
FOREIGN KEY (migration_id)
REFERENCES "ql3"."schema_migrations" (migration_id)
DEFERRABLE INITIALLY DEFERRED
)
`.trim(),
],
});
@@ -0,0 +1,224 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_RUN_TABLE = 'runs';
export const POSTGRESQL_RUN_ATTEMPT_TABLE = 'run_attempts';
export const POSTGRESQL_RUN_EVENT_TABLE = 'run_events';
export const pg0002RunCoreMigration = definePostgresSqlMigration({
id: 'pg-0002-run-core',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_TABLE}" (
id varchar(36) PRIMARY KEY,
project_id varchar(128) NOT NULL,
task_id varchar(255) NOT NULL,
task_revision varchar(128) NOT NULL,
task_name varchar(255),
task_snapshot_ref varchar(512),
parent_run_id varchar(36),
retry_of_run_id varchar(36),
trigger_id varchar(36),
trigger_type varchar(64) NOT NULL,
execution_origin varchar(64) NOT NULL,
execution_owner varchar(16) NOT NULL
CONSTRAINT ql3_runs_execution_owner_check
CHECK (execution_owner = 'runtime'),
triggered_by varchar(255),
request_id varchar(128),
scheduled_for_ms bigint
CONSTRAINT ql3_runs_scheduled_for_check
CHECK (scheduled_for_ms IS NULL OR scheduled_for_ms >= 0),
status varchar(32) NOT NULL
CONSTRAINT ql3_runs_status_check
CHECK (status IN (
'created', 'queued', 'dispatching', 'running', 'waiting_approval',
'retry_wait', 'lost', 'succeeded', 'failed', 'cancelled', 'timed_out'
)),
version integer NOT NULL DEFAULT 0
CONSTRAINT ql3_runs_version_check
CHECK (version >= 0),
event_sequence integer NOT NULL DEFAULT 0
CONSTRAINT ql3_runs_event_sequence_check
CHECK (event_sequence >= 0),
priority integer NOT NULL DEFAULT 0,
idempotency_key varchar(255),
input_ref varchar(512),
output_ref varchar(512),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_runs_created_at_check
CHECK (created_at_ms >= 0),
queued_at_ms bigint
CONSTRAINT ql3_runs_queued_at_check
CHECK (queued_at_ms IS NULL OR queued_at_ms >= 0),
started_at_ms bigint
CONSTRAINT ql3_runs_started_at_check
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
finished_at_ms bigint
CONSTRAINT ql3_runs_finished_at_check
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
cancel_requested_at_ms bigint
CONSTRAINT ql3_runs_cancel_requested_at_check
CHECK (cancel_requested_at_ms IS NULL OR cancel_requested_at_ms >= 0),
cancel_reason varchar(16)
CONSTRAINT ql3_runs_cancel_reason_check
CHECK (cancel_reason IS NULL OR cancel_reason IN (
'user', 'policy', 'shutdown', 'reconcile', 'timeout'
)),
error_code varchar(128),
error_summary varchar(1024),
CONSTRAINT ql3_runs_parent_fk
FOREIGN KEY (parent_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
CONSTRAINT ql3_runs_retry_of_fk
FOREIGN KEY (retry_of_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_runs_project_idempotency_uidx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, idempotency_key)
WHERE idempotency_key IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_runs_project_created_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_runs_task_created_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (task_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_runs_dispatch_candidates_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (priority DESC, queued_at_ms, id)
WHERE execution_owner = 'runtime'
AND status IN ('queued', 'dispatching')
AND cancel_requested_at_ms IS NULL
AND queued_at_ms IS NOT NULL
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (
id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
step_run_id varchar(36),
attempt integer NOT NULL
CONSTRAINT ql3_run_attempts_attempt_check
CHECK (attempt >= 1),
status varchar(32) NOT NULL
CONSTRAINT ql3_run_attempts_status_check
CHECK (status IN (
'claimed', 'starting', 'running', 'succeeded', 'failed',
'cancelled', 'timed_out', 'lost'
)),
executor_type varchar(64) NOT NULL,
worker_id varchar(128),
executor_handle varchar(2048),
pid integer
CONSTRAINT ql3_run_attempts_pid_check
CHECK (pid IS NULL OR pid >= 1),
log_artifact_id varchar(36),
lease_token varchar(128),
lease_expires_at_ms bigint
CONSTRAINT ql3_run_attempts_lease_expiry_check
CHECK (lease_expires_at_ms IS NULL OR lease_expires_at_ms >= 0),
deadline_at_ms bigint
CONSTRAINT ql3_run_attempts_deadline_check
CHECK (deadline_at_ms IS NULL OR deadline_at_ms >= 0),
callback_token_hash varchar(128),
callback_sequence integer NOT NULL DEFAULT 0
CONSTRAINT ql3_run_attempts_callback_sequence_check
CHECK (callback_sequence >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_attempts_created_at_check
CHECK (created_at_ms >= 0),
started_at_ms bigint
CONSTRAINT ql3_run_attempts_started_at_check
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
finished_at_ms bigint
CONSTRAINT ql3_run_attempts_finished_at_check
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
exit_code integer,
error_code varchar(128),
error_summary varchar(1024),
CONSTRAINT ql3_run_attempts_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_attempts_run_attempt_uidx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (run_id, attempt)
`.trim(),
`
CREATE INDEX ql3_run_attempts_dispatch_candidates_idx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (status, run_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_run_attempts_lease_idx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (lease_expires_at_ms, id)
WHERE lease_expires_at_ms IS NOT NULL
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (
id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
sequence integer NOT NULL
CONSTRAINT ql3_run_events_sequence_check
CHECK (sequence >= 1),
type varchar(128) NOT NULL,
dedupe_key varchar(255),
actor_type varchar(64) NOT NULL
CONSTRAINT ql3_run_events_actor_type_check
CHECK (actor_type IN (
'user', 'api_app', 'trigger', 'agent', 'mcp_client', 'worker',
'executor', 'system', 'legacy_shell', 'scheduler', 'reconciler',
'compatibility'
)),
actor_id varchar(255),
attempt_id varchar(36),
step_run_id varchar(36),
payload jsonb NOT NULL
CONSTRAINT ql3_run_events_payload_check
CHECK (jsonb_typeof(payload) = 'object'),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_events_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_run_events_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
CONSTRAINT ql3_run_events_attempt_fk
FOREIGN KEY (attempt_id) REFERENCES "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_events_run_sequence_uidx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, sequence)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_events_run_dedupe_uidx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, dedupe_key)
WHERE dedupe_key IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_run_events_run_created_idx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, created_at_ms, id)
`.trim(),
`
INSERT INTO "ql3"."schema_capabilities" (
contract_name,
contract_version,
migration_id,
capabilities,
updated_at_ms
)
VALUES (
'control-core',
1,
'pg-0002-run-core',
'{"run_core":1}'::jsonb,
floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
)
ON CONFLICT (contract_name) DO UPDATE
SET contract_version = EXCLUDED.contract_version,
migration_id = EXCLUDED.migration_id,
capabilities = EXCLUDED.capabilities,
updated_at_ms = EXCLUDED.updated_at_ms
WHERE "schema_capabilities".contract_version < EXCLUDED.contract_version
`.trim(),
],
});
@@ -0,0 +1,78 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_RUN_RETRY_POLICY_TABLE = 'run_retry_policies';
export const pg0003RunRetryPolicyMigration = definePostgresSqlMigration({
id: 'pg-0003-run-retry-policy',
statements: [
`
ALTER TABLE "ql3"."runs"
ADD COLUMN legacy_cron_id integer
CONSTRAINT ql3_runs_legacy_cron_id_check
CHECK (legacy_cron_id IS NULL OR legacy_cron_id >= 1)
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (
run_id varchar(36) PRIMARY KEY,
max_attempts integer NOT NULL
CONSTRAINT ql3_run_retry_policies_max_attempts_check
CHECK (max_attempts BETWEEN 1 AND 16),
retry_on_lost boolean NOT NULL,
safety varchar(16) NOT NULL
CONSTRAINT ql3_run_retry_policies_safety_check
CHECK (safety IN ('unknown', 'idempotent', 'deduplicated')),
backoff_base_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_backoff_base_check
CHECK (backoff_base_ms BETWEEN 0 AND 86400000),
backoff_max_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_backoff_max_check
CHECK (backoff_max_ms BETWEEN backoff_base_ms AND 86400000),
next_attempt_at_ms bigint
CONSTRAINT ql3_run_retry_policies_next_attempt_check
CHECK (next_attempt_at_ms IS NULL OR next_attempt_at_ms >= 0),
version integer NOT NULL DEFAULT 0
CONSTRAINT ql3_run_retry_policies_version_check
CHECK (version >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_created_at_check
CHECK (created_at_ms >= 0),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_updated_at_check
CHECK (updated_at_ms >= created_at_ms),
CONSTRAINT ql3_run_retry_policies_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE
)
`.trim(),
`
CREATE INDEX ql3_run_retry_policies_due_idx
ON "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (next_attempt_at_ms, run_id)
WHERE next_attempt_at_ms IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_runs_lost_retry_idx
ON "ql3"."runs" (execution_owner, status, id)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 2,
migration_id = 'pg-0003-run-retry-policy',
capabilities = '{"run_core":1,"run_retry_policy":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 1
AND migration_id = 'pg-0002-run-core'
AND capabilities = '{"run_core":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 1'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,206 @@
export interface PostgresSchemaContractTable {
readonly name: string;
readonly columns: readonly string[];
}
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 2;
readonly migrationId: 'pg-0003-run-retry-policy';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
run_core: 1;
run_retry_policy: 1;
}>;
readonly tables: readonly PostgresSchemaContractTable[];
readonly indexes: readonly string[];
readonly checks: readonly string[];
readonly foreignKeys: readonly string[];
}
function table(
name: string,
columns: readonly string[],
): PostgresSchemaContractTable {
return Object.freeze({ name, columns: Object.freeze([...columns]) });
}
export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 2,
migrationId: 'pg-0003-run-retry-policy',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({ run_core: 1, run_retry_policy: 1 }),
tables: Object.freeze([
table('schema_migrations', [
'migration_id',
'stream_id',
'dialect',
'checksum',
'applied_at_ms',
]),
table('schema_capabilities', [
'contract_name',
'contract_version',
'migration_id',
'capabilities',
'updated_at_ms',
]),
table('runs', [
'id',
'project_id',
'task_id',
'task_revision',
'task_name',
'task_snapshot_ref',
'legacy_cron_id',
'parent_run_id',
'retry_of_run_id',
'trigger_id',
'trigger_type',
'execution_origin',
'execution_owner',
'triggered_by',
'request_id',
'scheduled_for_ms',
'status',
'version',
'event_sequence',
'priority',
'idempotency_key',
'input_ref',
'output_ref',
'created_at_ms',
'queued_at_ms',
'started_at_ms',
'finished_at_ms',
'cancel_requested_at_ms',
'cancel_reason',
'error_code',
'error_summary',
]),
table('run_attempts', [
'id',
'run_id',
'step_run_id',
'attempt',
'status',
'executor_type',
'worker_id',
'executor_handle',
'pid',
'log_artifact_id',
'lease_token',
'lease_expires_at_ms',
'deadline_at_ms',
'callback_token_hash',
'callback_sequence',
'created_at_ms',
'started_at_ms',
'finished_at_ms',
'exit_code',
'error_code',
'error_summary',
]),
table('run_events', [
'id',
'run_id',
'sequence',
'type',
'dedupe_key',
'actor_type',
'actor_id',
'attempt_id',
'step_run_id',
'payload',
'created_at_ms',
]),
table('run_retry_policies', [
'run_id',
'max_attempts',
'retry_on_lost',
'safety',
'backoff_base_ms',
'backoff_max_ms',
'next_attempt_at_ms',
'version',
'created_at_ms',
'updated_at_ms',
]),
]),
indexes: Object.freeze([
'schema_migrations_pkey',
'schema_capabilities_pkey',
'runs_pkey',
'ql3_runs_project_idempotency_uidx',
'ql3_runs_project_created_idx',
'ql3_runs_task_created_idx',
'ql3_runs_dispatch_candidates_idx',
'run_attempts_pkey',
'ql3_run_attempts_run_attempt_uidx',
'ql3_run_attempts_dispatch_candidates_idx',
'ql3_run_attempts_lease_idx',
'run_events_pkey',
'ql3_run_events_run_sequence_uidx',
'ql3_run_events_run_dedupe_uidx',
'ql3_run_events_run_created_idx',
'run_retry_policies_pkey',
'ql3_run_retry_policies_due_idx',
'ql3_runs_lost_retry_idx',
]),
checks: Object.freeze([
'ql3_schema_migrations_dialect_check',
'ql3_schema_migrations_checksum_check',
'ql3_schema_migrations_applied_at_check',
'ql3_schema_capabilities_version_check',
'ql3_schema_capabilities_payload_check',
'ql3_schema_capabilities_updated_at_check',
'ql3_runs_legacy_cron_id_check',
'ql3_runs_execution_owner_check',
'ql3_runs_scheduled_for_check',
'ql3_runs_status_check',
'ql3_runs_version_check',
'ql3_runs_event_sequence_check',
'ql3_runs_created_at_check',
'ql3_runs_queued_at_check',
'ql3_runs_started_at_check',
'ql3_runs_finished_at_check',
'ql3_runs_cancel_requested_at_check',
'ql3_runs_cancel_reason_check',
'ql3_run_attempts_attempt_check',
'ql3_run_attempts_status_check',
'ql3_run_attempts_pid_check',
'ql3_run_attempts_lease_expiry_check',
'ql3_run_attempts_deadline_check',
'ql3_run_attempts_callback_sequence_check',
'ql3_run_attempts_created_at_check',
'ql3_run_attempts_started_at_check',
'ql3_run_attempts_finished_at_check',
'ql3_run_events_sequence_check',
'ql3_run_events_actor_type_check',
'ql3_run_events_payload_check',
'ql3_run_events_created_at_check',
'ql3_run_retry_policies_max_attempts_check',
'ql3_run_retry_policies_safety_check',
'ql3_run_retry_policies_backoff_base_check',
'ql3_run_retry_policies_backoff_max_check',
'ql3_run_retry_policies_next_attempt_check',
'ql3_run_retry_policies_version_check',
'ql3_run_retry_policies_created_at_check',
'ql3_run_retry_policies_updated_at_check',
]),
foreignKeys: Object.freeze([
'ql3_schema_capabilities_migration_fk',
'ql3_runs_parent_fk',
'ql3_runs_retry_of_fk',
'ql3_run_attempts_run_fk',
'ql3_run_events_run_fk',
'ql3_run_events_attempt_fk',
'ql3_run_retry_policies_run_fk',
]),
});
@@ -0,0 +1,470 @@
import {
readPostgresMigrationHistory,
type PostgresMigrationQueryable,
} from '../adapters/postgresMigrationStreamStore';
import { auditMigrationStreamHistory } from '../core/migrationStream';
import { postgresqlMainMigrationStream } from '.';
import {
postgresqlControlSchemaContract,
type PostgresSchemaContract,
} from './schemaContract';
export const POSTGRES_SCHEMA_READINESS_ERROR_CODES = [
'server_version_unsupported',
'server_not_writable_primary',
'migration_history_invalid',
'capability_invalid',
'schema_contract_invalid',
'runtime_role_invalid',
] as const;
export type PostgresSchemaReadinessErrorCode =
(typeof POSTGRES_SCHEMA_READINESS_ERROR_CODES)[number];
export class PostgresSchemaReadinessError extends Error {
constructor(
readonly code: PostgresSchemaReadinessErrorCode,
readonly facts: readonly string[] = [],
) {
super(`PostgreSQL schema is not ready: ${code}`);
this.name = 'PostgresSchemaReadinessError';
}
}
export interface PostgresSchemaReadinessReport {
readonly ready: true;
readonly writablePrimary: true;
readonly serverVersionNum: number;
readonly serverMajor: number;
readonly currentUser: string;
readonly contractName: string;
readonly contractVersion: number;
readonly migrationIds: readonly string[];
}
interface ServerRow extends Record<string, unknown> {
serverVersionNum: unknown;
currentUser: unknown;
inRecovery: unknown;
transactionReadOnly: unknown;
}
interface CapabilityRow extends Record<string, unknown> {
contractName: unknown;
contractVersion: unknown;
migrationId: unknown;
capabilities: unknown;
}
interface ColumnRow extends Record<string, unknown> {
tableName: unknown;
columnName: unknown;
}
interface IndexRow extends Record<string, unknown> {
indexName: unknown;
}
interface ConstraintRow extends Record<string, unknown> {
constraintName: unknown;
constraintType: unknown;
}
interface SchemaPrivilegeRow extends Record<string, unknown> {
schemaUsage: unknown;
schemaCreate: unknown;
}
interface TablePrivilegeRow extends Record<string, unknown> {
tableName: unknown;
selectAllowed: unknown;
insertAllowed: unknown;
updateAllowed: unknown;
deleteAllowed: unknown;
isOwner: unknown;
}
const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
schema_migrations: Object.freeze({
select: true,
insert: false,
update: false,
delete: false,
}),
schema_capabilities: Object.freeze({
select: true,
insert: false,
update: false,
delete: false,
}),
runs: Object.freeze({
select: true,
insert: true,
update: true,
delete: false,
}),
run_attempts: Object.freeze({
select: true,
insert: true,
update: true,
delete: false,
}),
run_events: Object.freeze({
select: true,
insert: true,
update: false,
delete: false,
}),
run_retry_policies: Object.freeze({
select: true,
insert: true,
update: true,
delete: false,
}),
});
function safeInteger(value: unknown): number | null {
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
return Number.isSafeInteger(parsed) ? parsed : null;
}
return null;
}
function sorted(values: Iterable<string>): string[] {
return [...values].sort((left, right) => left.localeCompare(right));
}
function exactJsonObject(
actual: unknown,
expected: Readonly<Record<string, unknown>>,
): boolean {
if (!actual || typeof actual !== 'object' || Array.isArray(actual)) {
return false;
}
const actualObject = actual as Record<string, unknown>;
const actualKeys = sorted(Object.keys(actualObject));
const expectedKeys = sorted(Object.keys(expected));
return (
actualKeys.length === expectedKeys.length &&
actualKeys.every(
(key, index) =>
key === expectedKeys[index] && actualObject[key] === expected[key],
)
);
}
async function readServer(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<{
writablePrimary: true;
serverVersionNum: number;
serverMajor: number;
currentUser: string;
}> {
const result = await queryable.query<ServerRow>(
`
SELECT
current_setting('server_version_num') AS "serverVersionNum",
current_user AS "currentUser",
pg_is_in_recovery() AS "inRecovery",
current_setting('transaction_read_only') AS "transactionReadOnly"
`.trim(),
);
const row = result.rows[0];
const serverVersionNum = safeInteger(row?.serverVersionNum);
const currentUser = row?.currentUser;
const inRecovery = row?.inRecovery;
const transactionReadOnly = row?.transactionReadOnly;
const serverMajor =
serverVersionNum === null ? null : Math.floor(serverVersionNum / 10_000);
if (
result.rows.length !== 1 ||
serverVersionNum === null ||
serverMajor === null ||
serverMajor < contract.minimumServerMajor ||
serverMajor > contract.maximumServerMajor ||
typeof currentUser !== 'string' ||
currentUser.length === 0
) {
throw new PostgresSchemaReadinessError('server_version_unsupported', [
String(serverVersionNum ?? 'invalid'),
]);
}
if (inRecovery !== false || transactionReadOnly !== 'off') {
throw new PostgresSchemaReadinessError('server_not_writable_primary', [
`in-recovery:${String(inRecovery)}`,
`transaction-read-only:${String(transactionReadOnly)}`,
]);
}
return {
writablePrimary: true,
serverVersionNum,
serverMajor,
currentUser,
};
}
async function assertHistory(
queryable: PostgresMigrationQueryable,
): Promise<readonly string[]> {
const history = await readPostgresMigrationHistory(queryable);
try {
auditMigrationStreamHistory(history, postgresqlMainMigrationStream);
return Object.freeze(history.map(({ migrationId }) => migrationId));
} catch (error) {
throw new PostgresSchemaReadinessError('migration_history_invalid', [
error instanceof Error ? error.name : 'UnknownError',
]);
}
}
async function assertCapability(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<void> {
const result = await queryable.query<CapabilityRow>(
`
SELECT
contract_name AS "contractName",
contract_version AS "contractVersion",
migration_id AS "migrationId",
capabilities
FROM "${contract.schema}"."schema_capabilities"
WHERE contract_name = $1
`.trim(),
[contract.contractName],
);
const row = result.rows[0];
if (
result.rows.length !== 1 ||
!row ||
row.contractName !== contract.contractName ||
safeInteger(row.contractVersion) !== contract.contractVersion ||
row.migrationId !== contract.migrationId ||
!exactJsonObject(row.capabilities, contract.capabilities)
) {
throw new PostgresSchemaReadinessError('capability_invalid');
}
}
async function assertSchemaContract(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<void> {
const [columnsResult, indexesResult, constraintsResult] = await Promise.all([
queryable.query<ColumnRow>(
`
SELECT table_name AS "tableName", column_name AS "columnName"
FROM information_schema.columns
WHERE table_schema = $1
ORDER BY table_name, ordinal_position
`.trim(),
[contract.schema],
),
queryable.query<IndexRow>(
`
SELECT indexname AS "indexName"
FROM pg_indexes
WHERE schemaname = $1
ORDER BY indexname
`.trim(),
[contract.schema],
),
queryable.query<ConstraintRow>(
`
SELECT
constraints.conname AS "constraintName",
CASE constraints.contype
WHEN 'c' THEN 'check'
WHEN 'f' THEN 'foreign_key'
END AS "constraintType"
FROM pg_constraint constraints
JOIN pg_class tables ON tables.oid = constraints.conrelid
JOIN pg_namespace schemas ON schemas.oid = tables.relnamespace
WHERE schemas.nspname = $1
AND constraints.contype IN ('c', 'f')
ORDER BY constraints.contype, constraints.conname
`.trim(),
[contract.schema],
),
]);
const actualTables = new Map<string, Set<string>>();
for (const row of columnsResult.rows) {
if (
typeof row.tableName !== 'string' ||
typeof row.columnName !== 'string'
) {
throw new PostgresSchemaReadinessError('schema_contract_invalid');
}
const columns = actualTables.get(row.tableName) ?? new Set<string>();
columns.add(row.columnName);
actualTables.set(row.tableName, columns);
}
const expectedTables = new Map(
contract.tables.map((table) => [table.name, new Set(table.columns)]),
);
const findings: string[] = [];
for (const [tableName, expectedColumns] of expectedTables) {
const actualColumns = actualTables.get(tableName);
if (!actualColumns) {
findings.push(`missing-table:${tableName}`);
continue;
}
for (const column of expectedColumns) {
if (!actualColumns.has(column)) {
findings.push(`missing-column:${tableName}.${column}`);
}
}
for (const column of actualColumns) {
if (!expectedColumns.has(column)) {
findings.push(`unknown-column:${tableName}.${column}`);
}
}
}
for (const tableName of actualTables.keys()) {
if (!expectedTables.has(tableName))
findings.push(`unknown-table:${tableName}`);
}
const actualIndexes = new Set<string>();
for (const row of indexesResult.rows) {
if (typeof row.indexName !== 'string') {
throw new PostgresSchemaReadinessError('schema_contract_invalid');
}
actualIndexes.add(row.indexName);
}
const expectedIndexes = new Set(contract.indexes);
for (const index of expectedIndexes) {
if (!actualIndexes.has(index)) findings.push(`missing-index:${index}`);
}
for (const index of actualIndexes) {
if (!expectedIndexes.has(index)) findings.push(`unknown-index:${index}`);
}
const actualChecks = new Set<string>();
const actualForeignKeys = new Set<string>();
for (const row of constraintsResult.rows) {
if (
typeof row.constraintName !== 'string' ||
(row.constraintType !== 'check' && row.constraintType !== 'foreign_key')
) {
throw new PostgresSchemaReadinessError('schema_contract_invalid');
}
const target =
row.constraintType === 'check' ? actualChecks : actualForeignKeys;
target.add(row.constraintName);
}
for (const check of contract.checks) {
if (!actualChecks.has(check)) findings.push(`missing-check:${check}`);
}
for (const check of actualChecks) {
if (!contract.checks.includes(check))
findings.push(`unknown-check:${check}`);
}
for (const foreignKey of contract.foreignKeys) {
if (!actualForeignKeys.has(foreignKey)) {
findings.push(`missing-foreign-key:${foreignKey}`);
}
}
for (const foreignKey of actualForeignKeys) {
if (!contract.foreignKeys.includes(foreignKey)) {
findings.push(`unknown-foreign-key:${foreignKey}`);
}
}
if (findings.length > 0) {
throw new PostgresSchemaReadinessError(
'schema_contract_invalid',
sorted(findings),
);
}
}
async function assertRuntimeRole(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<void> {
const schemaResult = await queryable.query<SchemaPrivilegeRow>(
`
SELECT
has_schema_privilege(current_user, $1, 'USAGE') AS "schemaUsage",
has_schema_privilege(current_user, $1, 'CREATE') AS "schemaCreate"
`.trim(),
[contract.schema],
);
const schema = schemaResult.rows[0];
const tableNames = Object.keys(REQUIRED_RUNTIME_PRIVILEGES);
const tableResult = await queryable.query<TablePrivilegeRow>(
`
SELECT
requested.table_name AS "tableName",
has_table_privilege(current_user, format('%I.%I', $1, requested.table_name), 'SELECT') AS "selectAllowed",
has_table_privilege(current_user, format('%I.%I', $1, requested.table_name), 'INSERT') AS "insertAllowed",
has_table_privilege(current_user, format('%I.%I', $1, requested.table_name), 'UPDATE') AS "updateAllowed",
has_table_privilege(current_user, format('%I.%I', $1, requested.table_name), 'DELETE') AS "deleteAllowed",
pg_get_userbyid(classes.relowner) = current_user AS "isOwner"
FROM unnest($2::text[]) AS requested(table_name)
JOIN pg_namespace namespaces ON namespaces.nspname = $1
JOIN pg_class classes
ON classes.relnamespace = namespaces.oid
AND classes.relname = requested.table_name
ORDER BY requested.table_name
`.trim(),
[contract.schema, tableNames],
);
const findings: string[] = [];
if (
schemaResult.rows.length !== 1 ||
schema?.schemaUsage !== true ||
schema?.schemaCreate !== false
) {
findings.push('schema-privileges');
}
const privilegesByTable = new Map(
tableResult.rows.map((row) => [row.tableName, row]),
);
for (const tableName of tableNames) {
const expected =
REQUIRED_RUNTIME_PRIVILEGES[
tableName as keyof typeof REQUIRED_RUNTIME_PRIVILEGES
];
const actual = privilegesByTable.get(tableName);
if (
!actual ||
actual.selectAllowed !== expected.select ||
actual.insertAllowed !== expected.insert ||
actual.updateAllowed !== expected.update ||
actual.deleteAllowed !== expected.delete ||
actual.isOwner !== false
) {
findings.push(`table-privileges:${tableName}`);
}
}
if (privilegesByTable.size !== tableNames.length) {
findings.push('table-privilege-row-count');
}
if (findings.length > 0) {
throw new PostgresSchemaReadinessError(
'runtime_role_invalid',
sorted(findings),
);
}
}
export async function assertPostgresSchemaReady(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract = postgresqlControlSchemaContract,
): Promise<PostgresSchemaReadinessReport> {
const server = await readServer(queryable, contract);
const migrationIds = await assertHistory(queryable);
await assertCapability(queryable, contract);
await assertSchemaContract(queryable, contract);
await assertRuntimeRole(queryable, contract);
return Object.freeze({
ready: true,
...server,
contractName: contract.contractName,
contractVersion: contract.contractVersion,
migrationIds,
});
}
@@ -0,0 +1,34 @@
import { createHash } from 'crypto';
import type { MigrationStreamStep } from '../core/migrationStream';
import type { PostgresMigrationContext } from '../adapters/postgresMigrationStreamStore';
export interface PostgresSqlMigrationDefinition {
readonly id: string;
readonly statements: readonly string[];
}
export function checksumPostgresStatements(
statements: readonly string[],
): string {
return createHash('sha256')
.update(
JSON.stringify({
format: 1,
statements,
}),
)
.digest('hex');
}
export function definePostgresSqlMigration(
definition: PostgresSqlMigrationDefinition,
): MigrationStreamStep<PostgresMigrationContext> {
const statements = Object.freeze([...definition.statements]);
return Object.freeze({
id: definition.id,
checksum: checksumPostgresStatements(statements),
async up(context: PostgresMigrationContext) {
for (const statement of statements) await context.query(statement);
},
});
}
+91
View File
@@ -0,0 +1,91 @@
import type { ModelStatic, Sequelize } from 'sequelize';
import Logger from '../loaders/logger';
import {
SchemaMigrationInstance,
SchemaMigrationModel,
} from '../data/schemaMigration';
import { sequelize } from '../data';
import { migrations as registeredMigrations } from '.';
import {
SQLITE_MAIN_MIGRATION_STREAM_ID,
SequelizeSqliteMigrationStreamStore,
} from './adapters/sequelizeSqliteMigrationStreamStore';
import {
runMigrationStream,
type MigrationStreamStore,
} from './core/migrationStream';
import type { Migration, MigrationContext } from './types';
interface MigrationLogger {
info(message: string): unknown;
}
export interface RunMigrationsOptions {
database?: Sequelize;
migrationModel?: ModelStatic<SchemaMigrationInstance>;
migrations?: Migration[];
logger?: MigrationLogger;
}
function validateMigrations(migrations: Migration[]) {
const ids = new Set<string>();
for (const migration of migrations) {
if (ids.has(migration.id)) {
throw new Error(`Duplicate migration id: ${migration.id}`);
}
ids.add(migration.id);
}
}
function scopeHistoryToCustomMigrations(
store: MigrationStreamStore<MigrationContext>,
migrations: readonly Migration[],
): MigrationStreamStore<MigrationContext> {
const ids = new Set(migrations.map(({ id }) => id));
return {
ensureHistory: () => store.ensureHistory(),
listAll: async () =>
(await store.listAll()).filter(({ migrationId }) => ids.has(migrationId)),
findById: (migrationId) => store.findById(migrationId),
transaction: (work) => store.transaction(work),
};
}
export async function runMigrations(
options: RunMigrationsOptions = {},
): Promise<void> {
const database = options.database || sequelize;
const migrationModel = options.migrationModel || SchemaMigrationModel;
const migrations = options.migrations || registeredMigrations;
const logger = options.logger || Logger;
validateMigrations(migrations);
const store = new SequelizeSqliteMigrationStreamStore(
database,
migrationModel,
);
await runMigrationStream({
stream: {
id: SQLITE_MAIN_MIGRATION_STREAM_ID,
dialect: 'sqlite',
migrationIdScheme: 'sqlite-numbered',
checksumScheme: 'legacy-opaque',
migrations,
},
store:
options.migrations === undefined
? store
: scopeHistoryToCustomMigrations(store, migrations),
clock: Date.now,
logger: {
info(message) {
logger.info(
message.replace(
`[migration:${SQLITE_MAIN_MIGRATION_STREAM_ID}]`,
'[migration]',
),
);
},
},
});
}
+141
View File
@@ -0,0 +1,141 @@
import {
SqliteSchemaOwnershipManifest,
sqliteSchemaOwnership,
} from './schemaOwnership';
export interface SqliteSchemaSnapshotTable {
name: string;
columns: readonly string[];
indexes: readonly string[];
}
export interface SqliteSchemaSnapshot {
tables: readonly SqliteSchemaSnapshotTable[];
migrationIds: readonly string[];
}
export interface SchemaColumnFinding {
table: string;
column: string;
}
export interface SqliteSchemaAuditReport {
compatible: boolean;
driftDetected: boolean;
missingTables: string[];
missingColumns: SchemaColumnFinding[];
missingIndexes: string[];
missingMigrationIds: string[];
unknownTables: string[];
unknownColumns: SchemaColumnFinding[];
unknownIndexes: string[];
extraMigrationIds: string[];
}
function sortedUnique(values: readonly string[]): string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
function sortColumns(values: SchemaColumnFinding[]): SchemaColumnFinding[] {
return values.sort(
(left, right) =>
left.table.localeCompare(right.table) ||
left.column.localeCompare(right.column),
);
}
export function auditSqliteSchema(
snapshot: SqliteSchemaSnapshot,
manifest: SqliteSchemaOwnershipManifest = sqliteSchemaOwnership,
): SqliteSchemaAuditReport {
const actualTables = new Map(
snapshot.tables.map((table) => [
table.name,
{
columns: new Set(table.columns),
indexes: new Set(table.indexes),
},
]),
);
const ownedTables = new Map(
manifest.tables.map((table) => [table.name, table]),
);
const actualIndexes = new Set(
snapshot.tables.flatMap((table) => [...table.indexes]),
);
const ownedIndexes = new Set(manifest.indexes.map((index) => index.name));
const actualMigrations = new Set(snapshot.migrationIds);
const ownedMigrations = new Set(manifest.migrationIds);
const missingTables: string[] = [];
const missingColumns: SchemaColumnFinding[] = [];
const unknownColumns: SchemaColumnFinding[] = [];
for (const table of manifest.tables) {
const actual = actualTables.get(table.name);
if (!actual) {
if (table.mode === 'unmanaged-legacy') continue;
missingTables.push(table.name);
continue;
}
for (const column of table.requiredColumns) {
if (!actual.columns.has(column)) {
missingColumns.push({ table: table.name, column });
}
}
if (table.mode === 'full') {
const ownedColumns = new Set(table.requiredColumns);
for (const column of actual.columns) {
if (!ownedColumns.has(column)) {
unknownColumns.push({ table: table.name, column });
}
}
}
}
const missingIndexes = manifest.indexes
.map((index) => index.name)
.filter((index) => !actualIndexes.has(index));
const missingMigrationIds = manifest.migrationIds.filter(
(migrationId) => !actualMigrations.has(migrationId),
);
const unknownTables = snapshot.tables
.map((table) => table.name)
.filter(
(table) => !table.startsWith('sqlite_') && !ownedTables.has(table),
);
const reportableIndexes = new Set(
snapshot.tables.flatMap((table) => {
const ownership = ownedTables.get(table.name);
return !ownership || ownership.mode === 'full' ? [...table.indexes] : [];
}),
);
const unknownIndexes = [...reportableIndexes].filter(
(index) => !index.startsWith('sqlite_autoindex_') && !ownedIndexes.has(index),
);
const extraMigrationIds = snapshot.migrationIds.filter(
(migrationId) => !ownedMigrations.has(migrationId),
);
const compatible =
missingTables.length === 0 &&
missingColumns.length === 0 &&
missingIndexes.length === 0 &&
missingMigrationIds.length === 0;
const driftDetected =
unknownTables.length > 0 ||
unknownColumns.length > 0 ||
unknownIndexes.length > 0 ||
extraMigrationIds.length > 0;
return {
compatible,
driftDetected,
missingTables: sortedUnique(missingTables),
missingColumns: sortColumns(missingColumns),
missingIndexes: sortedUnique(missingIndexes),
missingMigrationIds: sortedUnique(missingMigrationIds),
unknownTables: sortedUnique(unknownTables),
unknownColumns: sortColumns(unknownColumns),
unknownIndexes: sortedUnique(unknownIndexes),
extraMigrationIds: sortedUnique(extraMigrationIds),
};
}
+391
View File
@@ -0,0 +1,391 @@
import { legacyColumnOwnership } from './0001-legacy-columns';
import {
RUN_ATTEMPT_TABLE,
RUN_EVENT_TABLE,
RUN_TABLE,
runSchemaManifest,
} from './0002-run-schema';
import {
RUNNING_INSTANCE_TABLE,
runningInstanceRunReferenceManifest,
} from './0003-running-instance-run-reference';
import { runCancellationRequestManifest } from './0004-run-cancellation-request';
import {
RUN_CANCELLATION_DISPATCH_TABLE,
runCancellationDispatchManifest,
} from './0005-run-cancellation-dispatch';
import { runAttemptDeadlineManifest } from './0006-run-attempt-deadline';
import {
COMPLETION_RECEIPT_JOURNAL_TABLE,
completionReceiptJournalManifest,
} from './0007-completion-receipt-journal';
import {
WORKER_REGISTRY_TABLE,
workerRegistryManifest,
} from './0008-worker-registry';
import {
RUN_DISPATCH_LEASE_TABLE,
runDispatchLeaseManifest,
} from './0009-run-dispatch-lease';
import { runDispatchCandidateManifest } from './0010-run-dispatch-candidates';
import {
RUN_RETRY_POLICY_TABLE,
runRetryPolicyManifest,
} from './0011-run-retry-policy';
import {
TASK_EXECUTION_REVISION_TABLE,
taskExecutionRevisionManifest,
} from './0012-task-execution-revisions';
import {
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
localExecutionContextRecipeManifest,
} from './0013-local-execution-context-recipes';
import {
LOCAL_SECRET_ENVELOPE_TABLE,
localSecretEnvelopeManifest,
} from './0014-local-secret-envelopes';
import {
LOCAL_ARTIFACT_RETENTION_TABLE,
localArtifactRetentionManifest,
} from './0015-local-artifact-retention';
import {
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
localArtifactMaintenanceCursorManifest,
} from './0016-local-artifact-maintenance-cursor';
import {
PROJECT_ROLE_BINDING_TABLE,
PROJECT_TABLE,
projectPolicyManifest,
} from './0017-project-policy';
import {
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
projectOwnerBootstrapManifest,
} from './0018-project-owner-bootstrap';
import {
IDENTITY_AUTHENTICATION_BINDING_TABLE,
IDENTITY_SUBJECT_TABLE,
identityDirectoryManifest,
} from './0019-identity-directory';
import {
APPROVAL_REQUEST_TABLE,
APPROVED_ACTION_DISPATCH_TABLE,
approvalRequestManifest,
} from './0020-approval-requests';
import {
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
approvedActionDispatchExecutionManifest,
} from './0021-approved-action-dispatch-executions';
import {
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
approvedActionRecoveryManifest,
} from './0022-approved-action-recovery';
import {
APPROVED_RUN_ACTION_RECEIPT_TABLE,
approvedRunActionReceiptManifest,
} from './0023-approved-run-action-receipts';
import {
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
approvedActionRecoveryAuthorizationManifest,
} from './0024-approved-action-recovery-authorization';
export type SchemaOwnershipMode = 'full' | 'extension' | 'unmanaged-legacy';
export interface SqliteTableOwnership {
name: string;
mode: SchemaOwnershipMode;
requiredColumns: readonly string[];
}
export interface SqliteIndexOwnership {
name: string;
}
export interface SqliteSchemaOwnershipManifest {
version: 1;
database: 'database.sqlite';
migrationIds: readonly string[];
tables: readonly SqliteTableOwnership[];
indexes: readonly SqliteIndexOwnership[];
constraints: readonly string[];
unknownObjectPolicy: 'preserve-and-report';
}
function unique(values: readonly string[]): string[] {
return [...new Set(values)];
}
function indexName(definition: string): string {
const boundary = definition.indexOf('(');
return boundary === -1 ? definition : definition.slice(0, boundary);
}
function legacyColumns(table: string): string[] {
return legacyColumnOwnership
.filter((definition) => definition.table === table)
.map((definition) => definition.column);
}
export const sqliteSchemaOwnership: SqliteSchemaOwnershipManifest = {
version: 1,
database: 'database.sqlite',
migrationIds: [
'0001-legacy-columns',
'0002-run-schema',
'0003-running-instance-run-reference',
'0004-run-cancellation-request',
'0005-run-cancellation-dispatch',
'0006-run-attempt-deadline',
'0007-completion-receipt-journal',
'0008-worker-registry',
'0009-run-dispatch-lease',
'0010-run-dispatch-candidates',
'0011-run-retry-policy',
'0012-task-execution-revisions',
'0013-local-execution-context-recipes',
'0014-local-secret-envelopes',
'0015-local-artifact-retention',
'0016-local-artifact-maintenance-cursor',
'0017-project-policy',
'0018-project-owner-bootstrap',
'0019-identity-directory',
'0020-approval-requests',
'0021-approved-action-dispatch-executions',
'0022-approved-action-recovery',
'0023-approved-run-action-receipts',
'0024-approved-action-recovery-authorization',
],
tables: [
{
name: 'SchemaMigrations',
mode: 'full',
requiredColumns: ['id', 'checksum', 'applied_at'],
},
{
name: 'Apps',
mode: 'unmanaged-legacy',
requiredColumns: [],
},
{
name: 'Auths',
mode: 'unmanaged-legacy',
requiredColumns: [],
},
{
name: 'CrontabStats',
mode: 'unmanaged-legacy',
requiredColumns: [],
},
{
name: 'Dependences',
mode: 'unmanaged-legacy',
requiredColumns: [],
},
{
name: 'CrontabViews',
mode: 'extension',
requiredColumns: legacyColumns('CrontabViews'),
},
{
name: 'Subscriptions',
mode: 'extension',
requiredColumns: legacyColumns('Subscriptions'),
},
{
name: 'Crontabs',
mode: 'extension',
requiredColumns: legacyColumns('Crontabs'),
},
{
name: 'Envs',
mode: 'extension',
requiredColumns: legacyColumns('Envs'),
},
{
name: RUN_TABLE,
mode: 'full',
requiredColumns: unique([
...runSchemaManifest.tables.Runs,
...Object.keys(runCancellationRequestManifest.columns),
]),
},
{
name: RUN_ATTEMPT_TABLE,
mode: 'full',
requiredColumns: unique([
...runSchemaManifest.tables.RunAttempts,
...Object.keys(runAttemptDeadlineManifest.columns),
]),
},
{
name: RUN_EVENT_TABLE,
mode: 'full',
requiredColumns: runSchemaManifest.tables.RunEvents,
},
{
name: RUNNING_INSTANCE_TABLE,
mode: 'extension',
requiredColumns: Object.keys(runningInstanceRunReferenceManifest.columns),
},
{
name: RUN_CANCELLATION_DISPATCH_TABLE,
mode: 'full',
requiredColumns: runCancellationDispatchManifest.columns,
},
{
name: COMPLETION_RECEIPT_JOURNAL_TABLE,
mode: 'full',
requiredColumns: completionReceiptJournalManifest.columns,
},
{
name: WORKER_REGISTRY_TABLE,
mode: 'full',
requiredColumns: workerRegistryManifest.columns,
},
{
name: RUN_DISPATCH_LEASE_TABLE,
mode: 'full',
requiredColumns: runDispatchLeaseManifest.columns,
},
{
name: RUN_RETRY_POLICY_TABLE,
mode: 'full',
requiredColumns: runRetryPolicyManifest.columns,
},
{
name: TASK_EXECUTION_REVISION_TABLE,
mode: 'full',
requiredColumns: taskExecutionRevisionManifest.columns,
},
{
name: LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
mode: 'full',
requiredColumns: localExecutionContextRecipeManifest.columns,
},
{
name: LOCAL_SECRET_ENVELOPE_TABLE,
mode: 'full',
requiredColumns: localSecretEnvelopeManifest.columns,
},
{
name: LOCAL_ARTIFACT_RETENTION_TABLE,
mode: 'full',
requiredColumns: localArtifactRetentionManifest.columns,
},
{
name: LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
mode: 'full',
requiredColumns: localArtifactMaintenanceCursorManifest.columns,
},
{
name: PROJECT_TABLE,
mode: 'full',
requiredColumns: projectPolicyManifest.tables.Projects,
},
{
name: PROJECT_ROLE_BINDING_TABLE,
mode: 'full',
requiredColumns: projectPolicyManifest.tables.ProjectRoleBindings,
},
{
name: PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
mode: 'full',
requiredColumns: projectOwnerBootstrapManifest.columns,
},
{
name: IDENTITY_SUBJECT_TABLE,
mode: 'full',
requiredColumns: identityDirectoryManifest.tables.IdentitySubjects,
},
{
name: IDENTITY_AUTHENTICATION_BINDING_TABLE,
mode: 'full',
requiredColumns:
identityDirectoryManifest.tables.IdentityAuthenticationBindings,
},
{
name: APPROVAL_REQUEST_TABLE,
mode: 'full',
requiredColumns: approvalRequestManifest.tables.ApprovalRequests,
},
{
name: APPROVED_ACTION_DISPATCH_TABLE,
mode: 'full',
requiredColumns: approvalRequestManifest.tables.ApprovedActionDispatches,
},
{
name: APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
mode: 'full',
requiredColumns: approvedActionDispatchExecutionManifest.columns,
},
{
name: APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
mode: 'full',
requiredColumns:
approvedActionRecoveryManifest.tables.ApprovedActionRecoveryControls,
},
{
name: APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
mode: 'full',
requiredColumns:
approvedActionRecoveryManifest.tables.ApprovedActionRecoveryResolutions,
},
{
name: APPROVED_RUN_ACTION_RECEIPT_TABLE,
mode: 'full',
requiredColumns: approvedRunActionReceiptManifest.columns,
},
{
name: APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
mode: 'full',
requiredColumns: approvedActionRecoveryAuthorizationManifest.columns,
},
],
indexes: unique([
...runSchemaManifest.indexes,
...runningInstanceRunReferenceManifest.indexes.map(indexName),
...runCancellationRequestManifest.indexes.map(indexName),
...runCancellationDispatchManifest.indexes.map(indexName),
...runAttemptDeadlineManifest.indexes.map(indexName),
...completionReceiptJournalManifest.indexes.map(indexName),
...workerRegistryManifest.indexes.map(indexName),
...runDispatchLeaseManifest.indexes.map(indexName),
...runDispatchCandidateManifest.indexes.map(indexName),
...runRetryPolicyManifest.indexes.map(indexName),
...taskExecutionRevisionManifest.indexes.map(indexName),
...localExecutionContextRecipeManifest.indexes.map(indexName),
...localSecretEnvelopeManifest.indexes.map(indexName),
...localArtifactRetentionManifest.indexes.map(indexName),
...localArtifactMaintenanceCursorManifest.indexes.map(indexName),
...projectPolicyManifest.indexes.map(indexName),
...projectOwnerBootstrapManifest.indexes.map(indexName),
...identityDirectoryManifest.indexes.map(indexName),
...approvalRequestManifest.indexes.map(indexName),
...approvedActionDispatchExecutionManifest.indexes.map(indexName),
...approvedActionRecoveryManifest.indexes.map(indexName),
...approvedRunActionReceiptManifest.indexes.map(indexName),
...approvedActionRecoveryAuthorizationManifest.indexes.map(indexName),
]).map((name) => ({ name })),
constraints: unique([
...runSchemaManifest.constraints,
...runCancellationDispatchManifest.constraints,
...completionReceiptJournalManifest.constraints,
...workerRegistryManifest.constraints,
...runDispatchLeaseManifest.constraints,
...runRetryPolicyManifest.constraints,
...taskExecutionRevisionManifest.constraints,
...localExecutionContextRecipeManifest.constraints,
...localSecretEnvelopeManifest.constraints,
...localArtifactRetentionManifest.constraints,
...localArtifactMaintenanceCursorManifest.constraints,
...projectPolicyManifest.constraints,
...projectOwnerBootstrapManifest.constraints,
...identityDirectoryManifest.constraints,
...approvalRequestManifest.constraints,
...approvedActionDispatchExecutionManifest.constraints,
...approvedActionRecoveryManifest.constraints,
...approvedRunActionReceiptManifest.constraints,
...approvedActionRecoveryAuthorizationManifest.constraints,
]),
unknownObjectPolicy: 'preserve-and-report',
};
+12
View File
@@ -0,0 +1,12 @@
import type { QueryInterface, Transaction } from 'sequelize';
export interface MigrationContext {
queryInterface: QueryInterface;
transaction: Transaction;
}
export interface Migration {
id: string;
checksum: string;
up(context: MigrationContext): Promise<void>;
}