@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.
- package/index.ts +5 -0
- package/package.json +27 -0
- package/src/bundle-validation.ts +218 -0
- package/src/dialects.ts +139 -0
- package/src/runner.ts +193 -0
- package/src/schema/bulk-data.ts +87 -0
- package/src/schema/common.ts +33 -0
- package/src/schema/durable-jobs.ts +266 -0
- package/src/schema/file-storage.ts +94 -0
- package/src/schema/governance.ts +204 -0
- package/src/schema/governed-exports.ts +90 -0
- package/src/schema/identity.ts +169 -0
- package/src/schema/index.ts +33 -0
- package/src/schema/license-legacy.ts +93 -0
- package/src/schema/license-v1.ts +246 -0
- package/src/schema/migration-history.ts +30 -0
- package/src/schema/notifications.ts +60 -0
- package/src/schema/platform-settings.ts +69 -0
- package/src/schema/scheduler.ts +89 -0
- package/src/schema/work-orders.ts +23 -0
- package/src/schema/workflow.ts +86 -0
- package/src/sql-store.ts +101 -0
- package/src/store.ts +32 -0
|
@@ -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
|
+
);
|
package/src/sql-store.ts
ADDED
|
@@ -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
|
+
}
|