@lobb-js/lobb-ext-reports 0.11.0 → 0.12.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.
Files changed (25) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +9 -3
  4. package/dist/lib/components/gridStack.svelte +16 -4
  5. package/dist/lib/components/pages/reports/components/chart.svelte +64 -39
  6. package/dist/lib/components/pages/reports/components/charts/table.svelte +0 -1
  7. package/dist/lib/components/pages/reports/components/report.svelte +115 -102
  8. package/dist/lib/components/pages/reports/index.svelte +5 -11
  9. package/dist/lib/components/pages/shared_report/index.svelte +109 -0
  10. package/dist/lib/components/pages/shared_report/index.svelte.d.ts +14 -0
  11. package/dist/lib/components/reportBody.svelte +119 -0
  12. package/dist/lib/components/reportBody.svelte.d.ts +14 -0
  13. package/extensions/reports/index.ts +33 -5
  14. package/extensions/reports/studio/index.ts +10 -3
  15. package/extensions/reports/studio/lib/components/gridStack.svelte +16 -4
  16. package/extensions/reports/studio/lib/components/pages/reports/components/chart.svelte +64 -39
  17. package/extensions/reports/studio/lib/components/pages/reports/components/charts/table.svelte +0 -1
  18. package/extensions/reports/studio/lib/components/pages/reports/components/report.svelte +115 -102
  19. package/extensions/reports/studio/lib/components/pages/reports/index.svelte +5 -11
  20. package/extensions/reports/studio/lib/components/pages/shared_report/index.svelte +109 -0
  21. package/extensions/reports/studio/lib/components/reportBody.svelte +119 -0
  22. package/extensions/reports/workflows.ts +73 -0
  23. package/package.json +3 -3
  24. package/extensions/reports/collectionRoutes.ts +0 -10
  25. package/extensions/reports/controllers/chartsController.ts +0 -78
@@ -1,38 +1,31 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
- import GridStackComponent from "../../../gridStack.svelte";
4
- import Chart from "./chart.svelte";
3
+ import ReportBody from "../../../reportBody.svelte";
5
4
 
6
- const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
5
+ const { reportId, utils }: { reportId: string; utils: any } = $props();
7
6
  const {
8
7
  SidebarTrigger,
9
8
  CreateDetailViewButton,
9
+ Button,
10
10
  Icons,
11
11
  } = utils.components;
12
12
 
13
13
  let report: any = $state(null);
14
- let charts: any[] = $state([]);
15
- let loading = $state(true);
16
14
  let saving = $state(false);
17
15
  let editable = $state(false);
16
+ let canShare = $state(false);
17
+ let sharing = $state(false);
18
+ // Handle captured from ReportBody via its onReady callback — lets us
19
+ // trigger a reload of charts after creating a new one without duplicating
20
+ // the fetch logic here.
21
+ let body: { reload: () => Promise<void> } | undefined = $state();
18
22
 
19
- async function loadCharts({ showLoading = true } = {}) {
20
- if (showLoading) loading = true;
21
- const reportsRes = await utils.lobb.findAll("reports_dashboards", {
22
- sort: "id",
23
- filter: { id: reportId },
24
- });
25
- report = (await reportsRes.json()).data[0];
26
- if (!report) {
27
- if (showLoading) loading = false;
28
- return;
29
- }
23
+ function handleReportLoaded(r: any) {
24
+ report = r;
25
+ }
30
26
 
31
- const chartsRes = await utils.lobb.findAll("reports_charts", {
32
- filter: { report_id: report.id },
33
- });
34
- charts = (await chartsRes.json()).data;
35
- if (showLoading) loading = false;
27
+ function handleReady(api: { reload: () => Promise<void> }) {
28
+ body = api;
36
29
  }
37
30
 
38
31
  async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
@@ -47,10 +40,6 @@
47
40
  saving = false;
48
41
  }
49
42
 
50
- function getChartGridSize(chart: any): Record<string, unknown> {
51
- return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
52
- }
53
-
54
43
  function handleChartClick(_event: any, elements: any[], chart: any) {
55
44
  if (!elements.length) return;
56
45
  const { index, datasetIndex } = elements[0];
@@ -62,100 +51,124 @@
62
51
  });
63
52
  }
64
53
 
