Skip to content

Commit 51e102c

Browse files
committed
feat(settings): add configuration group and update settings routes for better organization
1 parent c18e375 commit 51e102c

5 files changed

Lines changed: 31 additions & 39 deletions

File tree

packages/platform-objects/src/apps/setup.app.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ export const SETUP_APP: App = {
9595
{ id: 'nav_approval_actions', type: 'object', label: 'Action History', objectName: 'sys_approval_action', icon: 'history', requiresObject: 'sys_approval_action' },
9696
],
9797
},
98+
{
99+
id: 'group_configuration',
100+
type: 'group',
101+
label: 'Configuration',
102+
icon: 'sliders-horizontal',
103+
children: [
104+
// Metadata-driven settings hub. Each entry maps to a SettingsManifest
105+
// namespace exposed by @objectstack/service-settings. URL navigation
106+
// is used (not `object`) because settings are stored in a generic
107+
// K/V table (`sys_setting`) rather than per-namespace objects, and
108+
// the renderer is a dedicated <SettingsView> page in objectui.
109+
{ id: 'nav_settings_hub', type: 'url', label: 'All Settings', url: '/apps/setup/system/settings', icon: 'settings-2' },
110+
{ id: 'nav_settings_mail', type: 'url', label: 'Email', url: '/apps/setup/system/settings/mail', icon: 'mail' },
111+
{ id: 'nav_settings_branding', type: 'url', label: 'Branding', url: '/apps/setup/system/settings/branding', icon: 'palette' },
112+
{ id: 'nav_settings_feature_flags', type: 'url', label: 'Feature Flags', url: '/apps/setup/system/settings/feature_flags', icon: 'flag' },
113+
],
114+
},
98115
{
99116
id: 'group_diagnostics',
100117
type: 'group',

packages/services/service-settings/src/settings-routes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, expect, it, vi } from 'vitest';
4-
import type { IHttpServer, IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts/http-server';
4+
import type { IHttpServer, IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts';
55
import { SettingsService } from './settings-service';
66
import { registerSettingsRoutes } from './settings-routes';
77
import { brandingSettingsManifest } from './manifests/branding.manifest';

packages/services/service-settings/src/settings-routes.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* `SettingsService`.
1414
*/
1515

16-
import type { IHttpServer, IHttpRequest, IHttpResponse } from '@objectstack/spec/contracts/http-server';
16+
import type { IHttpServer, IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts';
1717
import { SettingsService } from './settings-service.js';
1818
import {
1919
SettingsLockedError,
@@ -60,17 +60,17 @@ export function registerSettingsRoutes(
6060
const base = opts.basePath ?? '/api/settings';
6161
const ctxOf = opts.contextFromRequest ?? defaultContext;
6262

63-
http.get(base, async (req, res) => {
63+
http.get(base, (async (req, res) => {
6464
try {
6565
const ctx = ctxOf(req);
6666
const manifests = service.listManifests(ctx);
6767
await res.json({ manifests });
6868
} catch (err: any) {
6969
sendError(res, 500, 'INTERNAL', err?.message ?? 'Failed to list manifests');
7070
}
71-
});
71+
}) satisfies RouteHandler);
7272

73-
http.get(`${base}/:namespace`, async (req, res) => {
73+
http.get(`${base}/:namespace`, (async (req, res) => {
7474
const ns = req.params.namespace;
7575
try {
7676
const ctx = ctxOf(req);
@@ -83,9 +83,9 @@ export function registerSettingsRoutes(
8383
sendError(res, 500, 'INTERNAL', err?.message ?? 'Failed to read namespace');
8484
}
8585
}
86-
});
86+
}) satisfies RouteHandler);
8787

88-
http.put(`${base}/:namespace`, async (req, res) => {
88+
http.put(`${base}/:namespace`, (async (req, res) => {
8989
const ns = req.params.namespace;
9090
const body = (req.body ?? {}) as Record<string, unknown>;
9191
try {
@@ -107,9 +107,9 @@ export function registerSettingsRoutes(
107107
sendError(res, 500, 'INTERNAL', err?.message ?? 'Failed to write namespace');
108108
}
109109
}
110-
});
110+
}) satisfies RouteHandler);
111111

112-
http.post(`${base}/:namespace/:actionId`, async (req, res) => {
112+
http.post(`${base}/:namespace/:actionId`, (async (req, res) => {
113113
const { namespace, actionId } = req.params;
114114
try {
115115
const ctx = ctxOf(req);
@@ -123,5 +123,5 @@ export function registerSettingsRoutes(
123123
sendError(res, 500, 'INTERNAL', err?.message ?? 'Action failed');
124124
}
125125
}
126-
});
126+
}) satisfies RouteHandler);
127127
}

packages/services/service-settings/src/settings-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ export class SettingsService {
331331
};
332332
const existing = await this.engine.find(this.objectName, { where, limit: 1 });
333333
if (existing[0]) {
334-
await this.engine.update(this.objectName, { where, data: row });
334+
await this.engine.update(this.objectName, { where, data: { ...row } });
335335
} else {
336-
await this.engine.insert(this.objectName, row);
336+
await this.engine.insert(this.objectName, { ...row });
337337
}
338338
return;
339339
}

packages/services/service-settings/src/settings-service.types.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,8 @@
2323
* plugin wires those pieces up.
2424
*/
2525

26-
import type {
27-
SettingsManifest,
28-
ResolvedSettingValue,
29-
SettingsNamespacePayload,
30-
SettingsActionResult,
31-
SpecifierScope,
32-
} from '@objectstack/spec/system';
33-
import { CryptoAdapter, NoopCryptoAdapter } from './crypto-adapter.js';
26+
import type { SettingsActionResult, SpecifierScope } from '@objectstack/spec/system';
27+
import { type CryptoAdapter } from './crypto-adapter.js';
3428

3529
/** Caller identity used by the resolver and audit log. */
3630
export interface SettingsContext {
@@ -110,8 +104,6 @@ export interface SettingsServiceOptions {
110104
objectName?: string;
111105
}
112106

113-
const DEFAULT_OBJECT = 'sys_setting';
114-
115107
/**
116108
* Convert `(namespace, key)` to the env var convention defined in
117109
* ADR-0007: uppercase, dots → underscores, hyphens → underscores.
@@ -121,23 +113,6 @@ export function envKeyOf(namespace: string, key: string): string {
121113
return slug;
122114
}
123115

124-
/** Cast an env string to the type hinted by the manifest default. */
125-
function coerceEnvValue(raw: string, hint: unknown): unknown {
126-
if (typeof hint === 'boolean') return raw === 'true' || raw === '1' || raw === 'yes';
127-
if (typeof hint === 'number') {
128-
const n = Number(raw);
129-
return Number.isFinite(n) ? n : raw;
130-
}
131-
if (Array.isArray(hint) || (hint && typeof hint === 'object')) {
132-
try {
133-
return JSON.parse(raw);
134-
} catch {
135-
return raw;
136-
}
137-
}
138-
return raw;
139-
}
140-
141116
/** Thrown when a caller tries to write a value pinned by env. */
142117
export class SettingsLockedError extends Error {
143118
readonly code = 'SETTINGS_LOCKED' as const;

0 commit comments

Comments
 (0)