@southwind-ai/database 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ // 即使两个可选模块都被裁剪,也要保持该文件可由 schema barrel 导出。
2
+ export {};
3
+
4
+ // @windy-module system.configuration begin
5
+ import type { ModuleConfigDiffEntry, ModuleConfigValues } from "@southwind-ai/shared";
6
+ import {
7
+ index,
8
+ integer,
9
+ jsonb,
10
+ pgTable as configTable,
11
+ timestamp,
12
+ uniqueIndex,
13
+ varchar as configVarchar,
14
+ } from "drizzle-orm/pg-core";
15
+ // @windy-module system.configuration end
16
+ // @windy-module system.settings begin
17
+ import {
18
+ pgTable as settingsTable,
19
+ text,
20
+ varchar as settingsVarchar,
21
+ } from "drizzle-orm/pg-core";
22
+ import { auditColumns } from "./common.js";
23
+ // @windy-module system.settings end
24
+
25
+ // @windy-module system.settings begin
26
+ export const platformBrandingSettings = settingsTable(
27
+ "platform_branding_settings",
28
+ {
29
+ id: settingsVarchar("id", { length: 64 }).primaryKey(),
30
+ softwareName: settingsVarchar("software_name", { length: 120 }).notNull(),
31
+ logoUrl: text("logo_url"),
32
+ description: settingsVarchar("description", { length: 500 }),
33
+ ...auditColumns,
34
+ },
35
+ );
36
+ // @windy-module system.settings end
37
+
38
+ // @windy-module system.configuration begin
39
+ export const moduleConfigVersions = configTable(
40
+ "module_config_versions",
41
+ {
42
+ id: configVarchar("id", { length: 64 }).primaryKey(),
43
+ moduleKey: configVarchar("module_key", { length: 80 }).notNull(),
44
+ version: integer("version").notNull(),
45
+ values: jsonb("values").$type<ModuleConfigValues>().notNull(),
46
+ diff: jsonb("diff").$type<ModuleConfigDiffEntry[]>().notNull(),
47
+ applyStatus: configVarchar("apply_status", { length: 24 }).notNull(),
48
+ applyError: configVarchar("apply_error", { length: 500 }),
49
+ rollbackOfVersion: integer("rollback_of_version"),
50
+ reason: configVarchar("reason", { length: 500 }),
51
+ createdAt: timestamp("created_at", { withTimezone: true })
52
+ .notNull()
53
+ .defaultNow(),
54
+ createdBy: configVarchar("created_by", { length: 64 }).notNull(),
55
+ appliedAt: timestamp("applied_at", { withTimezone: true }),
56
+ },
57
+ (table) => [
58
+ uniqueIndex("uk_module_config_versions_module_version").on(
59
+ table.moduleKey,
60
+ table.version,
61
+ ),
62
+ index("idx_module_config_versions_module_status").on(
63
+ table.moduleKey,
64
+ table.applyStatus,
65
+ ),
66
+ index("idx_module_config_versions_created_at").on(table.createdAt),
67
+ ],
68
+ );
69
+ // @windy-module system.configuration end
@@ -0,0 +1,89 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ boolean,
4
+ index,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ text,
9
+ timestamp,
10
+ uniqueIndex,
11
+ varchar,
12
+ } from "drizzle-orm/pg-core";
13
+ import { durableJobs } from "./durable-jobs.js";
14
+
15
+ export const scheduledTasks = pgTable(
16
+ "scheduled_tasks",
17
+ {
18
+ key: varchar("key", { length: 160 }).primaryKey(),
19
+ label: varchar("label", { length: 160 }).notNull(),
20
+ description: text("description"),
21
+ featureKey: varchar("feature_key", { length: 160 }).notNull(),
22
+ permissionKey: varchar("permission_key", { length: 160 }).notNull(),
23
+ intervalSeconds: integer("interval_seconds").notNull(),
24
+ maxAttempts: integer("max_attempts").notNull().default(1),
25
+ retryDelaySeconds: integer("retry_delay_seconds").notNull().default(60),
26
+ enabled: boolean("enabled").notNull().default(true),
27
+ status: varchar("status", { length: 32 }).notNull().default("active"),
28
+ nextRunAt: timestamp("next_run_at", { withTimezone: true }).notNull(),
29
+ lastRunAt: timestamp("last_run_at", { withTimezone: true }),
30
+ createdAt: timestamp("created_at", { withTimezone: true })
31
+ .notNull()
32
+ .defaultNow(),
33
+ createdBy: varchar("created_by", { length: 64 }).notNull(),
34
+ updatedAt: timestamp("updated_at", { withTimezone: true })
35
+ .notNull()
36
+ .defaultNow(),
37
+ updatedBy: varchar("updated_by", { length: 64 }).notNull(),
38
+ },
39
+ (table) => [
40
+ index("idx_scheduled_tasks_due").on(
41
+ table.status,
42
+ table.enabled,
43
+ table.nextRunAt,
44
+ ),
45
+ index("idx_scheduled_tasks_feature").on(table.featureKey),
46
+ ],
47
+ );
48
+
49
+ export const scheduledTaskRuns = pgTable(
50
+ "scheduled_task_runs",
51
+ {
52
+ id: varchar("id", { length: 64 }).primaryKey(),
53
+ taskKey: varchar("task_key", { length: 160 })
54
+ .notNull()
55
+ .references(() => scheduledTasks.key, { onDelete: "restrict" }),
56
+ trigger: varchar("trigger", { length: 32 }).notNull(),
57
+ status: varchar("status", { length: 32 }).notNull(),
58
+ attempt: integer("attempt").notNull().default(1),
59
+ requestedBy: varchar("requested_by", { length: 64 }).notNull(),
60
+ retryOfRunId: varchar("retry_of_run_id", { length: 64 }),
61
+ scheduledFor: timestamp("scheduled_for", { withTimezone: true }).notNull(),
62
+ startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
63
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
64
+ nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
65
+ error: text("error"),
66
+ result: jsonb("result").$type<Record<string, unknown>>(),
67
+ durableJobId: varchar("durable_job_id", { length: 64 }).references(
68
+ () => durableJobs.id,
69
+ { onDelete: "restrict" },
70
+ ),
71
+ createdAt: timestamp("created_at", { withTimezone: true })
72
+ .notNull()
73
+ .defaultNow(),
74
+ },
75
+ (table) => [
76
+ uniqueIndex("uk_scheduled_task_runs_single_running")
77
+ .on(table.taskKey)
78
+ .where(sql`${table.status} = 'running'`),
79
+ index("idx_scheduled_task_runs_task_started").on(
80
+ table.taskKey,
81
+ table.startedAt,
82
+ ),
83
+ index("idx_scheduled_task_runs_retry_due").on(
84
+ table.status,
85
+ table.nextAttemptAt,
86
+ ),
87
+ index("idx_scheduled_task_runs_durable_job").on(table.durableJobId),
88
+ ],
89
+ );
@@ -0,0 +1,23 @@
1
+ import { index, pgTable, text, varchar } from "drizzle-orm/pg-core";
2
+ import { auditColumns, idColumn, statusColumn } from "./common.js";
3
+
4
+ export const workOrders = pgTable(
5
+ "work_orders",
6
+ {
7
+ id: idColumn().primaryKey(),
8
+ title: varchar("title", { length: 120 }).notNull(),
9
+ description: text("description").notNull(),
10
+ priority: varchar("priority", { length: 24 }).notNull(),
11
+ workflowStatus: varchar("workflow_status", { length: 32 }).notNull(),
12
+ ownerId: varchar("owner_id", { length: 64 }).notNull(),
13
+ departmentId: varchar("department_id", { length: 64 }),
14
+ status: statusColumn().notNull(),
15
+ ...auditColumns,
16
+ },
17
+ (table) => [
18
+ index("idx_work_orders_owner").on(table.ownerId),
19
+ index("idx_work_orders_department").on(table.departmentId),
20
+ index("idx_work_orders_status").on(table.status),
21
+ index("idx_work_orders_workflow").on(table.workflowStatus),
22
+ ],
23
+ );
@@ -0,0 +1,86 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ check,
4
+ index,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ text,
9
+ timestamp,
10
+ uniqueIndex,
11
+ varchar,
12
+ } from "drizzle-orm/pg-core";
13
+ import type { CandidateRole, TargetRef, WorkItem } from "@southwind-ai/shared";
14
+
15
+ export const workflowWorkItems = pgTable(
16
+ "workflow_work_items",
17
+ {
18
+ id: varchar("id", { length: 64 }).primaryKey(),
19
+ title: varchar("title", { length: 160 }).notNull(),
20
+ status: varchar("status", { length: 32 }).notNull(),
21
+ requesterId: varchar("requester_id", { length: 64 }).notNull(),
22
+ candidateRoles: jsonb("candidate_roles").$type<CandidateRole[]>().notNull(),
23
+ assigneeId: varchar("assignee_id", { length: 64 }),
24
+ assigneeName: varchar("assignee_name", { length: 120 }),
25
+ assigneeRoleCodes: jsonb("assignee_role_codes")
26
+ .$type<string[]>()
27
+ .notNull()
28
+ .default([]),
29
+ dueAt: timestamp("due_at", { withTimezone: true }).notNull(),
30
+ target: jsonb("target").$type<TargetRef>().notNull(),
31
+ createdIdempotencyKey: varchar("created_idempotency_key", {
32
+ length: 160,
33
+ }).notNull(),
34
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
35
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
36
+ version: integer("version").notNull(),
37
+ },
38
+ (table) => [
39
+ uniqueIndex("uk_workflow_work_items_create_request").on(
40
+ table.requesterId,
41
+ table.createdIdempotencyKey,
42
+ ),
43
+ index("idx_workflow_work_items_candidate").using(
44
+ "gin",
45
+ table.candidateRoles,
46
+ ),
47
+ index("idx_workflow_work_items_assignee").on(
48
+ table.assigneeId,
49
+ table.status,
50
+ ),
51
+ index("idx_workflow_work_items_due").on(table.status, table.dueAt),
52
+ check("ck_workflow_work_items_version", sql`${table.version} > 0`),
53
+ ],
54
+ );
55
+
56
+ export const workflowTransitions = pgTable(
57
+ "workflow_transitions",
58
+ {
59
+ id: varchar("id", { length: 64 }).primaryKey(),
60
+ workItemId: varchar("work_item_id", { length: 64 })
61
+ .notNull()
62
+ .references(() => workflowWorkItems.id, { onDelete: "restrict" }),
63
+ command: varchar("command", { length: 32 }).notNull(),
64
+ beforeStatus: varchar("before_status", { length: 32 }).notNull(),
65
+ afterStatus: varchar("after_status", { length: 32 }).notNull(),
66
+ actorId: varchar("actor_id", { length: 64 }).notNull(),
67
+ idempotencyKey: varchar("idempotency_key", { length: 160 }).notNull(),
68
+ decision: varchar("decision", { length: 32 }),
69
+ reason: text("reason"),
70
+ previousAssigneeId: varchar("previous_assignee_id", { length: 64 }),
71
+ assigneeId: varchar("assignee_id", { length: 64 }),
72
+ target: jsonb("target").$type<TargetRef>().notNull(),
73
+ resultSnapshot: jsonb("result_snapshot").$type<WorkItem>().notNull(),
74
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
75
+ },
76
+ (table) => [
77
+ uniqueIndex("uk_workflow_transitions_request").on(
78
+ table.actorId,
79
+ table.idempotencyKey,
80
+ ),
81
+ index("idx_workflow_transitions_item").on(
82
+ table.workItemId,
83
+ table.occurredAt,
84
+ ),
85
+ ],
86
+ );
@@ -0,0 +1,101 @@
1
+ import type { MigrationStatement } from "@southwind-ai/shared";
2
+ import type { MigrationHistoryEntry, MigrationHistoryStore } from "./store.js";
3
+
4
+ export interface QueryResult<Row> {
5
+ rows: Row[];
6
+ }
7
+
8
+ export interface QueryableClient {
9
+ query<Row = Record<string, unknown>>(
10
+ sql: string,
11
+ params?: unknown[],
12
+ ): Promise<QueryResult<Row>>;
13
+ }
14
+
15
+ interface MigrationHistoryRow {
16
+ id: string;
17
+ module: string;
18
+ direction: "up" | "down";
19
+ status: "applied" | "failed";
20
+ checksum: string | null;
21
+ statements: string[] | null;
22
+ error: string | null;
23
+ started_at: Date | string;
24
+ finished_at: Date | string;
25
+ }
26
+
27
+ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
28
+ constructor(private readonly client: QueryableClient) {}
29
+
30
+ async listApplied(): Promise<MigrationHistoryEntry[]> {
31
+ const result = await this.client.query<MigrationHistoryRow>(
32
+ `select id, module, direction, status, checksum, statements, error, started_at, finished_at
33
+ from windy_migration_history
34
+ where status = $1
35
+ order by finished_at asc`,
36
+ ["applied"],
37
+ );
38
+
39
+ return result.rows.map(mapHistoryRow);
40
+ }
41
+
42
+ async record(entry: MigrationHistoryEntry): Promise<void> {
43
+ await this.client.query(
44
+ `insert into windy_migration_history
45
+ (id, module, direction, status, checksum, statements, error, started_at, finished_at)
46
+ values ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9)
47
+ on conflict (id) do update set
48
+ module = excluded.module,
49
+ direction = excluded.direction,
50
+ status = excluded.status,
51
+ checksum = excluded.checksum,
52
+ statements = excluded.statements,
53
+ error = excluded.error,
54
+ started_at = excluded.started_at,
55
+ finished_at = excluded.finished_at`,
56
+ [
57
+ entry.id,
58
+ entry.module,
59
+ entry.direction,
60
+ entry.status,
61
+ entry.checksum || null,
62
+ JSON.stringify(entry.statements || []),
63
+ entry.error || null,
64
+ entry.startedAt,
65
+ entry.finishedAt,
66
+ ],
67
+ );
68
+ }
69
+ }
70
+
71
+ export function createPostgresStatementExecutor(client: QueryableClient) {
72
+ return async (statement: MigrationStatement): Promise<void> => {
73
+ await client.query(statement.sql, statement.params);
74
+ };
75
+ }
76
+
77
+ export function splitDrizzleSqlStatements(sql: string): MigrationStatement[] {
78
+ return sql
79
+ .split("--> statement-breakpoint")
80
+ .map((statement) => statement.trim())
81
+ .filter(Boolean)
82
+ .map((statement) => ({ sql: statement }));
83
+ }
84
+
85
+ function mapHistoryRow(row: MigrationHistoryRow): MigrationHistoryEntry {
86
+ return {
87
+ id: row.id,
88
+ module: row.module,
89
+ direction: row.direction,
90
+ status: row.status,
91
+ startedAt: toIsoString(row.started_at),
92
+ finishedAt: toIsoString(row.finished_at),
93
+ checksum: row.checksum || undefined,
94
+ statements: row.statements || undefined,
95
+ error: row.error || undefined,
96
+ };
97
+ }
98
+
99
+ function toIsoString(value: Date | string): string {
100
+ return value instanceof Date ? value.toISOString() : value;
101
+ }
package/src/store.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { ISODateTime } from "@southwind-ai/shared";
2
+
3
+ export type MigrationStatus = "applied" | "failed";
4
+
5
+ export interface MigrationHistoryEntry {
6
+ id: string;
7
+ module: string;
8
+ direction: "up" | "down";
9
+ status: MigrationStatus;
10
+ startedAt: ISODateTime;
11
+ finishedAt: ISODateTime;
12
+ checksum?: string;
13
+ statements?: string[];
14
+ error?: string;
15
+ }
16
+
17
+ export interface MigrationHistoryStore {
18
+ listApplied(): Promise<MigrationHistoryEntry[]>;
19
+ record(entry: MigrationHistoryEntry): Promise<void>;
20
+ }
21
+
22
+ export class InMemoryMigrationHistoryStore implements MigrationHistoryStore {
23
+ private readonly entries: MigrationHistoryEntry[] = [];
24
+
25
+ async listApplied(): Promise<MigrationHistoryEntry[]> {
26
+ return this.entries.filter((entry) => entry.status === "applied");
27
+ }
28
+
29
+ async record(entry: MigrationHistoryEntry): Promise<void> {
30
+ this.entries.push(entry);
31
+ }
32
+ }