65
- onMount(() => loadCharts());
54
+ async function checkSharePermission() {
55
+ const allowed = await utils.emitEvent("auth.canAccess", {
56
+ collection: "auth_shares",
57
+ action: "create",
58
+ });
59
+ canShare = allowed === true;
60
+ }
61
+
62
+ async function handleShare() {
63
+ sharing = true;
64
+ try {
65
+ const event = {
66
+ reportId: String(reportId),
67
+ report,
68
+ handled: false,
69
+ };
70
+ await utils.emitEvent("reports.share", event);
71
+ if (event.handled) return;
72
+
73
+ const basePermissions: Record<string, any> = {
74
+ reports_dashboards: { read: { filter: { id: Number(reportId) } } },
75
+ reports_charts: { read: { filter: { report_id: Number(reportId) } } },
76
+ };
77
+ const shareReadAccess = (utils.ctx.meta?.extensions?.reports?.shareReadAccess ?? {}) as Record<string, any>;
78
+ const extraPermissions: Record<string, any> = {};
79
+ for (const [collectionName, readGrant] of Object.entries(shareReadAccess)) {
80
+ extraPermissions[collectionName] = { read: readGrant };
81
+ }
82
+ const permissions = { ...basePermissions, ...extraPermissions };
83
+ const response = await utils.lobb.createOne("auth_shares", {
84
+ label: `Share for: ${report?.name ?? "report"}`,
85
+ permissions: JSON.stringify(permissions),
86
+ expires_in_seconds: 7 * 24 * 60 * 60,
87
+ });
88
+ if (!response.ok) {
89
+ const body = await response.json().catch(() => null);
90
+ utils.toast.error(body?.message ?? "Failed to create share");
91
+ return;
92
+ }
93
+ const body = await response.json();
94
+ const url = `${window.location.origin}/studio/public/reports/shared_report?report_id=${reportId}&share_token=${body.data.token}`;
95
+ await navigator.clipboard.writeText(url);
96
+ utils.toast.success("Share link copied to clipboard");
97
+ } catch (err) {
98
+ utils.toast.error(err instanceof Error ? err.message : "Failed to create share");
99
+ } finally {
100
+ sharing = false;
101
+ }
102
+ }
103
+
104
+ onMount(() => {
105
+ checkSharePermission();
106
+ });
66
107
  </script>
67
108
 
68
109
  <div class="report-layout">
