322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
import { desc, sql } from 'drizzle-orm';
|
|
import {
|
|
type AnySQLiteColumn,
|
|
check,
|
|
index,
|
|
integer,
|
|
primaryKey,
|
|
sqliteTable,
|
|
text,
|
|
unique,
|
|
uniqueIndex,
|
|
} from 'drizzle-orm/sqlite-core';
|
|
|
|
const timestamps = {
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
};
|
|
|
|
export const instances = sqliteTable(
|
|
'instances',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
baseUrl: text('base_url').notNull(),
|
|
authMode: text('auth_mode').notNull().default('password'),
|
|
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
|
configRevision: integer('config_revision').notNull().default(1),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
unique('instances_base_url_unique').on(table.baseUrl),
|
|
check('instances_enabled_check', sql`${table.enabled} in (0, 1)`),
|
|
check('instances_config_revision_check', sql`${table.configRevision} > 0`),
|
|
],
|
|
);
|
|
|
|
export const instanceTags = sqliteTable(
|
|
'instance_tags',
|
|
{
|
|
instanceId: text('instance_id')
|
|
.notNull()
|
|
.references(() => instances.id, { onDelete: 'cascade' }),
|
|
tag: text('tag').notNull(),
|
|
createdAt: text('created_at').notNull(),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.instanceId, table.tag] })],
|
|
);
|
|
|
|
export const capabilities = sqliteTable(
|
|
'capabilities',
|
|
{
|
|
instanceId: text('instance_id')
|
|
.notNull()
|
|
.references(() => instances.id, { onDelete: 'cascade' }),
|
|
operationId: text('operation_id').notNull(),
|
|
state: text('state').notNull(),
|
|
upstreamVersion: text('upstream_version'),
|
|
detailCode: text('detail_code'),
|
|
observedAt: text('observed_at').notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
primaryKey({ columns: [table.instanceId, table.operationId] }),
|
|
check(
|
|
'capabilities_state_check',
|
|
sql`${table.state} in ('supported', 'unsupported', 'auth-required', 'degraded', 'unknown')`,
|
|
),
|
|
index('idx_capabilities_state').on(table.state),
|
|
],
|
|
);
|
|
|
|
export const statusSnapshots = sqliteTable(
|
|
'status_snapshots',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
instanceId: text('instance_id')
|
|
.notNull()
|
|
.references(() => instances.id, { onDelete: 'cascade' }),
|
|
category: text('category').notNull(),
|
|
state: text('state').notNull(),
|
|
payloadJson: text('payload_json').notNull(),
|
|
observedAt: text('observed_at').notNull(),
|
|
expiresAt: text('expires_at'),
|
|
createdAt: text('created_at').notNull(),
|
|
},
|
|
(table) => [
|
|
check(
|
|
'status_snapshots_state_check',
|
|
sql`${table.state} in ('fresh', 'stale', 'expired', 'unknown')`,
|
|
),
|
|
index('idx_status_snapshots_instance_observed_at').on(table.instanceId, desc(table.observedAt)),
|
|
uniqueIndex('uq_status_snapshots_instance_category').on(table.instanceId, table.category),
|
|
],
|
|
);
|
|
|
|
export const jobs = sqliteTable(
|
|
'jobs',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
parentJobId: text('parent_job_id').references((): AnySQLiteColumn => jobs.id, {
|
|
onDelete: 'restrict',
|
|
}),
|
|
rootJobId: text('root_job_id').references((): AnySQLiteColumn => jobs.id, {
|
|
onDelete: 'restrict',
|
|
}),
|
|
retryOfJobId: text('retry_of_job_id').references((): AnySQLiteColumn => jobs.id, {
|
|
onDelete: 'restrict',
|
|
}),
|
|
operationId: text('operation_id').notNull(),
|
|
riskLevel: text('risk_level').notNull(),
|
|
status: text('status').notNull(),
|
|
requestedBy: text('requested_by').notNull(),
|
|
requestId: text('request_id').notNull(),
|
|
parametersDigest: text('parameters_digest').notNull(),
|
|
createdAt: text('created_at').notNull(),
|
|
startedAt: text('started_at'),
|
|
finishedAt: text('finished_at'),
|
|
updatedAt: text('updated_at').notNull(),
|
|
},
|
|
(table) => [
|
|
check('jobs_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`),
|
|
index('idx_jobs_status_created_at').on(table.status, desc(table.createdAt)),
|
|
],
|
|
);
|
|
|
|
export const jobItems = sqliteTable(
|
|
'job_items',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
jobId: text('job_id')
|
|
.notNull()
|
|
.references(() => jobs.id, { onDelete: 'cascade' }),
|
|
instanceId: text('instance_id').notNull(),
|
|
attemptNumber: integer('attempt_number').notNull().default(1),
|
|
status: text('status').notNull(),
|
|
resultCode: text('result_code'),
|
|
errorJson: text('error_json'),
|
|
sourceJobItemId: text('source_job_item_id').references((): AnySQLiteColumn => jobItems.id, {
|
|
onDelete: 'restrict',
|
|
}),
|
|
createdAt: text('created_at').notNull(),
|
|
startedAt: text('started_at'),
|
|
finishedAt: text('finished_at'),
|
|
updatedAt: text('updated_at').notNull(),
|
|
},
|
|
(table) => [
|
|
unique('job_items_job_instance_attempt_unique').on(
|
|
table.jobId,
|
|
table.instanceId,
|
|
table.attemptNumber,
|
|
),
|
|
check('job_items_attempt_number_check', sql`${table.attemptNumber} > 0`),
|
|
index('idx_job_items_job_status').on(table.jobId, table.status),
|
|
],
|
|
);
|
|
|
|
export const jobAttempts = sqliteTable(
|
|
'job_attempts',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
jobId: text('job_id')
|
|
.notNull()
|
|
.references(() => jobs.id, { onDelete: 'cascade' }),
|
|
status: text('status').notNull(),
|
|
startedAt: text('started_at').notNull(),
|
|
finishedAt: text('finished_at'),
|
|
createdAt: text('created_at').notNull(),
|
|
},
|
|
(table) => [index('idx_job_attempts_job_started_at').on(table.jobId, table.startedAt)],
|
|
);
|
|
|
|
export const operationPreparations = sqliteTable(
|
|
'operation_preparations',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
operationId: text('operation_id').notNull(),
|
|
riskLevel: text('risk_level').notNull(),
|
|
status: text('status').notNull(),
|
|
targetInstanceId: text('target_instance_id').notNull(),
|
|
targetRevision: integer('target_revision').notNull(),
|
|
parameterSchemaId: text('parameter_schema_id').notNull(),
|
|
parametersDigest: text('parameters_digest').notNull(),
|
|
tokenDigest: text('token_digest').notNull(),
|
|
requestedBy: text('requested_by').notNull(),
|
|
requestId: text('request_id').notNull(),
|
|
expiresAt: text('expires_at').notNull(),
|
|
consumedAt: text('consumed_at'),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
index('idx_operation_preparations_status_expires_at').on(table.status, table.expiresAt),
|
|
],
|
|
);
|
|
|
|
export const auditEvents = sqliteTable(
|
|
'audit_events',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
instanceId: text('instance_id').references(() => instances.id, { onDelete: 'set null' }),
|
|
jobId: text('job_id').references(() => jobs.id, { onDelete: 'set null' }),
|
|
actor: text('actor').notNull(),
|
|
operationId: text('operation_id').notNull(),
|
|
riskLevel: text('risk_level').notNull(),
|
|
requestId: text('request_id').notNull(),
|
|
parametersSummaryJson: text('parameters_summary_json').notNull(),
|
|
bodyDigest: text('body_digest'),
|
|
resultCode: text('result_code').notNull(),
|
|
durationMs: integer('duration_ms').notNull(),
|
|
createdAt: text('created_at').notNull(),
|
|
},
|
|
(table) => [
|
|
check('audit_events_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`),
|
|
check('audit_events_duration_ms_check', sql`${table.durationMs} >= 0`),
|
|
index('idx_audit_events_created_at').on(desc(table.createdAt)),
|
|
],
|
|
);
|
|
|
|
export const appSettings = sqliteTable('app_settings', {
|
|
key: text('key').primaryKey(),
|
|
valueJson: text('value_json').notNull(),
|
|
...timestamps,
|
|
});
|
|
|
|
export const secretReferences = sqliteTable(
|
|
'secret_references',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
instanceId: text('instance_id').references(() => instances.id, { onDelete: 'cascade' }),
|
|
purpose: text('purpose').notNull(),
|
|
provider: text('provider').notNull(),
|
|
externalReference: text('external_reference').notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
unique('secret_references_provider_external_unique').on(
|
|
table.provider,
|
|
table.externalReference,
|
|
),
|
|
uniqueIndex('uq_secret_references_instance_purpose')
|
|
.on(table.instanceId, table.purpose)
|
|
.where(sql`${table.instanceId} is not null`),
|
|
],
|
|
);
|
|
|
|
export const secretCleanupTasks = sqliteTable(
|
|
'secret_cleanup_tasks',
|
|
{
|
|
reference: text('reference').primaryKey(),
|
|
instanceId: text('instance_id').notNull(),
|
|
purpose: text('purpose').notNull(),
|
|
provider: text('provider').notNull(),
|
|
generation: integer('generation').notNull().default(1),
|
|
queuedAt: text('queued_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
},
|
|
(table) => [index('idx_secret_cleanup_tasks_queued_at').on(table.queuedAt)],
|
|
);
|
|
|
|
export const scheduledTasks = sqliteTable(
|
|
'scheduled_tasks',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
operationType: text('operation_type').notNull(),
|
|
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
|
version: integer('version').notNull().default(1),
|
|
cronExpression: text('cron_expression').notNull(),
|
|
timezone: text('timezone').notNull(),
|
|
targetSelectorJson: text('target_selector_json').notNull(),
|
|
smsSecretReference: text('sms_secret_reference'),
|
|
smsRecipientCount: integer('sms_recipient_count'),
|
|
effectiveStartAt: text('effective_start_at'),
|
|
effectiveEndAt: text('effective_end_at'),
|
|
misfirePolicy: text('misfire_policy').notNull(),
|
|
overlapPolicy: text('overlap_policy').notNull(),
|
|
retryPolicyJson: text('retry_policy_json').notNull(),
|
|
nextDueAt: text('next_due_at'),
|
|
lastEvaluatedAt: text('last_evaluated_at'),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull(),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
deletedAt: text('deleted_at'),
|
|
},
|
|
(table) => [
|
|
check('scheduled_tasks_enabled_check', sql`${table.enabled} in (0, 1)`),
|
|
check('scheduled_tasks_version_check', sql`${table.version} > 0`),
|
|
index('idx_scheduled_tasks_enabled_next_due').on(table.enabled, table.nextDueAt),
|
|
],
|
|
);
|
|
|
|
export const scheduledRuns = sqliteTable(
|
|
'scheduled_runs',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
scheduledTaskId: text('scheduled_task_id')
|
|
.notNull()
|
|
.references(() => scheduledTasks.id, { onDelete: 'restrict' }),
|
|
scheduleVersion: integer('schedule_version').notNull(),
|
|
taskSnapshotJson: text('task_snapshot_json').notNull(),
|
|
dueAt: text('due_at').notNull(),
|
|
claimedAt: text('claimed_at').notNull(),
|
|
startedAt: text('started_at'),
|
|
finishedAt: text('finished_at'),
|
|
targetSnapshotJson: text('target_snapshot_json').notNull(),
|
|
outcome: text('outcome'),
|
|
reason: text('reason'),
|
|
jobIdsJson: text('job_ids_json').notNull().default('[]'),
|
|
triggerSource: text('trigger_source').notNull(),
|
|
attempt: integer('attempt').notNull().default(1),
|
|
},
|
|
(table) => [
|
|
unique('scheduled_runs_occurrence_unique').on(
|
|
table.scheduledTaskId,
|
|
table.scheduleVersion,
|
|
table.dueAt,
|
|
table.triggerSource,
|
|
),
|
|
index('idx_scheduled_runs_task_due').on(table.scheduledTaskId, desc(table.dueAt)),
|
|
],
|
|
);
|