mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
chore(server): remove the MCP config re-export and the dormant row-shape helper
Only their own tests imported them. The MCP knobs are parsed in app-config/parsers.ts, which tests them directly, and toCamelCase was never wired into a plugin RPC handler.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Pure env-string coercion helpers shared by the whole config layer (derive.ts,
|
||||
* env.schema.ts) and — until their call sites migrate — by legacy readers like
|
||||
* src/mcp/config.ts. Kept free of imports so units can test them in isolation.
|
||||
* env.schema.ts). Kept free of imports so units can test them in isolation.
|
||||
*
|
||||
* Each helper reproduces a coercion family that already exists in the codebase.
|
||||
* Parity is law: do NOT "fix" a family's quirks here (e.g. `numberOr` treating
|
||||
@@ -102,7 +101,6 @@ export function parseDurationMs(value: string): number | null {
|
||||
/**
|
||||
* Session idle TTL in SECONDS via MCP_SESSION_TTL, default 1 hour, clamped to
|
||||
* 24h so a milliseconds-value typo can't produce a 1000-hour session.
|
||||
* (Same contract as src/mcp/config.ts, which commit 7 retires in favor of this.)
|
||||
*/
|
||||
export function resolveSessionTtlMs(raw: string | undefined): number {
|
||||
const parsed = Number.parseInt(raw ?? '');
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* MCP tuning knobs from the environment (#1414). The parsers moved into the
|
||||
* app-config layer (src/app-config/parsers.ts) — this module re-exports them
|
||||
* so existing import paths keep working. mcp/index.ts consumes the derived
|
||||
* values via readEnv().mcp.
|
||||
*/
|
||||
export { resolveSessionTtlMs, resolveKeepaliveMs } from '../app-config/parsers';
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* `toCamelCase` — the R-facade utility from Plan 3j's Task 6 (design spec D8, program
|
||||
* `docs/superpowers/plans/2026-09-25-orm-phase3j-plugins.md`). It is DORMANT: built,
|
||||
* tested and exported, but wired into ZERO of the 23 `*.rpc.ts` plugin-host handlers.
|
||||
* `plugin-sdk/src/index.ts`'s published row types (`User`, `Place`, `Day`, `Reservation`,
|
||||
* `PackingItem`, `TripFile`, `BudgetItem`, …) are snake_case today, and neither the SDK
|
||||
* nor its tests carry any camelCase precedent — so applying this additively to a
|
||||
* handler's return value would grow the plugin contract's visible surface with fields no
|
||||
* published type, plugin author or test fixture expects, for no proven benefit. Whether
|
||||
* TREK should start publishing camelCase copies going forward is a real product decision
|
||||
* (see the plan's "For the user" note), not one an ORM-migration task makes unilaterally.
|
||||
* This function exists so that decision, if it is ever made, is a one-line change at
|
||||
* `HostSurfaceRpc#getUser`'s single return statement rather than new plumbing to design
|
||||
* and land. It is a pure function — no class, no DI, no constructor — matching the
|
||||
* `num`/`str`/`asPayload` shape in the sibling `rpc-params.ts` every one of the 23
|
||||
* `*.rpc.ts` files already imports the same way, specifically so it never ripples through
|
||||
* `tests/helpers/plugin-host.ts`/`rpc-host-deps.ts`'s hand-construction of all 23 classes.
|
||||
*
|
||||
* Scope, operate ONLY on a DB-row-shaped object the HOST itself assembled (e.g.
|
||||
* `SELECT id, username, display_name, avatar FROM users …`). NEVER apply it to an opaque
|
||||
* JSON-text value a PLUGIN wrote and the host merely parsed (`MetaRpc#get`/`#list`'s
|
||||
* values, a plugin's own `config`) — a generic key transform would silently rewrite keys
|
||||
* inside that stored blob, which is exactly why a generic deep-key transform was rejected
|
||||
* project-wide (design spec D1/D8).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a shallow, additive camelCase COPY of `value`'s keys: every key containing `_`
|
||||
* gets a camelCase sibling holding the same value (`display_name` -> `displayName`,
|
||||
* `content_base64` -> `contentBase64`); the original snake_case key is never removed or
|
||||
* replaced. A key with no underscore is left exactly as it is — no identity-transform
|
||||
* copy is ever added for it. The conversion is ONE level deep only: a nested object, a
|
||||
* `Date`, a `Buffer` or any other VALUE under a key is passed through completely
|
||||
* unchanged (never recursed into, never spread) — only the row's own TOP-LEVEL key names
|
||||
* are touched. This is the scope choice inventory §13 flagged as an open ambiguity in the
|
||||
* design spec's "shallow" wording; a future caller wiring this in should expect a
|
||||
* `user_profile: { first_name: 'A' }` row to gain `userProfile` pointing at the SAME
|
||||
* nested object, with `first_name` inside it untouched — never a recursively-mapped
|
||||
* `userProfile: { firstName: 'A' }`.
|
||||
*
|
||||
* An array is mapped element-wise, each element run back through `toCamelCase`. `null`,
|
||||
* `undefined`, a `Date`, a `Buffer` and any other non-plain-object value passed as the
|
||||
* top-level argument pass through completely unchanged, defensively — a caller mistake
|
||||
* here (this is never called with anything but a row object in the codebase today, since
|
||||
* it is unwired) must not throw into an RPC dispatch.
|
||||
*/
|
||||
export function toCamelCase<T extends Record<string, unknown>>(value: readonly T[]): (T & Record<string, unknown>)[];
|
||||
export function toCamelCase<T extends Record<string, unknown>>(value: T): T & Record<string, unknown>;
|
||||
export function toCamelCase<V>(value: V): V;
|
||||
export function toCamelCase(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => toCamelCase(item));
|
||||
|
||||
if (
|
||||
value === null ||
|
||||
typeof value !== 'object' ||
|
||||
value instanceof Date ||
|
||||
Buffer.isBuffer(value)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const row = value as Record<string, unknown>;
|
||||
const camelCopies: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(row)) {
|
||||
if (!key.includes('_')) continue;
|
||||
const camelKey = snakeKeyToCamel(key);
|
||||
if (camelKey !== key) camelCopies[camelKey] = row[key];
|
||||
}
|
||||
return { ...row, ...camelCopies };
|
||||
}
|
||||
|
||||
/**
|
||||
* `display_name` -> `displayName`; `content_base64` -> `contentBase64`. A digit right
|
||||
* after an underscore follows the same rule and needs no special case — uppercasing a
|
||||
* digit is a no-op, so `place_2_id` -> `place2Id`.
|
||||
*/
|
||||
function snakeKeyToCamel(key: string): string {
|
||||
return key.replace(/_([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase());
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* MCP env tuning knobs (#1414): MCP_SESSION_TTL + MCP_SSE_KEEPALIVE parsing.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { resolveSessionTtlMs, resolveKeepaliveMs } from '../../../src/mcp/config';
|
||||
|
||||
describe('resolveSessionTtlMs', () => {
|
||||
it('defaults to 1 hour when unset or invalid', () => {
|
||||
expect(resolveSessionTtlMs(undefined)).toBe(60 * 60 * 1000);
|
||||
expect(resolveSessionTtlMs('')).toBe(60 * 60 * 1000);
|
||||
expect(resolveSessionTtlMs('nope')).toBe(60 * 60 * 1000);
|
||||
expect(resolveSessionTtlMs('0')).toBe(60 * 60 * 1000);
|
||||
expect(resolveSessionTtlMs('-5')).toBe(60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it('reads seconds from MCP_SESSION_TTL', () => {
|
||||
expect(resolveSessionTtlMs('3600')).toBe(3600 * 1000);
|
||||
expect(resolveSessionTtlMs('120')).toBe(120 * 1000);
|
||||
});
|
||||
|
||||
it('clamps to 24h so a milliseconds typo cannot yield a 1000-hour session', () => {
|
||||
expect(resolveSessionTtlMs('3600000')).toBe(24 * 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveKeepaliveMs', () => {
|
||||
it('defaults to 25s when unset or invalid', () => {
|
||||
expect(resolveKeepaliveMs(undefined)).toBe(25_000);
|
||||
expect(resolveKeepaliveMs('abc')).toBe(25_000);
|
||||
expect(resolveKeepaliveMs('-1')).toBe(25_000);
|
||||
});
|
||||
|
||||
it('reads seconds and allows 0 to disable', () => {
|
||||
expect(resolveKeepaliveMs('30')).toBe(30_000);
|
||||
expect(resolveKeepaliveMs('0')).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* `toCamelCase` (Plan 3j Task 6, design spec D8) — the DORMANT plugin-RPC facade
|
||||
* utility. This is its entire test surface: it is wired into no `*.rpc.ts` handler, so
|
||||
* no RPC-handler test changes alongside it. See `row-shape.ts`'s own docstring for why
|
||||
* it exists unwired and what it must never be applied to.
|
||||
*/
|
||||
import { describe, it, expect, expectTypeOf } from 'vitest';
|
||||
import { toCamelCase } from '../../../../src/nest/plugins/host/rpc-kit/row-shape';
|
||||
|
||||
describe('toCamelCase', () => {
|
||||
it('ROWSHAPE-001 a flat row with several snake_case keys gets both cases, originals unchanged', () => {
|
||||
const row = { id: 1, display_name: 'Ada', avatar_url: null as string | null };
|
||||
const mapped = toCamelCase(row);
|
||||
expect(mapped).toEqual({
|
||||
id: 1,
|
||||
display_name: 'Ada',
|
||||
avatar_url: null,
|
||||
displayName: 'Ada',
|
||||
avatarUrl: null,
|
||||
});
|
||||
// Originals present and unchanged, not moved.
|
||||
expect(mapped.display_name).toBe('Ada');
|
||||
expect(mapped.avatar_url).toBeNull();
|
||||
});
|
||||
|
||||
it('ROWSHAPE-002 a key with no underscore passes through untouched — no identity-transform copy', () => {
|
||||
const row = { id: 1, username: 'ada' };
|
||||
const mapped = toCamelCase(row);
|
||||
expect(mapped).toEqual({ id: 1, username: 'ada' });
|
||||
expect(Object.keys(mapped).sort()).toEqual(['id', 'username']);
|
||||
});
|
||||
|
||||
it('ROWSHAPE-003 an array of rows maps element-wise', () => {
|
||||
const rows = [
|
||||
{ id: 1, display_name: 'Ada' },
|
||||
{ id: 2, display_name: 'Bea' },
|
||||
];
|
||||
const mapped = toCamelCase(rows);
|
||||
expect(mapped).toEqual([
|
||||
{ id: 1, display_name: 'Ada', displayName: 'Ada' },
|
||||
{ id: 2, display_name: 'Bea', displayName: 'Bea' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ROWSHAPE-004 a nested object stays untouched — proves the shallow, one-level scope', () => {
|
||||
const profile = { first_name: 'Ada', last_name: 'Lovelace' };
|
||||
const row = { id: 1, user_profile: profile };
|
||||
const mapped = toCamelCase(row);
|
||||
// The camel sibling points at the exact SAME object — never a recursively-mapped copy.
|
||||
expect(mapped.userProfile).toBe(profile);
|
||||
expect(mapped.user_profile).toBe(profile);
|
||||
// The nested object itself gained no camelCase siblings.
|
||||
expect(Object.keys(profile).sort()).toEqual(['first_name', 'last_name']);
|
||||
expect((profile as Record<string, unknown>).firstName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ROWSHAPE-005 null, undefined and a primitive pass through safely, unchanged', () => {
|
||||
expect(toCamelCase(null)).toBeNull();
|
||||
expect(toCamelCase(undefined)).toBeUndefined();
|
||||
expect(toCamelCase('a string')).toBe('a string');
|
||||
expect(toCamelCase(42)).toBe(42);
|
||||
expect(toCamelCase(true)).toBe(true);
|
||||
});
|
||||
|
||||
it('ROWSHAPE-006 a Date or Buffer value — at the top level or nested under a key — passes through unchanged', () => {
|
||||
const when = new Date('2026-09-24T00:00:00.000Z');
|
||||
const bytes = Buffer.from('hello');
|
||||
|
||||
// Top-level argument.
|
||||
expect(toCamelCase(when)).toBe(when);
|
||||
expect(toCamelCase(bytes)).toBe(bytes);
|
||||
|
||||
// Nested under a row key — never spread, never recursed into.
|
||||
const row = { id: 1, created_at: when, blob_data: bytes };
|
||||
const mapped = toCamelCase(row);
|
||||
expect(mapped.createdAt).toBe(when);
|
||||
expect(mapped.created_at).toBe(when);
|
||||
expect(mapped.blobData).toBe(bytes);
|
||||
expect(mapped.blob_data).toBe(bytes);
|
||||
});
|
||||
|
||||
it('ROWSHAPE-007 a digit immediately after an underscore follows the same rule (no special case)', () => {
|
||||
const row = { place_2_id: 7 };
|
||||
const mapped = toCamelCase(row);
|
||||
expect(mapped).toEqual({ place_2_id: 7, place2Id: 7 });
|
||||
});
|
||||
|
||||
it('ROWSHAPE-008 an empty object round-trips to an equal empty object', () => {
|
||||
expect(toCamelCase({})).toEqual({});
|
||||
});
|
||||
|
||||
it('ROWSHAPE-009 an empty array round-trips to an empty array', () => {
|
||||
expect(toCamelCase([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('ROWSHAPE-010 type-level: the SDK-shaped User row keeps its snake_case members AND exposes the camel siblings', () => {
|
||||
// Mirrors plugin-sdk/src/index.ts's published `User` shape exactly (id, username,
|
||||
// display_name, avatar, plus its index signature) — the one row HostSurfaceRpc#getUser
|
||||
// (HR1) would wrap if the facade were ever wired in.
|
||||
interface SdkUserRow {
|
||||
id: number;
|
||||
username?: string;
|
||||
display_name?: string | null;
|
||||
avatar?: string | null;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
const row: SdkUserRow = { id: 1, username: 'ada', display_name: 'Ada Lovelace', avatar: null };
|
||||
const mapped = toCamelCase(row);
|
||||
|
||||
// The mapped type is `SdkUserRow & Record<string, unknown>`: every original member
|
||||
// stays typed, and any camelCase sibling (`displayName`, unlisted on `SdkUserRow`
|
||||
// itself) resolves through the added index signature rather than failing to compile —
|
||||
// exactly the shape a plugin-sdk follow-up release's row types would need.
|
||||
expectTypeOf(mapped).toEqualTypeOf<SdkUserRow & Record<string, unknown>>();
|
||||
expectTypeOf(mapped.display_name).toEqualTypeOf<SdkUserRow['display_name']>();
|
||||
expectTypeOf(mapped.displayName).toEqualTypeOf<unknown>();
|
||||
|
||||
expect(mapped.display_name).toBe('Ada Lovelace');
|
||||
expect(mapped.displayName).toBe('Ada Lovelace');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user