Skip to content

Commit a45447c

Browse files
committed
Integrate settings service and plugin wiring
Add the Settings service as a capability and wire it into the CLI and runtime: include @objectstack/service-settings in package.json, add the settings capability to CAPABILITY_PROVIDERS, and register the plugin in the serve command. Update the example app config and built artifact to require the settings capability. Enhance the service-settings package: export builtin manifests, register built-in action handlers (mail.test) by default, allow passing custom manifests and action handlers, and add bindEngine to late-bind the real data engine and audit sink instead of re-instantiating the service at start. Also tidy mail manifest typing/shape and propagate action handler registration on init. Update pnpm lockfile to reflect the new workspace dependency and resolved package changes.
1 parent ff6f22f commit a45447c

10 files changed

Lines changed: 92 additions & 29 deletions

File tree

examples/app-crm/dist/objectstack.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25942,6 +25942,7 @@
2594225942
"ui",
2594325943
"storage",
2594425944
"approvals",
25945-
"sharing"
25945+
"sharing",
25946+
"settings"
2594625947
]
2594725948
}

examples/app-crm/objectstack.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default defineStack({
4848
// `ui` serves the Studio shell and CRM apps under /_studio/.
4949
// Both are required for a clickable login flow when running `objectstack start`
5050
// off the compiled artifact.
51-
requires: ['ai', 'automation', 'analytics', 'auth', 'ui', 'storage', 'approvals', 'sharing'],
51+
requires: ['ai', 'automation', 'analytics', 'auth', 'ui', 'storage', 'approvals', 'sharing', 'settings'],
5252

5353
objects: Object.values(objects),
5454
actions: Object.values(actions),

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@objectstack/service-package": "workspace:*",
6969
"@objectstack/service-queue": "workspace:*",
7070
"@objectstack/service-realtime": "workspace:*",
71+
"@objectstack/service-settings": "workspace:*",
7172
"@objectstack/service-storage": "workspace:*",
7273
"@objectstack/spec": "workspace:*",
7374
"@oclif/core": "^4.11.3",

packages/cli/src/commands/serve.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,11 @@ export default class Serve extends Command {
987987
export: 'ApprovalsServicePlugin',
988988
nameMatch: ['plugin-approvals', 'ApprovalsServicePlugin'],
989989
},
990+
settings: {
991+
pkg: '@objectstack/service-settings',
992+
export: 'SettingsServicePlugin',
993+
nameMatch: ['service-settings', 'SettingsServicePlugin'],
994+
},
990995
};
991996

992997
const hasPluginMatching = (fragments: string[]) =>

packages/runtime/src/cloud/capability-loader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export const CAPABILITY_PROVIDERS: Record<string, CapabilitySpec> = {
9393
pkg: '@objectstack/service-feed',
9494
export: 'FeedServicePlugin',
9595
},
96+
settings: {
97+
pkg: '@objectstack/service-settings',
98+
export: 'SettingsServicePlugin',
99+
},
96100
};
97101

98102
type Logger = { info?: (...a: any[]) => void; warn?: (...a: any[]) => void; error?: (...a: any[]) => void };

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ export {
3737
SETTINGS_PLUGIN_VERSION,
3838
} from './manifest.js';
3939

