Skip to content

Commit 69dd484

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/paginated-bigquery-schema
# Conflicts: # .github/workflows/release.yml # README.md # src/__tests__/mcpServer.test.ts # src/index.ts
2 parents c883a24 + ba11000 commit 69dd484

4 files changed

Lines changed: 127 additions & 7 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131

3232
- uses: actions/setup-node@v6
3333
with:
34-
node-version: '20'
34+
node-version: '24'
3535
registry-url: 'https://registry.npmjs.org'
3636
cache: 'pnpm'
3737

@@ -134,6 +134,10 @@ jobs:
134134
echo "${package_spec} is already published from ${RELEASE_COMMIT}; skipping npm publish."
135135
echo "published=true" >> "${GITHUB_OUTPUT}"
136136
137+
- name: Publish to npm
138+
if: github.repository == 'suthio/redash-mcp' && steps.npm-status.outputs.published != 'true'
139+
run: npm publish --access public
140+
137141
- name: Set up QEMU
138142
uses: docker/setup-qemu-action@v4
139143

@@ -173,7 +177,3 @@ jobs:
173177

174178
- name: Sign Docker image
175179
run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build-and-push.outputs.digest }}
176-
177-
- name: Publish to npm
178-
if: github.repository == 'suthio/redash-mcp' && steps.npm-status.outputs.published != 'true'
179-
run: npm publish --access public
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { scheduleSchema } from '../schedule.js';
2+
3+
describe('scheduleSchema', () => {
4+
it('should default day_of_week, time, and until to null when omitted', () => {
5+
const result = scheduleSchema.parse({
6+
interval: 86400,
7+
});
8+
expect(result).toEqual({
9+
interval: 86400,
10+
time: null,
11+
until: null,
12+
day_of_week: null,
13+
});
14+
});
15+
16+
it('should preserve day_of_week when explicitly provided', () => {
17+
const result = scheduleSchema.parse({
18+
interval: 604800,
19+
time: '06:00',
20+
day_of_week: 'Monday',
21+
});
22+
expect(result).toMatchObject({
23+
interval: 604800,
24+
day_of_week: 'Monday',
25+
});
26+
});
27+
28+
it('should accept all valid day_of_week values', () => {
29+
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
30+
for (const day of days) {
31+
const result = scheduleSchema.parse({ interval: 604800, day_of_week: day });
32+
expect(result).toMatchObject({ day_of_week: day });
33+
}
34+
});
35+
36+
it('should reject invalid day_of_week string', () => {
37+
expect(() => scheduleSchema.parse({
38+
interval: 604800,
39+
day_of_week: 'monday',
40+
})).toThrow();
41+
});
42+
43+
it('should preserve day_of_week as null when explicitly set to null', () => {
44+
const result = scheduleSchema.parse({
45+
interval: 86400,
46+
day_of_week: null,
47+
});
48+
expect(result).toMatchObject({
49+
interval: 86400,
50+
day_of_week: null,
51+
});
52+
});
53+
54+
it('should preserve time and until when provided', () => {
55+
const result = scheduleSchema.parse({
56+
interval: 86400,
57+
time: '01:15',
58+
until: '2026-08-14',
59+
});
60+
expect(result).toEqual({
61+
interval: 86400,
62+
time: '01:15',
63+
until: '2026-08-14',
64+
day_of_week: null,
65+
});
66+
});
67+
68+
it('should return null for null input', () => {
69+
const result = scheduleSchema.parse(null);
70+
expect(result).toBeNull();
71+
});
72+
73+
it('should return undefined for undefined input', () => {
74+
const result = scheduleSchema.parse(undefined);
75+
expect(result).toBeUndefined();
76+
});
77+
78+
it('should accept disabled field', () => {
79+
const result = scheduleSchema.parse({
80+
interval: 86400,
81+
disabled: true,
82+
});
83+
expect(result).toMatchObject({
84+
interval: 86400,
85+
day_of_week: null,
86+
disabled: true,
87+
});
88+
});
89+
90+
it('should reject invalid interval type', () => {
91+
expect(() => scheduleSchema.parse({
92+
interval: 'daily',
93+
})).toThrow();
94+
});
95+
96+
it('should require interval', () => {
97+
expect(() => scheduleSchema.parse({
98+
time: '01:15',
99+
})).toThrow();
100+
});
101+
});

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { buildParameterizedExecutionParameters, ParameterizedExecutionError } fr
2222
import { mergeDeep } from "./utils.js";
2323
import { buildWidgetLayoutOptions, dashboardGridDefaults, summarizeWidgetLayout, widgetLayoutEntrySchema, widgetPositionSchema } from "./widgetLayout.js";
2424
import { logger } from "./logger.js";
25+
import { scheduleSchema } from './schedule.js';
2526
import { TelemetryMcpServer, type McpTelemetryOptions } from "./mcpTelemetry.js";
2627
import { PACKAGE_VERSION } from "./packageInfo.js";
2728

@@ -125,7 +126,7 @@ const createQuerySchema = z.object({
125126
query: z.string().describe("SQL query text"),
126127
description: z.string().optional().describe("Description of the query"),
127128
options: z.record(z.string(), z.any()).optional().describe("Query options"),
128-
schedule: z.record(z.string(), z.any()).optional().describe("Query schedule"),
129+
schedule: scheduleSchema.describe("Query schedule"),
129130
tags: z.array(z.string()).optional().describe("Tags for the query")
130131
});
131132

@@ -174,7 +175,7 @@ const updateQuerySchema = z.object({
174175
query: z.string().optional().describe("SQL query text"),
175176
description: z.string().optional().describe("Description of the query"),
176177
options: z.record(z.string(), z.any()).optional().describe("Query options"),
177-
schedule: z.record(z.string(), z.any()).optional().describe("Query schedule"),
178+
schedule: scheduleSchema.describe("Query schedule"),
178179
tags: z.array(z.string()).optional().describe("Tags for the query"),
179180
is_archived: z.boolean().optional().describe("Whether the query is archived"),
180181
is_draft: z.boolean().optional().describe("Whether the query is a draft")

src/schedule.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { z } from 'zod';
2+
3+
const dayOfWeekEnum = z.enum([
4+
'Sunday', 'Monday', 'Tuesday', 'Wednesday',
5+
'Thursday', 'Friday', 'Saturday',
6+
]);
7+
8+
// Schedule schema with day_of_week, time, and until defaulting to null.
9+
// Redash's scheduler indexes these keys directly, so they must always
10+
// be present in the stored JSON (even as null).
11+
// interval is in seconds (e.g., 86400 = daily, 604800 = weekly)
12+
export const scheduleSchema = z.object({
13+
interval: z.number(),
14+
time: z.string().nullable().default(null),
15+
until: z.string().nullable().default(null),
16+
day_of_week: dayOfWeekEnum.nullable().default(null),
17+
disabled: z.boolean().optional(),
18+
}).optional().nullable();

0 commit comments

Comments
 (0)