Skip to content

Commit b507e7b

Browse files
committed
fix: review
1 parent 3f22671 commit b507e7b

File tree

6 files changed

+143
-18
lines changed

6 files changed

+143
-18
lines changed

apps/server/src/routes/rpc/services/status-page/__tests__/status-page.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ afterAll(async () => {
267267
await db
268268
.delete(page)
269269
.where(eq(page.slug, `${TEST_PREFIX}-locale-create-slug`));
270+
await db
271+
.delete(page)
272+
.where(eq(page.slug, `${TEST_PREFIX}-locale-default-slug`));
270273

271274
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
272275
});
@@ -552,7 +555,7 @@ describe("StatusPageService.UpdateStatusPage", () => {
552555
{
553556
id: String(testPageToUpdateId),
554557
defaultLocale: "LOCALE_FR",
555-
locales: ["LOCALE_EN", "LOCALE_FR", "LOCALE_DE"],
558+
locales: { locales: ["LOCALE_EN", "LOCALE_FR", "LOCALE_DE"] },
556559
},
557560
{ "x-openstatus-key": "1" },
558561
);
@@ -573,6 +576,70 @@ describe("StatusPageService.UpdateStatusPage", () => {
573576
.set({ defaultLocale: "en", locales: null })
574577
.where(eq(page.id, testPageToUpdateId));
575578
});
579+
580+
test("clears locale settings when empty LocaleList is sent", async () => {
581+
// First set some locales
582+
await db
583+
.update(page)
584+
.set({ defaultLocale: "en", locales: ["en", "fr"] })
585+
.where(eq(page.id, testPageToUpdateId));
586+
587+
// Send empty LocaleList to clear
588+
const res = await connectRequest(
589+
"UpdateStatusPage",
590+
{
591+
id: String(testPageToUpdateId),
592+
locales: { locales: [] },
593+
},
594+
{ "x-openstatus-key": "1" },
595+
);
596+
597+
expect(res.status).toBe(200);
598+
599+
const data = await res.json();
600+
// Locales should be cleared (empty array or omitted)
601+
expect(data.statusPage.locales ?? []).toEqual([]);
602+
603+
// Restore defaults
604+
await db
605+
.update(page)
606+
.set({ defaultLocale: "en", locales: null })
607+
.where(eq(page.id, testPageToUpdateId));
608+
});
609+
610+
test("does not change locales when field is omitted", async () => {
611+
// Set some locales
612+
await db
613+
.update(page)
614+
.set({ defaultLocale: "en", locales: ["en", "fr"] })
615+
.where(eq(page.id, testPageToUpdateId));
616+
617+
// Update only the title, omit locales entirely
618+
const res = await connectRequest(
619+
"UpdateStatusPage",
620+
{
621+
id: String(testPageToUpdateId),
622+
title: `${TEST_PREFIX}-no-locale-change`,
623+
},
624+
{ "x-openstatus-key": "1" },
625+
);
626+
627+
expect(res.status).toBe(200);
628+
629+
const data = await res.json();
630+
// Locales should remain unchanged
631+
expect(data.statusPage.locales).toEqual(["LOCALE_EN", "LOCALE_FR"]);
632+
633+
// Restore defaults
634+
await db
635+
.update(page)
636+
.set({
637+
title: `${TEST_PREFIX}-page-to-update`,
638+
defaultLocale: "en",
639+
locales: null,
640+
})
641+
.where(eq(page.id, testPageToUpdateId));
642+
});
576643
});
577644