69
- {#if loading}
70
- <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
71
- <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
72
- <div class="flex flex-col items-center justify-center">
73
- <div class="text-muted-foreground">Loading the dashboard...</div>
74
- <div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
75
- </div>
76
- </div>
77
- {:else if !report}
78
- <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
79
- <Icons.CircleSlash2 class="opacity-50" size="50" />
80
- <div class="flex flex-col items-center justify-center">
81
- <div class="text-muted-foreground">Report not found</div>
82
- <div class="text-xs text-muted-foreground">The report you're looking for doesn't exist or couldn't be loaded</div>
110
+ <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
111
+ <div class="flex gap-2">
112
+ <div class="mt-1"><SidebarTrigger /></div>
113
+ <div class="flex flex-col justify-center">
114
+ <h2 class="font-medium text-primary">{report?.name ?? ""}</h2>
115
+ <div class="text-xs text-muted-foreground">{report?.description ?? ""}</div>
83
116
  </div>
84
117
  </div>
85
- {:else}
86
- <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
87
- <div class="flex gap-2">
88
- <div class="mt-1"><SidebarTrigger /></div>
89
- <div class="flex flex-col justify-center">
90
- <h2 class="font-medium text-primary">{report?.name}</h2>
91
- <div class="text-xs text-muted-foreground">{report?.description}</div>
118
+ <div class="flex items-center gap-2 self-end">
119
+ {#if saving}
120
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
121
+ <Icons.LoaderCircle class="animate-spin" size={12} />
122
+ Saving...
92
123
  </div>
93
- </div>
94
- <div class="flex items-center gap-2 self-end">
95
- {#if saving}
96
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
97
- <Icons.LoaderCircle class="animate-spin" size={12} />
98
- Saving...
99
- </div>
100
- {:else if editable}
101
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
102
- <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
103
- Layout editable
104
- </div>
105
- {:else}
106
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
107
- <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
108
- Expand window to rearrange
109
- </div>
110
- {/if}
124
+ {:else if editable}
125
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
126
+ <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
127
+ Layout editable
128
+ </div>
129
+ {:else}
130
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
131
+ <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
132
+ Expand window to rearrange
133
+ </div>
134
+ {/if}
135
+ {#if canShare}
136
+ <Button
137
+ variant="outline"
138
+ class="h-7 px-3 text-xs font-normal"
139
+ Icon={sharing ? Icons.LoaderCircle : Icons.Share2}
140
+ disabled={sharing}
141
+ onclick={handleShare}
142
+ >
143
+ {sharing ? "Sharing..." : "Share"}
144
+ </Button>
145
+ {/if}
146
+ {#if report}
111
147
  <CreateDetailViewButton
112
148
  collectionName="reports_charts"
113
149
  values={{ report_id: { id: reportId, name: report.name } }}
114
150
  variant="default"
115
151
  class="h-7 px-3 text-xs font-normal"
116
152
  Icon={Icons.Plus}
117
- onSuccessfullSave={async () => await loadCharts()}
153
+ onSuccessfullSave={async () => await body?.reload()}
118
154
  >
119
155
  Create chart
120
156
  </CreateDetailViewButton>
121
- </div>
122
- </div>
123
-
124
- <div class="charts-export-container overflow-auto p-2">
125
- {#if charts.length === 0}
126
- <div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
127
- <Icons.CircleSlash2 class="opacity-50" size="50" />
128
- <div class="flex flex-col items-center justify-center">
129
- <div>No charts available</div>
130
- <div class="text-xs">Create a new chart to fill this report page</div>
131
- </div>
132
- </div>
133
- {:else}
134
- {#key charts}
135
- <GridStackComponent bind:editable onLayoutChange={handleLayoutChange}>
136
- {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
137
- <div
138
- class="grid-stack-item"
139
- data-gs-id={chart.id}
140
- {...getChartGridSize(chart)}
141
- >
142
- <div class="grid-stack-item-content">
143
- <Chart
144
- chartRecord={chart}
145
- onChartDeleted={async () => await loadCharts()}
146
- onChartEdited={async () => await loadCharts()}
147
- onChartClick={handleChartClick}
148
- {utils}
149
- {...props}
150
- />
151
- </div>
152
- </div>
153
- {/each}
154
- </GridStackComponent>
155
- {/key}
156
157
  {/if}
157
158
  </div>
158
- {/if}
159
+ </div>
160
+
161
+ <div class="charts-export-container overflow-auto p-2">
162
+ <ReportBody
163
+ {reportId}
164
+ {utils}
165
+ allowEdit
166
+ onLayoutChange={handleLayoutChange}
167
+ onChartClick={handleChartClick}
168
+ onReportLoaded={handleReportLoaded}
169
+ onReady={handleReady}
170
+ />
171
+ </div>
159
172
  </div>
160
173
 
161
174
  <style>
@@ -1,12 +1,11 @@
1
1
  <script lang="ts">
2
2
  import type { ExtensionProps } from "@lobb-js/studio";
3
- import type { Location } from "@wjfe/n-savant";
4
3
  import Report from "./components/report.svelte";
5
4
 
6
5
  const { utils, ...props }: ExtensionProps = $props();
7
6
 
8
- let extensionPagePath = "/studio/extensions/reports/analytics";
9
- let reportId: string | null = $derived(getReportID(utils.location));
7
+ let extensionPagePath = "/studio/extensions/reports/dashboards";
8
+ let reportId: string | null = $derived(utils.page.url.pathname.split("/")[5] ?? null);
10
9
  let sideBarData: any = $state(null);
11
10
  const {
12
11
  CreateDetailViewButton,
@@ -34,9 +33,7 @@
34
33
  name: report.name,
35
34
  icon: icons.FileChartColumn,
36
35
  onclick: () => {
37
- utils.location.navigate(
38
- `${extensionPagePath}/${report.id}`,
39
- );
36
+ utils.goto(`${extensionPagePath}/${report.id}`);
40
37
  },
41
38
  meta: {
42
39
  reportId: report.id,
@@ -57,12 +54,9 @@
57
54
  }
58
55
  }
59
56
 
60
- function getReportID(location: Location) {
61
- return location.url.pathname.split("/")[5];
62
- }
63
57
  </script>
64
58
 
65
- <Sidebar title="Reports" data={sideBarData}>
59
+ <Sidebar title="Dashboards" data={sideBarData}>
66
60
  {#snippet belowSearch()}
67
61
  <div class="p-2">
68
62
  <CreateDetailViewButton
@@ -129,7 +123,7 @@
129
123
  class="h-7 px-3 text-xs font-normal"
130
124
  Icon={icons.Plus}
131
125
  onSuccessfullSave={async (record: any) =>
132
- utils.location.navigate(`?report=${record.id}`)}
126
+ utils.goto(`?report=${record.id}`)}
133
127
  >
134
128
  Create a report
135
129
  </CreateDetailViewButton>
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import ReportBody from "../../reportBody.svelte";
4
+
5
+ const { utils }: { utils: any } = $props();
6
+ const { Icons } = utils.components;
7
+
8
+ // Pull share_token and report_id from the URL. The token is what
9
+ // authorises every request below — we install it as the LobbClient's
10
+ // Authorization header so the existing CRUD helpers work transparently.
11
+ // The report_id is what we render. Both come from the link minted by
12
+ // the "Share" button on the gated report view.
13
+ const params = new URLSearchParams(window.location.search);
14
+ const shareToken = params.get("share_token");
15
+ const reportId = params.get("report_id");
16
+
17
+ let ready = $state(false);
18
+ let report: any = $state(null);
19
+ let error: string | null = $state(null);
20
+
21
+ function handleReportLoaded(r: any) {
22
+ report = r;
23
+ if (!r) error = "The shared report could not be found.";
24
+ }
25
+
26
+ onMount(async () => {
27
+ if (!shareToken || !reportId) {
28
+ error = "This share link is missing required parameters.";
29
+ ready = true;
30
+ return;
31
+ }
32
+
33
+ // Swap the LobbClient's bearer to the share token for the duration
34
+ // of this page. The studio shell already short-circuited the auth
35
+ // gate for /studio/public/*, so there's no session header to clobber.
36
+ utils.lobb.setHeaders({ Authorization: `Bearer ${shareToken}` });
37
+
38
+ // Populate ctx.extensions.auth.permissions from the share's snapshot
39
+ // so client-side gates (auth.canAccess) match the server's enforcement.
40
+ // Reuses the same /me endpoint logged-in users hit — for a share
41
+ // bearer, meAlias short-circuits with { data: null, permissions }.
42
+ try {
43
+ const meRes = await utils.lobb.findOne("auth_users", "me");
44
+ if (meRes.ok) {
45
+ const meBody = await meRes.json();
46
+ if (!utils.ctx.extensions.auth) utils.ctx.extensions.auth = {};
47
+ utils.ctx.extensions.auth.permissions = meBody.permissions;
48
+ utils.ctx.extensions.auth.user = meBody.data;
49
+ }
50
+ } catch {
51
+ // Soft-fail — the page will still render, UI gates just default to
52
+ // hidden, and the server-side enforcement is the real safety net.
53
+ }
54
+
55
+ ready = true;
56
+ });
57
+ </script>
58
+
59
+ <div class="shared-report-layout">
60
+ {#if !ready}
61
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
62
+ <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
63
+ <div class="flex flex-col items-center justify-center">
64
+ <div class="text-muted-foreground">Loading the shared report...</div>
65
+ </div>
66
+ </div>
67
+ {:else if error}
68
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
69
+ <Icons.CircleSlash2 class="opacity-50" size="50" />
70
+ <div class="flex flex-col items-center justify-center">
71
+ <div class="text-muted-foreground">Unavailable</div>
72
+ <div class="text-xs text-muted-foreground">{error}</div>
73
+ </div>
74
+ </div>
75
+ {:else}
76
+ <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
77
+ <div class="flex flex-col justify-center">
78
+ <h2 class="font-medium text-primary">{report?.name ?? ""}</h2>
79
+ <div class="text-xs text-muted-foreground">{report?.description ?? ""}</div>
80
+ </div>
81
+ <div class="text-xs text-muted-foreground">Shared view</div>
82
+ </div>
83
+
84
+ <div class="bg-muted overflow-auto p-2">
85
+ <ReportBody reportId={reportId ?? ""} {utils} onReportLoaded={handleReportLoaded} />
86
+ </div>
87
+ {/if}
88
+
89
+ <!-- Subtle attribution on shared / public pages only. Sits over the
90
+ charts area in the bottom-right; the gated dashboard never renders
91
+ this component so customers' main UI stays unbranded. -->
92
+ <a
93
+ href="https://lobb.app"
94
+ target="_blank"
95
+ rel="noopener"
96
+ class="fixed bottom-2 right-6 text-[10px] text-muted-foreground/60 hover:text-muted-foreground"
97
+ >
98
+ Made with Lobb
99
+ </a>
100
+ </div>
101
+
102
+ <style>
103
+ .shared-report-layout {
104
+ display: grid;
105
+ grid-template-rows: auto 1fr;
106
+ width: 100%;
107
+ height: 100%;
108
+ }
109
+ </style>
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { ExtensionProps } from "@lobb-js/studio";
4
+ import GridStackComponent from "./gridStack.svelte";
5
+ import Chart from "./pages/reports/components/chart.svelte";
6
+
7
+ interface Props extends ExtensionProps {
8
+ reportId: string | number;
9
+ editable?: boolean;
10
+ allowEdit?: boolean;
11
+ showChartHeaders?: boolean;
12
+ onLayoutChange?: (changes: any[]) => void;
13
+ onChartClick?: (event: any, elements: any[], chart: any) => void;
14
+ onReportLoaded?: (report: any) => void;
15
+ onReady?: (api: { reload: () => Promise<void> }) => void;
16
+ }
17
+
18
+ const {
19
+ reportId,
20
+ editable = false,
21
+ allowEdit = false,
22
+ showChartHeaders = true,
23
+ onLayoutChange,
24
+ onChartClick,
25
+ onReportLoaded,
26
+ onReady,
27
+ ...props
28
+ }: Props = $props();
29
+
30
+ const utils = props.utils;
31
+
32
+ const { Icons } = utils.components;
33
+
34
+ let report: any = $state(null);
35
+ let charts: any[] = $state([]);
36
+ let loading = $state(true);
37
+
38
+ async function loadCharts({ showLoading = true } = {}) {
39
+ if (showLoading) loading = true;
40
+ const reportsRes = await utils.lobb.findAll("reports_dashboards", {
41
+ sort: "id",
42
+ filter: { id: reportId },
43
+ });
44
+ report = (await reportsRes.json()).data[0];
45
+ if (onReportLoaded) onReportLoaded(report);
46
+ if (!report) {
47
+ if (showLoading) loading = false;
48
+ return;
49
+ }
50
+
51
+ const chartsRes = await utils.lobb.findAll("reports_charts", {
52
+ filter: { report_id: report.id },
53
+ });
54
+ charts = (await chartsRes.json()).data;
55
+ if (showLoading) loading = false;
56
+ }
57
+
58
+ function getChartGridSize(chart: any): Record<string, unknown> {
59
+ return {
60
+ "gs-w": chart.col_span ?? 6,
61
+ "gs-h": chart.row_span ?? 2,
62
+ "gs-auto-position": "true",
63
+ };
64
+ }
65
+
66
+ onMount(async () => {
67
+ await loadCharts();
68
+ if (onReady) onReady({ reload: () => loadCharts() });
69
+ });
70
+ </script>
71
+
72
+ {#if loading}
73
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4">
74
+ <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
75
+ <div class="flex flex-col items-center justify-center">
76
+ <div class="text-muted-foreground">Loading the dashboard...</div>
77
+ <div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
78
+ </div>
79
+ </div>
80
+ {:else if !report}
81
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4">
82
+ <Icons.CircleSlash2 class="opacity-50" size="50" />
83
+ <div class="flex flex-col items-center justify-center">
84
+ <div class="text-muted-foreground">Report not found</div>
85
+ <div class="text-xs text-muted-foreground">The report you're looking for doesn't exist or couldn't be loaded</div>
86
+ </div>
87
+ </div>
88
+ {:else if charts.length === 0}
89
+ <div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
90
+ <Icons.CircleSlash2 class="opacity-50" size="50" />
91
+ <div class="flex flex-col items-center justify-center">
92
+ <div>No charts available</div>
93
+ <div class="text-xs">Create a new chart to fill this report page</div>
94
+ </div>
95
+ </div>
96
+ {:else}
97
+ {#key charts}
98
+ <GridStackComponent {editable} {allowEdit} {onLayoutChange}>
99
+ {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
100
+ <div
101
+ class="grid-stack-item"
102
+ data-gs-id={chart.id}
103
+ {...getChartGridSize(chart)}
104
+ >
105
+ <div class="grid-stack-item-content">
106
+ <Chart
107
+ chartRecord={chart}
108
+ showHeader={showChartHeaders}
109
+ onChartDeleted={async () => await loadCharts()}
110
+ onChartEdited={async () => await loadCharts()}
111
+ {onChartClick}
112
+ {utils}
113
+ />
114
+ </div>
115
+ </div>
116
+ {/each}
117
+ </GridStackComponent>
118
+ {/key}
119
+ {/if}
@@ -0,0 +1,73 @@
1
+ import type { Lobb, Workflow } from "@lobb-js/core";
2
+ import type { Context } from "hono";
3
+
4
+ export const workflows: Workflow[] = [
5
+ {
6
+ // Intercept GET /api/collections/reports_charts/:id when ?action=run_query
7
+ // is present — runs the chart's parsed function and short-circuits the
8
+ // default findOne behaviour by throwing a Response.
9
+ name: "reports_chartsRunQueryHandler",
10
+ eventName: "core.controllers.preFindOne",
11
+ handler: async (input, ctx) => {
12
+ if (input.collectionName !== "reports_charts") return input;
13
+
14
+ const c = input.context as Context;
15
+ const actionQuery = c.req.query("action");
16
+ if (!actionQuery) return input;
17
+
18
+ if (actionQuery !== "run_query") {
19
+ throw new ctx.LobbError({
20
+ code: "BAD_REQUEST",
21
+ message: `The passed (${actionQuery}) action query param is not supported`,
22
+ });
23
+ }
24
+
25
+ const lobb = c.get("lobb") as Lobb;
26
+ const reportRecordId = input.id;
27
+ const contextParam = c.req.query("context");
28
+ const context = JSON.parse(contextParam ?? "{}");
29
+
30
+ const driver = lobb.databaseService.getDriver();
31
+ const pool = driver.getConnection();
32
+ using client = await pool.connect();
33
+
34
+ const chartRecord = (await lobb.collectionService.findOne({
35
+ collectionName: "reports_charts",
36
+ client,
37
+ id: reportRecordId,
38
+ })).data;
39
+
40
+ if (!chartRecord) {
41
+ throw new ctx.LobbError({
42
+ code: "NOT_FOUND",
43
+ message: `The chart you are looking for does not exist.`,
44
+ });
45
+ }
46
+
47
+ const pendingQueries: Promise<unknown>[] = [];
48
+
49
+ async function query(sql: string, args: Record<string, any>) {
50
+ const promise = client.query(sql, args).then((result) => result.rows);
51
+ pendingQueries.push(promise);
52
+ return await promise;
53
+ }
54
+
55
+ try {
56
+ const chartReturnedValue = await lobb.utils.runParsedFunction(
57
+ chartRecord.chart,
58
+ query,
59
+ context,
60
+ );
61
+ await Promise.allSettled(pendingQueries);
62
+ throw new Response(JSON.stringify(chartReturnedValue), {
63
+ status: 200,
64
+ headers: { "Content-Type": "application/json" },
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof Response) throw error;
68
+ await Promise.allSettled(pendingQueries);
69
+ throw error;
70
+ }
71
+ },
72
+ },
73
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/lobb-ext-reports",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "package": "svelte-package --input extensions/reports/studio"
33
33
  },
34
34
  "dependencies": {
35
- "@lobb-js/core": "^0.31.9",
35
+ "@lobb-js/core": "^0.33.0",
36
36
  "chart.js": "^4.4.8",
37
37
  "gridstack": "^12.6.0",
38
38
  "hono": "^4.7.0",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@faker-js/faker": "^9.6.0",
46
- "@lobb-js/studio": "^0.29.0",
46
+ "@lobb-js/studio": "^0.31.0",
47
47
  "@lucide/svelte": "^0.563.1",
48
48
  "@sveltejs/adapter-node": "^5.5.4",
49
49
  "@sveltejs/kit": "^2.60.1",
@@ -1,10 +0,0 @@
1
- import type { Hono } from "hono";
2
-
3
- import { ChartsController } from "./controllers/chartsController.ts";
4
-
5
- export function collectionRoutes(route: Hono) {
6
- route.get(
7
- "/reports_charts/:id",
8
- ChartsController.findOne,
9
- );
10
- }