@voyant-travel/reporting 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/LICENSE +201 -0
- package/dist/api-runtime.d.ts +12 -0
- package/dist/api-runtime.js +11 -0
- package/dist/graph-registry.d.ts +18 -0
- package/dist/graph-registry.js +94 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/registry.d.ts +81 -0
- package/dist/registry.js +235 -0
- package/dist/routes.d.ts +12 -0
- package/dist/routes.js +153 -0
- package/dist/schema.d.ts +864 -0
- package/dist/schema.js +49 -0
- package/dist/service.d.ts +509 -0
- package/dist/service.js +259 -0
- package/dist/voyant-admin.d.ts +29 -0
- package/dist/voyant-admin.js +30 -0
- package/dist/voyant.d.ts +2 -0
- package/dist/voyant.js +116 -0
- package/migrations/20260718090000_reporting_baseline.sql +54 -0
- package/migrations/meta/_journal.json +13 -0
- package/openapi/admin/reporting.json +298 -0
- package/package.json +106 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { reportDefinitions, reportRuns, reportVersions, } from "./schema.js";
|
|
3
|
+
export class ReportDefinitionRevisionConflictError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("The report draft changed since it was loaded.");
|
|
6
|
+
this.name = "ReportDefinitionRevisionConflictError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ReportingRecordNotFoundError extends Error {
|
|
10
|
+
constructor(record) {
|
|
11
|
+
super(`${record} was not found.`);
|
|
12
|
+
this.name = "ReportingRecordNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ReportDefinitionRetentionConflictError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super("Reports with retained execution history cannot be deleted.");
|
|
18
|
+
this.name = "ReportDefinitionRetentionConflictError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function createReportingService(registry) {
|
|
22
|
+
return {
|
|
23
|
+
async list(db, input) {
|
|
24
|
+
const [data, count] = await Promise.all([
|
|
25
|
+
db
|
|
26
|
+
.select()
|
|
27
|
+
.from(reportDefinitions)
|
|
28
|
+
.orderBy(desc(reportDefinitions.updatedAt))
|
|
29
|
+
.limit(input.limit)
|
|
30
|
+
.offset(input.offset),
|
|
31
|
+
db.select({ count: sql `count(*)::int` }).from(reportDefinitions),
|
|
32
|
+
]);
|
|
33
|
+
return { data, total: count[0]?.count ?? 0, ...input };
|
|
34
|
+
},
|
|
35
|
+
async get(db, id) {
|
|
36
|
+
const [row] = await db
|
|
37
|
+
.select()
|
|
38
|
+
.from(reportDefinitions)
|
|
39
|
+
.where(eq(reportDefinitions.id, id))
|
|
40
|
+
.limit(1);
|
|
41
|
+
return row ?? null;
|
|
42
|
+
},
|
|
43
|
+
async create(db, input, actorId) {
|
|
44
|
+
const [row] = await db
|
|
45
|
+
.insert(reportDefinitions)
|
|
46
|
+
.values({
|
|
47
|
+
...input,
|
|
48
|
+
description: input.description ?? null,
|
|
49
|
+
sourceTemplateId: input.sourceTemplateId ?? null,
|
|
50
|
+
sourceTemplateVersion: input.sourceTemplateVersion ?? null,
|
|
51
|
+
createdByUserId: actorId,
|
|
52
|
+
updatedByUserId: actorId,
|
|
53
|
+
})
|
|
54
|
+
.returning();
|
|
55
|
+
if (!row)
|
|
56
|
+
throw new Error("Report definition insert returned no row.");
|
|
57
|
+
return row;
|
|
58
|
+
},
|
|
59
|
+
async instantiateTemplate(db, input, actorId) {
|
|
60
|
+
const template = registry.getTemplate(input.templateId, input.version);
|
|
61
|
+
if (!template)
|
|
62
|
+
throw new ReportingRecordNotFoundError("Report template");
|
|
63
|
+
return this.create(db, {
|
|
64
|
+
name: input.name,
|
|
65
|
+
description: input.description,
|
|
66
|
+
sourceTemplateId: template.id,
|
|
67
|
+
sourceTemplateVersion: template.version,
|
|
68
|
+
draft: { parameters: {}, widgets: template.widgets },
|
|
69
|
+
}, actorId);
|
|
70
|
+
},
|
|
71
|
+
async update(db, id, input, actorId) {
|
|
72
|
+
const { revision, ...patch } = input;
|
|
73
|
+
const [row] = await db
|
|
74
|
+
.update(reportDefinitions)
|
|
75
|
+
.set({ ...patch, revision: revision + 1, updatedByUserId: actorId, updatedAt: new Date() })
|
|
76
|
+
.where(and(eq(reportDefinitions.id, id), eq(reportDefinitions.revision, revision)))
|
|
77
|
+
.returning();
|
|
78
|
+
if (row)
|
|
79
|
+
return row;
|
|
80
|
+
const existing = await this.get(db, id);
|
|
81
|
+
if (!existing)
|
|
82
|
+
throw new ReportingRecordNotFoundError("Report definition");
|
|
83
|
+
throw new ReportDefinitionRevisionConflictError();
|
|
84
|
+
},
|
|
85
|
+
async remove(db, id) {
|
|
86
|
+
return db.transaction(async (transaction) => {
|
|
87
|
+
const [definition] = await transaction
|
|
88
|
+
.select({ id: reportDefinitions.id })
|
|
89
|
+
.from(reportDefinitions)
|
|
90
|
+
.where(eq(reportDefinitions.id, id))
|
|
91
|
+
.limit(1)
|
|
92
|
+
.for("update");
|
|
93
|
+
if (!definition)
|
|
94
|
+
return false;
|
|
95
|
+
await transaction
|
|
96
|
+
.select({ id: reportVersions.id })
|
|
97
|
+
.from(reportVersions)
|
|
98
|
+
.where(eq(reportVersions.reportDefinitionId, id))
|
|
99
|
+
.for("update");
|
|
100
|
+
const [retainedRun] = await transaction
|
|
101
|
+
.select({ id: reportRuns.id })
|
|
102
|
+
.from(reportRuns)
|
|
103
|
+
.innerJoin(reportVersions, eq(reportRuns.reportVersionId, reportVersions.id))
|
|
104
|
+
.where(eq(reportVersions.reportDefinitionId, id))
|
|
105
|
+
.limit(1);
|
|
106
|
+
if (retainedRun)
|
|
107
|
+
throw new ReportDefinitionRetentionConflictError();
|
|
108
|
+
await transaction.delete(reportDefinitions).where(eq(reportDefinitions.id, id));
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
async createVersion(db, reportDefinitionId, expectedRevision, actorId) {
|
|
113
|
+
return db.transaction(async (transaction) => {
|
|
114
|
+
const [definition] = await transaction
|
|
115
|
+
.select()
|
|
116
|
+
.from(reportDefinitions)
|
|
117
|
+
.where(eq(reportDefinitions.id, reportDefinitionId))
|
|
118
|
+
.limit(1)
|
|
119
|
+
.for("update");
|
|
120
|
+
if (!definition)
|
|
121
|
+
throw new ReportingRecordNotFoundError("Report definition");
|
|
122
|
+
if (definition.revision !== expectedRevision)
|
|
123
|
+
throw new ReportDefinitionRevisionConflictError();
|
|
124
|
+
const [latest] = await transaction
|
|
125
|
+
.select({ version: reportVersions.version })
|
|
126
|
+
.from(reportVersions)
|
|
127
|
+
.where(eq(reportVersions.reportDefinitionId, reportDefinitionId))
|
|
128
|
+
.orderBy(desc(reportVersions.version))
|
|
129
|
+
.limit(1);
|
|
130
|
+
const [version] = await transaction
|
|
131
|
+
.insert(reportVersions)
|
|
132
|
+
.values({
|
|
133
|
+
reportDefinitionId,
|
|
134
|
+
version: (latest?.version ?? 0) + 1,
|
|
135
|
+
name: definition.name,
|
|
136
|
+
description: definition.description,
|
|
137
|
+
definitionRevision: definition.revision,
|
|
138
|
+
snapshot: registry.snapshotDraft(definition.draft),
|
|
139
|
+
createdByUserId: actorId,
|
|
140
|
+
})
|
|
141
|
+
.returning();
|
|
142
|
+
if (!version)
|
|
143
|
+
throw new Error("Report version insert returned no row.");
|
|
144
|
+
return version;
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
async runVersion(db, versionId, parameters, context) {
|
|
148
|
+
const execution = await db.transaction(async (transaction) => {
|
|
149
|
+
const [version] = await transaction
|
|
150
|
+
.select()
|
|
151
|
+
.from(reportVersions)
|
|
152
|
+
.where(eq(reportVersions.id, versionId))
|
|
153
|
+
.limit(1)
|
|
154
|
+
.for("share");
|
|
155
|
+
if (!version)
|
|
156
|
+
throw new ReportingRecordNotFoundError("Report version");
|
|
157
|
+
for (const resolved of registry.resolveDraft(version.snapshot, "edit")) {
|
|
158
|
+
if (resolved.status === "missing" || !resolved.definition)
|
|
159
|
+
continue;
|
|
160
|
+
registry.validateQuery(resolved.definition.query, context.grantedScopes);
|
|
161
|
+
}
|
|
162
|
+
const [run] = await transaction
|
|
163
|
+
.insert(reportRuns)
|
|
164
|
+
.values({ reportVersionId: versionId, parameters, triggeredByUserId: context.actorId })
|
|
165
|
+
.returning();
|
|
166
|
+
if (!run)
|
|
167
|
+
throw new Error("Report run insert returned no row.");
|
|
168
|
+
return { run, version };
|
|
169
|
+
});
|
|
170
|
+
const { run, version } = execution;
|
|
171
|
+
try {
|
|
172
|
+
const executionSignal = context.signal
|
|
173
|
+
? AbortSignal.any([context.signal, AbortSignal.timeout(60_000)])
|
|
174
|
+
: AbortSignal.timeout(60_000);
|
|
175
|
+
const output = await executeDraft(registry, db, version.snapshot, parameters, {
|
|
176
|
+
...context,
|
|
177
|
+
signal: executionSignal,
|
|
178
|
+
});
|
|
179
|
+
const failed = output.widgets.some((widget) => widget.status === "failed");
|
|
180
|
+
const [completed] = await db
|
|
181
|
+
.update(reportRuns)
|
|
182
|
+
.set({
|
|
183
|
+
status: failed ? "failed" : "succeeded",
|
|
184
|
+
output,
|
|
185
|
+
error: failed ? "One or more widgets failed." : null,
|
|
186
|
+
completedAt: new Date(),
|
|
187
|
+
})
|
|
188
|
+
.where(eq(reportRuns.id, run.id))
|
|
189
|
+
.returning();
|
|
190
|
+
return completed ?? run;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : "Report execution failed.";
|
|
194
|
+
const [failed] = await db
|
|
195
|
+
.update(reportRuns)
|
|
196
|
+
.set({ status: "failed", error: message, completedAt: new Date() })
|
|
197
|
+
.where(eq(reportRuns.id, run.id))
|
|
198
|
+
.returning();
|
|
199
|
+
return failed ?? run;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
async getRun(db, id, grantedScopes) {
|
|
203
|
+
const [run] = await db.select().from(reportRuns).where(eq(reportRuns.id, id)).limit(1);
|
|
204
|
+
if (run) {
|
|
205
|
+
const requiredScopes = [
|
|
206
|
+
...new Set(run.output?.widgets.flatMap((widget) => widget.requiredScopes ?? []) ?? []),
|
|
207
|
+
];
|
|
208
|
+
registry.requireScopes(requiredScopes, grantedScopes);
|
|
209
|
+
}
|
|
210
|
+
return run ?? null;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function executeDraft(registry, db, draft, runParameters, context) {
|
|
215
|
+
const parameters = { ...draft.parameters, ...runParameters };
|
|
216
|
+
const widgets = [];
|
|
217
|
+
for (const resolved of registry.resolveDraft(draft, "edit")) {
|
|
218
|
+
if (resolved.status === "missing" || !resolved.definition) {
|
|
219
|
+
widgets.push({
|
|
220
|
+
widgetInstanceId: resolved.instance.id,
|
|
221
|
+
status: "missing",
|
|
222
|
+
reason: resolved.missingReason ?? "Widget is unavailable.",
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
let provenance;
|
|
227
|
+
try {
|
|
228
|
+
const validation = registry.validateQuery(resolved.definition.query, context.grantedScopes);
|
|
229
|
+
provenance = {
|
|
230
|
+
datasetId: validation.dataset.definition.id,
|
|
231
|
+
datasetVersion: validation.dataset.definition.version,
|
|
232
|
+
requiredScopes: validation.requiredScopes,
|
|
233
|
+
};
|
|
234
|
+
const result = await registry.executeQuery({
|
|
235
|
+
db,
|
|
236
|
+
actorId: context.actorId,
|
|
237
|
+
grantedScopes: context.grantedScopes,
|
|
238
|
+
query: resolved.definition.query,
|
|
239
|
+
parameters,
|
|
240
|
+
signal: context.signal,
|
|
241
|
+
});
|
|
242
|
+
widgets.push({
|
|
243
|
+
widgetInstanceId: resolved.instance.id,
|
|
244
|
+
status: "succeeded",
|
|
245
|
+
...provenance,
|
|
246
|
+
result,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
widgets.push({
|
|
251
|
+
widgetInstanceId: resolved.instance.id,
|
|
252
|
+
status: "failed",
|
|
253
|
+
...provenance,
|
|
254
|
+
reason: error instanceof Error ? error.message : "Widget execution failed.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { widgets };
|
|
259
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static admin contribution metadata used by the Reporting deployment manifest.
|
|
3
|
+
* The selected factory contributes the operator navigation group and both
|
|
4
|
+
* routes; the per-route runtime carries the neutral factory the host binds.
|
|
5
|
+
*/
|
|
6
|
+
export declare const reportingVoyantAdmin: {
|
|
7
|
+
readonly compositionOrder: 60;
|
|
8
|
+
readonly runtime: {
|
|
9
|
+
readonly entry: "@voyant-travel/reporting-react/admin";
|
|
10
|
+
readonly export: "createSelectedReportingAdminExtension";
|
|
11
|
+
};
|
|
12
|
+
readonly routes: readonly [{
|
|
13
|
+
readonly id: "@voyant-travel/reporting#admin.route.index";
|
|
14
|
+
readonly path: "/reporting";
|
|
15
|
+
readonly requiredScopes: readonly ["reports:read"];
|
|
16
|
+
readonly runtime: {
|
|
17
|
+
readonly entry: "@voyant-travel/reporting-react/admin";
|
|
18
|
+
readonly export: "createReportingAdminExtension";
|
|
19
|
+
};
|
|
20
|
+
}, {
|
|
21
|
+
readonly id: "@voyant-travel/reporting#admin.route.detail";
|
|
22
|
+
readonly path: "/reporting/$id";
|
|
23
|
+
readonly requiredScopes: readonly ["reports:read"];
|
|
24
|
+
readonly runtime: {
|
|
25
|
+
readonly entry: "@voyant-travel/reporting-react/admin";
|
|
26
|
+
readonly export: "createReportingAdminExtension";
|
|
27
|
+
};
|
|
28
|
+
}];
|
|
29
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const reportingAdminRuntime = {
|
|
2
|
+
entry: "@voyant-travel/reporting-react/admin",
|
|
3
|
+
export: "createReportingAdminExtension",
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Static admin contribution metadata used by the Reporting deployment manifest.
|
|
7
|
+
* The selected factory contributes the operator navigation group and both
|
|
8
|
+
* routes; the per-route runtime carries the neutral factory the host binds.
|
|
9
|
+
*/
|
|
10
|
+
export const reportingVoyantAdmin = {
|
|
11
|
+
compositionOrder: 60,
|
|
12
|
+
runtime: {
|
|
13
|
+
entry: "@voyant-travel/reporting-react/admin",
|
|
14
|
+
export: "createSelectedReportingAdminExtension",
|
|
15
|
+
},
|
|
16
|
+
routes: [
|
|
17
|
+
{
|
|
18
|
+
id: "@voyant-travel/reporting#admin.route.index",
|
|
19
|
+
path: "/reporting",
|
|
20
|
+
requiredScopes: ["reports:read"],
|
|
21
|
+
runtime: reportingAdminRuntime,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "@voyant-travel/reporting#admin.route.detail",
|
|
25
|
+
path: "/reporting/$id",
|
|
26
|
+
requiredScopes: ["reports:read"],
|
|
27
|
+
runtime: reportingAdminRuntime,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
package/dist/voyant.d.ts
ADDED
package/dist/voyant.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { defineModule, requirePort } from "@voyant-travel/core/project";
|
|
2
|
+
import { reportingContributionRuntimePort } from "@voyant-travel/reporting-contracts/runtime-port";
|
|
3
|
+
import { reportingVoyantAdmin } from "./voyant-admin.js";
|
|
4
|
+
export const reportingVoyantModule = defineModule({
|
|
5
|
+
id: "@voyant-travel/reporting",
|
|
6
|
+
packageName: "@voyant-travel/reporting",
|
|
7
|
+
localId: "reporting",
|
|
8
|
+
runtimePorts: [
|
|
9
|
+
requirePort(reportingContributionRuntimePort, { optional: true, cardinality: "many" }),
|
|
10
|
+
],
|
|
11
|
+
reporting: {
|
|
12
|
+
templates: [
|
|
13
|
+
{
|
|
14
|
+
id: "reporting.template.operator-overview",
|
|
15
|
+
version: 1,
|
|
16
|
+
label: "Operator overview",
|
|
17
|
+
description: "A cross-module overview of booking activity and final-invoice receivables.",
|
|
18
|
+
requirements: [
|
|
19
|
+
{ kind: "widget", id: "bookings.widget.total" },
|
|
20
|
+
{ kind: "widget", id: "bookings.widget.monthly-trend" },
|
|
21
|
+
{ kind: "widget", id: "finance.outstanding-by-currency" },
|
|
22
|
+
{ kind: "widget", id: "finance.net-issued-trend" },
|
|
23
|
+
{ kind: "widget", id: "finance.invoice-status-breakdown" },
|
|
24
|
+
],
|
|
25
|
+
widgets: [
|
|
26
|
+
{
|
|
27
|
+
id: "total-bookings",
|
|
28
|
+
widgetId: "bookings.widget.total",
|
|
29
|
+
widgetVersion: 1,
|
|
30
|
+
layout: { x: 0, y: 0, width: 3, height: 2 },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "outstanding-receivables",
|
|
34
|
+
widgetId: "finance.outstanding-by-currency",
|
|
35
|
+
widgetVersion: 1,
|
|
36
|
+
layout: { x: 3, y: 0, width: 4, height: 3 },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "invoice-status",
|
|
40
|
+
widgetId: "finance.invoice-status-breakdown",
|
|
41
|
+
widgetVersion: 1,
|
|
42
|
+
layout: { x: 7, y: 0, width: 5, height: 3 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "monthly-bookings",
|
|
46
|
+
widgetId: "bookings.widget.monthly-trend",
|
|
47
|
+
widgetVersion: 1,
|
|
48
|
+
layout: { x: 0, y: 3, width: 6, height: 4 },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "net-issued",
|
|
52
|
+
widgetId: "finance.net-issued-trend",
|
|
53
|
+
widgetVersion: 1,
|
|
54
|
+
layout: { x: 6, y: 3, width: 6, height: 4 },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
api: [
|
|
61
|
+
{
|
|
62
|
+
id: "@voyant-travel/reporting#api.admin",
|
|
63
|
+
surface: "admin",
|
|
64
|
+
mount: "reporting",
|
|
65
|
+
resource: "reports",
|
|
66
|
+
authorization: "route",
|
|
67
|
+
openapi: { document: "reporting" },
|
|
68
|
+
runtime: {
|
|
69
|
+
entry: "@voyant-travel/reporting/api-runtime",
|
|
70
|
+
export: "createReportingApiModule",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
schema: [{ id: "@voyant-travel/reporting#schema", source: "@voyant-travel/reporting/schema" }],
|
|
75
|
+
migrations: [{ id: "@voyant-travel/reporting#migrations", source: "./migrations" }],
|
|
76
|
+
resources: [
|
|
77
|
+
{
|
|
78
|
+
id: "@voyant-travel/reporting#resource.database",
|
|
79
|
+
kind: "database",
|
|
80
|
+
required: true,
|
|
81
|
+
config: { engine: "postgres" },
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
access: {
|
|
85
|
+
resources: [
|
|
86
|
+
{
|
|
87
|
+
id: "@voyant-travel/reporting#access.reports",
|
|
88
|
+
resource: "reports",
|
|
89
|
+
label: "Reports",
|
|
90
|
+
description: "Build, view, publish, and export reports.",
|
|
91
|
+
actions: [
|
|
92
|
+
{
|
|
93
|
+
action: "read",
|
|
94
|
+
label: "View reports",
|
|
95
|
+
description: "View reports and preview queries.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
action: "write",
|
|
99
|
+
label: "Manage reports",
|
|
100
|
+
description: "Create, edit, remove, and publish reports.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
action: "export",
|
|
104
|
+
label: "Run and export reports",
|
|
105
|
+
description: "Execute immutable report versions and access their output.",
|
|
106
|
+
sensitive: true,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
admin: reportingVoyantAdmin,
|
|
113
|
+
lifecycle: { uninstall: { default: "retain-data", purge: "not-supported" } },
|
|
114
|
+
meta: { ownership: "package" },
|
|
115
|
+
});
|
|
116
|
+
export default reportingVoyantModule;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
CREATE TYPE "public"."report_run_status" AS ENUM('running', 'succeeded', 'failed');
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
CREATE TABLE "report_definitions" (
|
|
4
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
5
|
+
"name" text NOT NULL,
|
|
6
|
+
"description" text,
|
|
7
|
+
"source_template_id" text,
|
|
8
|
+
"source_template_version" integer,
|
|
9
|
+
"draft" jsonb NOT NULL,
|
|
10
|
+
"revision" integer DEFAULT 1 NOT NULL,
|
|
11
|
+
"created_by_user_id" text,
|
|
12
|
+
"updated_by_user_id" text,
|
|
13
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
14
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE "report_versions" (
|
|
18
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
19
|
+
"report_definition_id" text NOT NULL,
|
|
20
|
+
"version" integer NOT NULL,
|
|
21
|
+
"name" text NOT NULL,
|
|
22
|
+
"description" text,
|
|
23
|
+
"definition_revision" integer NOT NULL,
|
|
24
|
+
"snapshot" jsonb NOT NULL,
|
|
25
|
+
"created_by_user_id" text,
|
|
26
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
27
|
+
);
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
CREATE TABLE "report_runs" (
|
|
30
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
31
|
+
"report_version_id" text NOT NULL,
|
|
32
|
+
"status" "report_run_status" DEFAULT 'running' NOT NULL,
|
|
33
|
+
"parameters" jsonb NOT NULL,
|
|
34
|
+
"output" jsonb,
|
|
35
|
+
"error" text,
|
|
36
|
+
"triggered_by_user_id" text,
|
|
37
|
+
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
38
|
+
"completed_at" timestamp with time zone,
|
|
39
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
40
|
+
);
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
ALTER TABLE "report_versions" ADD CONSTRAINT "report_versions_report_definition_id_report_definitions_id_fk" FOREIGN KEY ("report_definition_id") REFERENCES "public"."report_definitions"("id") ON DELETE cascade ON UPDATE no action;
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
ALTER TABLE "report_runs" ADD CONSTRAINT "report_runs_report_version_id_report_versions_id_fk" FOREIGN KEY ("report_version_id") REFERENCES "public"."report_versions"("id") ON DELETE restrict ON UPDATE no action;
|
|
45
|
+
--> statement-breakpoint
|
|
46
|
+
CREATE INDEX "idx_report_definitions_updated" ON "report_definitions" USING btree ("updated_at");
|
|
47
|
+
--> statement-breakpoint
|
|
48
|
+
CREATE UNIQUE INDEX "uidx_report_versions_definition_version" ON "report_versions" USING btree ("report_definition_id","version");
|
|
49
|
+
--> statement-breakpoint
|
|
50
|
+
CREATE INDEX "idx_report_versions_definition_created" ON "report_versions" USING btree ("report_definition_id","created_at");
|
|
51
|
+
--> statement-breakpoint
|
|
52
|
+
CREATE INDEX "idx_report_runs_version_created" ON "report_runs" USING btree ("report_version_id","created_at");
|
|
53
|
+
--> statement-breakpoint
|
|
54
|
+
CREATE INDEX "idx_report_runs_status_started" ON "report_runs" USING btree ("status","started_at");
|