@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.
@@ -0,0 +1,12 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import { type ReportingRegistry } from "./registry.js";
4
+ type Env = {
5
+ Variables: {
6
+ db: PostgresJsDatabase;
7
+ userId?: string;
8
+ scopes?: string[];
9
+ };
10
+ };
11
+ export declare function createReportingRoutes(registry: ReportingRegistry): OpenAPIHono<Env, {}, "/">;
12
+ export {};
package/dist/routes.js ADDED
@@ -0,0 +1,153 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { ForbiddenApiError, openApiValidationHook, parseJsonBody, parseQuery, RequestValidationError, } from "@voyant-travel/hono";
3
+ import { createReportDefinitionSchema, createReportVersionSchema, executeReportVersionSchema, instantiateReportTemplateSchema, listReportDefinitionsQuerySchema, parseReportQuery, parseReportQuerySourceSchema, previewReportQuerySchema, ReportQuerySyntaxError, updateReportDefinitionSchema, } from "@voyant-travel/reporting-contracts";
4
+ import { hasApiKeyPermission, permissionStringsToPermissions } from "@voyant-travel/types/api-keys";
5
+ import { ReportingAuthorizationError, ReportingRegistryError, } from "./registry.js";
6
+ import { createReportingService, ReportDefinitionRetentionConflictError, ReportDefinitionRevisionConflictError, ReportingRecordNotFoundError, } from "./service.js";
7
+ export function createReportingRoutes(registry) {
8
+ const routes = new OpenAPIHono({ defaultHook: openApiValidationHook });
9
+ const service = createReportingService(registry);
10
+ routes.get("/catalog", (c) => {
11
+ requireReportsPermission(c.get("scopes"), "read");
12
+ return c.json({ data: registry.catalog() });
13
+ });
14
+ routes.post("/queries/parse", async (c) => {
15
+ requireReportsPermission(c.get("scopes"), "read");
16
+ const input = await parseJsonBody(c, parseReportQuerySourceSchema);
17
+ try {
18
+ return c.json({ data: parseReportQuery(input.source) });
19
+ }
20
+ catch (error) {
21
+ if (error instanceof ReportQuerySyntaxError)
22
+ throw new RequestValidationError(error.message);
23
+ throw error;
24
+ }
25
+ });
26
+ routes.post("/queries/preview", async (c) => {
27
+ requireReportsPermission(c.get("scopes"), "read");
28
+ const input = await parseJsonBody(c, previewReportQuerySchema);
29
+ try {
30
+ const data = await registry.executeQuery({
31
+ db: c.get("db"),
32
+ actorId: c.get("userId"),
33
+ grantedScopes: c.get("scopes") ?? [],
34
+ query: input.query,
35
+ parameters: input.parameters,
36
+ signal: c.req.raw.signal,
37
+ });
38
+ return c.json({ data });
39
+ }
40
+ catch (error) {
41
+ return reportingErrorResponse(c, error);
42
+ }
43
+ });
44
+ routes.get("/reports", async (c) => {
45
+ requireReportsPermission(c.get("scopes"), "read");
46
+ const query = parseQuery(c, listReportDefinitionsQuerySchema);
47
+ return c.json(await service.list(c.get("db"), query));
48
+ });
49
+ routes.post("/reports", async (c) => {
50
+ requireReportsPermission(c.get("scopes"), "write");
51
+ const input = await parseJsonBody(c, createReportDefinitionSchema);
52
+ return c.json({ data: await service.create(c.get("db"), input, c.get("userId")) }, 201);
53
+ });
54
+ routes.get("/reports/:id", async (c) => {
55
+ requireReportsPermission(c.get("scopes"), "read");
56
+ const report = await service.get(c.get("db"), c.req.param("id"));
57
+ return report ? c.json({ data: report }) : c.json({ error: "report_not_found" }, 404);
58
+ });
59
+ routes.patch("/reports/:id", async (c) => {
60
+ requireReportsPermission(c.get("scopes"), "write");
61
+ const input = await parseJsonBody(c, updateReportDefinitionSchema);
62
+ try {
63
+ const report = await service.update(c.get("db"), c.req.param("id"), input, c.get("userId"));
64
+ return c.json({ data: report });
65
+ }
66
+ catch (error) {
67
+ return reportingErrorResponse(c, error);
68
+ }
69
+ });
70
+ routes.delete("/reports/:id", async (c) => {
71
+ requireReportsPermission(c.get("scopes"), "write");
72
+ try {
73
+ return (await service.remove(c.get("db"), c.req.param("id")))
74
+ ? c.json({ success: true })
75
+ : c.json({ error: "report_not_found" }, 404);
76
+ }
77
+ catch (error) {
78
+ return reportingErrorResponse(c, error);
79
+ }
80
+ });
81
+ routes.post("/templates/:id/instantiate", async (c) => {
82
+ requireReportsPermission(c.get("scopes"), "write");
83
+ const input = await parseJsonBody(c, instantiateReportTemplateSchema);
84
+ try {
85
+ const report = await service.instantiateTemplate(c.get("db"), { templateId: c.req.param("id"), ...input }, c.get("userId"));
86
+ return c.json({ data: report }, 201);
87
+ }
88
+ catch (error) {
89
+ return reportingErrorResponse(c, error);
90
+ }
91
+ });
92
+ routes.post("/reports/:id/versions", async (c) => {
93
+ requireReportsPermission(c.get("scopes"), "write");
94
+ const input = await parseJsonBody(c, createReportVersionSchema);
95
+ try {
96
+ const version = await service.createVersion(c.get("db"), c.req.param("id"), input.expectedRevision, c.get("userId"));
97
+ return c.json({ data: version }, 201);
98
+ }
99
+ catch (error) {
100
+ return reportingErrorResponse(c, error);
101
+ }
102
+ });
103
+ routes.post("/versions/:id/runs", async (c) => {
104
+ requireReportsPermission(c.get("scopes"), "export");
105
+ const input = await parseJsonBody(c, executeReportVersionSchema);
106
+ try {
107
+ const run = await service.runVersion(c.get("db"), c.req.param("id"), input.parameters, {
108
+ actorId: c.get("userId"),
109
+ grantedScopes: c.get("scopes") ?? [],
110
+ signal: c.req.raw.signal,
111
+ });
112
+ return c.json({ data: run }, 201);
113
+ }
114
+ catch (error) {
115
+ return reportingErrorResponse(c, error);
116
+ }
117
+ });
118
+ routes.get("/runs/:id", async (c) => {
119
+ requireReportsPermission(c.get("scopes"), "export");
120
+ try {
121
+ const run = await service.getRun(c.get("db"), c.req.param("id"), c.get("scopes") ?? []);
122
+ if (!run)
123
+ return c.json({ error: "report_run_not_found" }, 404);
124
+ return c.json({ data: run });
125
+ }
126
+ catch (error) {
127
+ return reportingErrorResponse(c, error);
128
+ }
129
+ });
130
+ return routes;
131
+ }
132
+ function requireReportsPermission(scopes, action) {
133
+ if (!hasApiKeyPermission(permissionStringsToPermissions(scopes ?? []), "reports", action)) {
134
+ throw new ForbiddenApiError();
135
+ }
136
+ }
137
+ function reportingErrorResponse(c, error) {
138
+ if (error instanceof ReportingRecordNotFoundError)
139
+ return c.json({ error: "not_found" }, 404);
140
+ if (error instanceof ReportDefinitionRevisionConflictError) {
141
+ return c.json({ error: "revision_conflict" }, 409);
142
+ }
143
+ if (error instanceof ReportDefinitionRetentionConflictError) {
144
+ return c.json({ error: "report_has_retained_runs" }, 409);
145
+ }
146
+ if (error instanceof ReportingAuthorizationError) {
147
+ return c.json({ error: "forbidden", missingScopes: error.missingScopes }, 403);
148
+ }
149
+ if (error instanceof ReportingRegistryError) {
150
+ return c.json({ error: "invalid_report_query", message: error.message }, 400);
151
+ }
152
+ throw error;
153
+ }