Files
multi-simadmin/apps/api/src/infrastructure/database/schema.ts
T

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)),
],
);