578645
// ==========================================================================
@@ -607,7 +674,7 @@ describe("StatusPageService locale fields", () => {
607674
"CreateStatusPage",
608675
{
609676
title: `${TEST_PREFIX}-locale-default`,
610-
slug: `${TEST_PREFIX}-locale-create-slug`,
677+
slug: `${TEST_PREFIX}-locale-default-slug`,
611678
},
612679
{ "x-openstatus-key": "1" },
613680
);

apps/server/src/routes/rpc/services/status-page/index.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ServiceImpl } from "@connectrpc/connect";
1+
import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect";
22
import {
33
and,
44
count,
@@ -158,6 +158,23 @@ export const statusPageServiceImpl: ServiceImpl<typeof StatusPageService> = {
158158
throw slugAlreadyExistsError(req.slug);
159159
}
160160

161+
// Resolve locale values
162+
const defaultLocale =
163+
req.defaultLocale !== undefined && req.defaultLocale !== 0
164+
? protoLocaleToDb(req.defaultLocale)
165+
: "en";
166+
const validLocales = req.locales.filter((l) => l !== 0);
167+
const locales =
168+
validLocales.length > 0 ? validLocales.map(protoLocaleToDb) : null;
169+
170+
// Validate defaultLocale is included in locales when locales are provided
171+
if (locales && !locales.includes(defaultLocale)) {
172+
throw new ConnectError(
173+
"Default locale must be included in the locales list",
174+
Code.InvalidArgument,
175+
);
176+
}
177+
161178
// Create the status page
162179
const newPage = await db
163180
.insert(page)
@@ -170,11 +187,8 @@ export const statusPageServiceImpl: ServiceImpl<typeof StatusPageService> = {
170187
published: false,
171188
homepageUrl: req.homepageUrl ?? null,
172189
contactUrl: req.contactUrl ?? null,
173-
defaultLocale: req.defaultLocale
174-
? protoLocaleToDb(req.defaultLocale)
175-
: "en",
176-
locales:
177-
req.locales.length > 0 ? req.locales.map(protoLocaleToDb) : null,
190+
defaultLocale,
191+
locales,
178192
})
179193
.returning()
180194
.get();
@@ -284,8 +298,24 @@ export const statusPageServiceImpl: ServiceImpl<typeof StatusPageService> = {
284298
if (req.defaultLocale !== undefined) {
285299
updateValues.defaultLocale = protoLocaleToDb(req.defaultLocale);
286300
}
287-
if (req.locales.length > 0) {
288-
updateValues.locales = req.locales.map(protoLocaleToDb);
301+
if (req.locales !== undefined) {
302+
const validLocales = req.locales.locales.filter((l) => l !== 0);
303+
updateValues.locales =
304+
validLocales.length > 0 ? validLocales.map(protoLocaleToDb) : null;
305+
}
306+
307+
// Validate defaultLocale is included in locales
308+
const finalDefaultLocale =
309+
(updateValues.defaultLocale as string) ?? pageData.defaultLocale;
310+
const finalLocales =
311+
"locales" in updateValues
312+
? (updateValues.locales as string[] | null)
313+
: (pageData.locales as string[] | null);
314+
if (finalLocales && !finalLocales.includes(finalDefaultLocale)) {
315+
throw new ConnectError(
316+
"Default locale must be included in the locales list",
317+
Code.InvalidArgument,
318+
);
289319
}
290320

291321
const updatedPage = await db

packages/proto/api/openstatus/status_page/v1/service.proto

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ message UpdateStatusPageRequest {
196196
optional Locale default_locale = 7;
197197

198198
// New enabled locales for the status page (optional).
199-
repeated Locale locales = 8;
199+
// When absent: don't change. When present but empty: clear locales. When present with values: set locales.
200+
optional LocaleList locales = 8;
200201
}
201202

202203
// UpdateStatusPageResponse is the response after updating a status page.

packages/proto/api/openstatus/status_page/v1/status_page.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ enum Locale {
3030
LOCALE_DE = 3;
3131
}
3232

33+
// LocaleList is a wrapper message for a list of locales.
34+
// Used with `optional` to distinguish "not set" from "set to empty".
35+
message LocaleList {
36+
repeated Locale locales = 1;
37+
}
38+
3339
// OverallStatus represents the aggregated status of all components on a page.
3440
enum OverallStatus {
3541
OVERALL_STATUS_UNSPECIFIED = 0;

packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts

Lines changed: 5 additions & 4 deletions
Large diffs are not rendered by default.

packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,27 @@ import type { Message } from "@bufbuild/protobuf";
1111
* Describes the file openstatus/status_page/v1/status_page.proto.
1212
*/
1313
export const file_openstatus_status_page_v1_status_page: GenFile = /*@__PURE__*/
14-
fileDesc("CitvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3N0YXR1c19wYWdlLnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIqAECgpTdGF0dXNQYWdlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEh4KBHNsdWcYBCABKAlCELpHDToLEglhY21lLWNvcnASMAoNY3VzdG9tX2RvbWFpbhgFIAEoCUIZukcWOhQSEnN0YXR1cy5leGFtcGxlLmNvbRIRCglwdWJsaXNoZWQYBiABKAgSPgoLYWNjZXNzX3R5cGUYByABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlEjMKBXRoZW1lGAggASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWUSFAoMaG9tZXBhZ2VfdXJsGAkgASgJEhMKC2NvbnRhY3RfdXJsGAogASgJEgwKBGljb24YCyABKAkSLwoKY3JlYXRlZF9hdBgMIAEoCUIbukcYOhYSFDIwMjQtMDEtMTVUMDk6MDA6MDBaEi8KCnVwZGF0ZWRfYXQYDSABKAlCG7pHGDoWEhQyMDI0LTA2LTIwVDE0OjMwOjAwWhI5Cg5kZWZhdWx0X2xvY2FsZRgOIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEjIKB2xvY2FsZXMYDyADKA4yIS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkxvY2FsZSJ3ChFTdGF0dXNQYWdlU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgRzbHVnGAMgASgJEhEKCXB1Ymxpc2hlZBgEIAEoCBISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkqnAEKDlBhZ2VBY2Nlc3NUeXBlEiAKHFBBR0VfQUNDRVNTX1RZUEVfVU5TUEVDSUZJRUQQABIbChdQQUdFX0FDQ0VTU19UWVBFX1BVQkxJQxABEicKI1BBR0VfQUNDRVNTX1RZUEVfUEFTU1dPUkRfUFJPVEVDVEVEEAISIgoeUEFHRV9BQ0NFU1NfVFlQRV9BVVRIRU5USUNBVEVEEAMqaQoJUGFnZVRoZW1lEhoKFlBBR0VfVEhFTUVfVU5TUEVDSUZJRUQQABIVChFQQUdFX1RIRU1FX1NZU1RFTRABEhQKEFBBR0VfVEhFTUVfTElHSFQQAhITCg9QQUdFX1RIRU1FX0RBUksQAypNCgZMb2NhbGUSFgoSTE9DQUxFX1VOU1BFQ0lGSUVEEAASDQoJTE9DQUxFX0VOEAESDQoJTE9DQUxFX0ZSEAISDQoJTE9DQUxFX0RFEAMq7AEKDU92ZXJhbGxTdGF0dXMSHgoaT1ZFUkFMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpPVkVSQUxMX1NUQVRVU19PUEVSQVRJT05BTBABEhsKF09WRVJBTExfU1RBVFVTX0RFR1JBREVEEAISIQodT1ZFUkFMTF9TVEFUVVNfUEFSVElBTF9PVVRBR0UQAxIfChtPVkVSQUxMX1NUQVRVU19NQUpPUl9PVVRBR0UQBBIeChpPVkVSQUxMX1NUQVRVU19NQUlOVEVOQU5DRRAFEhoKFk9WRVJBTExfU1RBVFVTX1VOS05PV04QBkJaWlhnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvc3RhdHVzX3BhZ2UvdjE7c3RhdHVzcGFnZXYxYgZwcm90bzM", [file_gnostic_openapi_v3_annotations]);
14+
fileDesc("CitvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3N0YXR1c19wYWdlLnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIkAKCkxvY2FsZUxpc3QSMgoHbG9jYWxlcxgBIAMoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlIqAECgpTdGF0dXNQYWdlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEh4KBHNsdWcYBCABKAlCELpHDToLEglhY21lLWNvcnASMAoNY3VzdG9tX2RvbWFpbhgFIAEoCUIZukcWOhQSEnN0YXR1cy5leGFtcGxlLmNvbRIRCglwdWJsaXNoZWQYBiABKAgSPgoLYWNjZXNzX3R5cGUYByABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlEjMKBXRoZW1lGAggASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWUSFAoMaG9tZXBhZ2VfdXJsGAkgASgJEhMKC2NvbnRhY3RfdXJsGAogASgJEgwKBGljb24YCyABKAkSLwoKY3JlYXRlZF9hdBgMIAEoCUIbukcYOhYSFDIwMjQtMDEtMTVUMDk6MDA6MDBaEi8KCnVwZGF0ZWRfYXQYDSABKAlCG7pHGDoWEhQyMDI0LTA2LTIwVDE0OjMwOjAwWhI5Cg5kZWZhdWx0X2xvY2FsZRgOIAEoDjIhLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuTG9jYWxlEjIKB2xvY2FsZXMYDyADKA4yIS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkxvY2FsZSJ3ChFTdGF0dXNQYWdlU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgRzbHVnGAMgASgJEhEKCXB1Ymxpc2hlZBgEIAEoCBISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkqnAEKDlBhZ2VBY2Nlc3NUeXBlEiAKHFBBR0VfQUNDRVNTX1RZUEVfVU5TUEVDSUZJRUQQABIbChdQQUdFX0FDQ0VTU19UWVBFX1BVQkxJQxABEicKI1BBR0VfQUNDRVNTX1RZUEVfUEFTU1dPUkRfUFJPVEVDVEVEEAISIgoeUEFHRV9BQ0NFU1NfVFlQRV9BVVRIRU5USUNBVEVEEAMqaQoJUGFnZVRoZW1lEhoKFlBBR0VfVEhFTUVfVU5TUEVDSUZJRUQQABIVChFQQUdFX1RIRU1FX1NZU1RFTRABEhQKEFBBR0VfVEhFTUVfTElHSFQQAhITCg9QQUdFX1RIRU1FX0RBUksQAypNCgZMb2NhbGUSFgoSTE9DQUxFX1VOU1BFQ0lGSUVEEAASDQoJTE9DQUxFX0VOEAESDQoJTE9DQUxFX0ZSEAISDQoJTE9DQUxFX0RFEAMq7AEKDU92ZXJhbGxTdGF0dXMSHgoaT1ZFUkFMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpPVkVSQUxMX1NUQVRVU19PUEVSQVRJT05BTBABEhsKF09WRVJBTExfU1RBVFVTX0RFR1JBREVEEAISIQodT1ZFUkFMTF9TVEFUVVNfUEFSVElBTF9PVVRBR0UQAxIfChtPVkVSQUxMX1NUQVRVU19NQUpPUl9PVVRBR0UQBBIeChpPVkVSQUxMX1NUQVRVU19NQUlOVEVOQU5DRRAFEhoKFk9WRVJBTExfU1RBVFVTX1VOS05PV04QBkJaWlhnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvc3RhdHVzX3BhZ2UvdjE7c3RhdHVzcGFnZXYxYgZwcm90bzM", [file_gnostic_openapi_v3_annotations]);
15+
16+
/**
17+
* LocaleList is a wrapper message for a list of locales.
18+
* Used with `optional` to distinguish "not set" from "set to empty".
19+
*
20+
* @generated from message openstatus.status_page.v1.LocaleList
21+
*/
22+
export type LocaleList = Message<"openstatus.status_page.v1.LocaleList"> & {
23+
/**
24+
* @generated from field: repeated openstatus.status_page.v1.Locale locales = 1;
25+
*/
26+
locales: Locale[];
27+
};
28+
29+
/**
30+
* Describes the message openstatus.status_page.v1.LocaleList.
31+
* Use `create(LocaleListSchema)` to create a new message.
32+
*/
33+
export const LocaleListSchema: GenMessage<LocaleList> = /*@__PURE__*/
34+
messageDesc(file_openstatus_status_page_v1_status_page, 0);
1535

1636
/**
1737
* StatusPage represents a full status page with all details.
@@ -130,7 +150,7 @@ export type StatusPage = Message<"openstatus.status_page.v1.StatusPage"> & {
130150
* Use `create(StatusPageSchema)` to create a new message.
131151
*/
132152
export const StatusPageSchema: GenMessage<StatusPage> = /*@__PURE__*/
133-
messageDesc(file_openstatus_status_page_v1_status_page, 0);
153+
messageDesc(file_openstatus_status_page_v1_status_page, 1);
134154

135155
/**
136156
* StatusPageSummary represents metadata for a status page (used in list responses).
@@ -186,7 +206,7 @@ export type StatusPageSummary = Message<"openstatus.status_page.v1.StatusPageSum
186206
* Use `create(StatusPageSummarySchema)` to create a new message.
187207
*/
188208
export const StatusPageSummarySchema: GenMessage<StatusPageSummary> = /*@__PURE__*/
189-
messageDesc(file_openstatus_status_page_v1_status_page, 1);
209+
messageDesc(file_openstatus_status_page_v1_status_page, 2);
190210

191211
/**
192212
* PageAccessType defines who can access the status page.

0 commit comments

Comments
 (0)