40+
// Reference manifests (mail / branding / feature flags) and the
41+
// convenience aggregate. Hosts can pass `builtinSettingsManifests`
42+
// directly to `new SettingsServicePlugin({ manifests })`.
43+
export {
44+
builtinSettingsManifests,
45+
brandingSettingsManifest,
46+
featureFlagsSettingsManifest,
47+
mailSettingsManifest,
48+
mailTestActionHandler,
49+
} from './manifests/index.js';
50+
4051
// Re-export the spec types for convenience so plugin authors only need
4152
// one import.
4253
export type {

packages/services/service-settings/src/manifests/mail.manifest.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { SettingsManifest, SettingsActionHandler } from '@objectstack/spec/system';
3+
import type { SettingsManifest } from '@objectstack/spec/system';
4+
import type { SettingsActionHandler } from '../settings-service.types.js';
45

5-
/** Mail Delivery — SMTP / API provider configuration. */
6-
export const mailSettingsManifest: SettingsManifest = {
6+
// Visibility expressions are written as inline strings here for
7+
// readability. The spec's ExpressionInputSchema accepts a bare string
8+
// and normalises it at parse time, but the inferred TypeScript output
9+
// type expects `{ dialect, source }` objects. Build the manifest as
10+
// `unknown` first, then cast — keeps the manifest source compact.
11+
const manifest = {
712
namespace: 'mail',
813
version: 1,
914
label: 'Mail Delivery',
@@ -53,6 +58,9 @@ export const mailSettingsManifest: SettingsManifest = {
5358
],
5459
};
5560

61+
/** Mail Delivery — SMTP / API provider configuration. */
62+
export const mailSettingsManifest = manifest as unknown as SettingsManifest;
63+
5664
/** Built-in action handler stub for `mail/test`. */
5765
export const mailTestActionHandler: SettingsActionHandler = async ({ values }) => {
5866
const provider = String(values.provider ?? 'smtp');

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

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,19 @@ import {
1313
SETTINGS_PLUGIN_ID,
1414
SETTINGS_PLUGIN_VERSION,
1515
} from './manifest.js';
16+
import {
17+
builtinSettingsManifests,
18+
mailTestActionHandler,
19+
} from './manifests/index.js';
1620

1721
/** Configuration options for the SettingsServicePlugin. */
1822
export interface SettingsServicePluginOptions {
19-
/** Pre-register these manifests at boot. */
23+
/**
24+
* Pre-register these manifests at boot. When omitted, the bundled
25+
* builtin manifests (mail / branding / feature_flags) are loaded so
26+
* a host gets a working Settings hub out of the box. Pass an empty
27+
* array to opt out entirely.
28+
*/
2029
manifests?: SettingsManifest[];
2130
/** Override the default crypto adapter. */
2231
crypto?: CryptoAdapter;
@@ -26,6 +35,12 @@ export interface SettingsServicePluginOptions {
2635
registerRoutes?: boolean;
2736
/** Override the env source. Defaults to `process.env`. */
2837
env?: Record<string, string | undefined>;
38+
/**
39+
* Action handlers to register at boot, keyed by namespace and action
40+
* id. The bundled `mail.test` handler is registered automatically
41+
* unless this object is provided.
42+
*/
43+
actionHandlers?: Record<string, Record<string, import('./settings-service.types.js').SettingsActionHandler>>;
2944
}
3045

3146
/**
@@ -46,7 +61,13 @@ export class SettingsServicePlugin implements Plugin {
4661
private service: SettingsService | null = null;
4762

4863
constructor(opts: SettingsServicePluginOptions = {}) {
49-
this.opts = opts;
64+
this.opts = {
65+
...opts,
66+
manifests: opts.manifests ?? builtinSettingsManifests,
67+
actionHandlers: opts.actionHandlers ?? {
68+
mail: { test: mailTestActionHandler },
69+
},
70+
};
5071
}
5172

5273
async init(ctx: PluginContext): Promise<void> {
@@ -55,6 +76,11 @@ export class SettingsServicePlugin implements Plugin {
5576
env: this.opts.env,
5677
});
5778
for (const m of this.opts.manifests ?? []) this.service.registerManifest(m);
79+
for (const [ns, handlers] of Object.entries(this.opts.actionHandlers ?? {})) {
80+
for (const [id, fn] of Object.entries(handlers)) {
81+
this.service.registerAction(ns, id, fn);
82+
}
83+
}
5884

5985
ctx.registerService('settings', this.service);
6086
ctx.logger?.info?.(
@@ -84,21 +110,14 @@ export class SettingsServicePlugin implements Plugin {
84110
// ok — fall back to in-memory.
85111
}
86112
if (engine) {
87-
// Stash the engine on the service via a thin re-init. The
88-
// SettingsService keeps the engine ref private, so we
89-
// construct a fresh instance preserving registered manifests +
90-
// crypto + env.
91-
const fresh = new SettingsService({
92-
engine: engine as unknown as SettingsEngine,
93-
crypto: this.opts.crypto,
94-
env: this.opts.env,
95-
audit: this.buildAuditSink(ctx, engine),
96-
});
97-
for (const m of this.opts.manifests ?? []) fresh.registerManifest(m);
98-
// Re-register any manifests that were added between init and start.
99-
for (const m of this.service!.listManifests()) fresh.registerManifest(m);
100-
this.service = fresh;
101-
ctx.registerService('settings', fresh);
113+
// Late-bind the engine + audit sink on the existing service
114+
// instance. We avoid re-registering the service because the
115+
// kernel disallows `registerService` for an already-registered
116+
// name.
117+
this.service!.bindEngine(
118+
engine as unknown as SettingsEngine,
119+
this.buildAuditSink(ctx, engine),
120+
);
102121
}
103122

104123
if (this.opts.registerRoutes === false) return;

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ interface RegisteredManifest {
5656
* the supporting types and `README.md` for the high-level contract.
5757
*/
5858
export class SettingsService {
59-
private readonly engine?: SettingsEngine;
59+
private engine?: SettingsEngine;
6060
private readonly crypto: CryptoAdapter;
61-
private readonly audit?: SettingsAuditSink;
61+
private audit?: SettingsAuditSink;
6262
private readonly env: Record<string, string | undefined>;
6363
private readonly objectName: string;
6464
private readonly registry = new Map<string, RegisteredManifest>();
@@ -73,6 +73,17 @@ export class SettingsService {
7373
this.objectName = opts.objectName ?? DEFAULT_OBJECT;
7474
}
7575

76+
/**
77+
* Late-bind a data engine and (optionally) an audit sink. Plugins
78+
* call this from `kernel:ready` once `objectql` is wired so the
79+
* SettingsService swaps from its in-memory fallback to the real
80+
* `sys_setting` table without re-registering the service.
81+
*/
82+
bindEngine(engine: SettingsEngine, audit?: SettingsAuditSink): void {
83+
this.engine = engine;
84+
if (audit) this.audit = audit;
85+
}
86+
7687
// ---------------------------------------------------------------------
7788
// Manifest registry
7889
// ---------------------------------------------------------------------

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)