Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/miserable-harlequin-dove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/agents-core": patch
---

Derive scope config types and WHERE helpers from single source of truth (scope-definitions.ts)
77 changes: 38 additions & 39 deletions packages/agents-core/src/data-access/manage/scope-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
import { and, eq } from 'drizzle-orm';
import type {
AgentScopeConfig,
ProjectScopeConfig,
SubAgentScopeConfig,
} from '../../types/utility';
import { and, eq, type SQL } from 'drizzle-orm';
import {
SCOPE_KEYS,
type ScopeConfig,
type ScopedTable,
type ScopeLevel,
} from '../../db/manage/scope-definitions';

type TenantScopeConfig = { tenantId: string };

type TenantScopedColumns = { tenantId: any };
type ProjectScopedColumns = TenantScopedColumns & { projectId: any };
type AgentScopedColumns = ProjectScopedColumns & { agentId: any };
type SubAgentScopedColumns = AgentScopedColumns & { subAgentId: any };
/**
* Build a WHERE clause that filters by all scope columns for the given level.
*
* The columns and config shape are both derived from `SCOPE_KEYS` in
* `scope-definitions.ts`, so they cannot drift apart.
*/
export function scopedWhere<L extends ScopeLevel>(
level: L,
table: ScopedTable<L>,
scopes: ScopeConfig<L>
): SQL | undefined {
const keys = SCOPE_KEYS[level];
const conditions = keys.map((key) =>
eq((table as Record<string, any>)[key], (scopes as Record<string, string>)[key])
);
return conditions.length === 1 ? conditions[0] : and(...conditions);
}

export function tenantScopedWhere<T extends TenantScopedColumns>(
// Named wrappers for ergonomics and grep-ability.
export const tenantScopedWhere = <T extends ScopedTable<'tenant'>>(
table: T,
scopes: TenantScopeConfig
) {
return eq(table.tenantId, scopes.tenantId);
}
scopes: ScopeConfig<'tenant'>
) => scopedWhere('tenant', table, scopes);

export function projectScopedWhere<T extends ProjectScopedColumns>(
export const projectScopedWhere = <T extends ScopedTable<'project'>>(
table: T,
scopes: ProjectScopeConfig
) {
return and(eq(table.tenantId, scopes.tenantId), eq(table.projectId, scopes.projectId));
}
scopes: ScopeConfig<'project'>
) => scopedWhere('project', table, scopes);

export function agentScopedWhere<T extends AgentScopedColumns>(table: T, scopes: AgentScopeConfig) {
return and(
eq(table.tenantId, scopes.tenantId),
eq(table.projectId, scopes.projectId),
eq(table.agentId, scopes.agentId)
);
}
export const agentScopedWhere = <T extends ScopedTable<'agent'>>(
table: T,
scopes: ScopeConfig<'agent'>
) => scopedWhere('agent', table, scopes);

export function subAgentScopedWhere<T extends SubAgentScopedColumns>(
export const subAgentScopedWhere = <T extends ScopedTable<'subAgent'>>(
table: T,
scopes: SubAgentScopeConfig
) {
return and(
eq(table.tenantId, scopes.tenantId),
eq(table.projectId, scopes.projectId),
eq(table.agentId, scopes.agentId),
eq(table.subAgentId, scopes.subAgentId)
);
}
scopes: ScopeConfig<'subAgent'>
) => scopedWhere('subAgent', table, scopes);
38 changes: 38 additions & 0 deletions packages/agents-core/src/db/manage/scope-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Single source of truth for hierarchical scope column definitions.
*
* Everything else — TypeScript config types, WHERE clause helpers, and
* compile-time table constraints — is derived from these key arrays.
*
* The schema column helpers (tenantScoped, projectScoped, etc.) in
* manage-schema.ts define the Drizzle columns using the same key names.
* If a key is added or removed here, the schema columns and all
* downstream consumers (types, query helpers) must be updated together —
* but now they'll fail at compile time instead of silently drifting.
*/
export const SCOPE_KEYS = {
tenant: ['tenantId'] as const,
project: ['tenantId', 'projectId'] as const,
agent: ['tenantId', 'projectId', 'agentId'] as const,
subAgent: ['tenantId', 'projectId', 'agentId', 'subAgentId'] as const,
} as const;

export type ScopeLevel = keyof typeof SCOPE_KEYS;

export type ScopeKeysOf<L extends ScopeLevel> = (typeof SCOPE_KEYS)[L][number];

/** A table that has the columns required for a given scope level. */
export type ScopedTable<L extends ScopeLevel> = {
[K in ScopeKeysOf<L>]: any;
};

/** The parameter object shape required to filter by a given scope level. */
export type ScopeConfig<L extends ScopeLevel> = {
[K in ScopeKeysOf<L>]: string;
};

// Named type aliases for convenience and backwards compatibility.
export type TenantScopeConfig = ScopeConfig<'tenant'>;
export type ProjectScopeConfig = ScopeConfig<'project'>;
export type AgentScopeConfig = ScopeConfig<'agent'>;
export type SubAgentScopeConfig = ScopeConfig<'subAgent'>;
19 changes: 7 additions & 12 deletions packages/agents-core/src/types/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,13 @@ export type PaginationResult = {
pages: number;
};

export type ProjectScopeConfig = {
tenantId: string;
projectId: string;
};

export type AgentScopeConfig = ProjectScopeConfig & {
agentId: string;
};

export type SubAgentScopeConfig = AgentScopeConfig & {
subAgentId: string;
};
// Scope config types are derived from SCOPE_KEYS in scope-definitions.ts.
// Re-exported here to keep existing import paths working.
export type {
AgentScopeConfig,
ProjectScopeConfig,
SubAgentScopeConfig,
} from '../db/manage/scope-definitions';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Consider: Export TenantScopeConfig for completeness

Issue: The new scope-definitions.ts exports TenantScopeConfig, but it's not re-exported here with the other scope config types.

Why: Minor asymmetry — TenantScopeConfig was only defined locally in scope-helpers.ts before, so this isn't a regression. However, consumers who need it must import from the deeper path while other scope configs come from this barrel.

Fix:

Suggested change
} from '../db/manage/scope-definitions';
export type {
AgentScopeConfig,
ProjectScopeConfig,
SubAgentScopeConfig,
TenantScopeConfig,
} from '../db/manage/scope-definitions';

Refs:

export interface ConversationScopeOptions {
taskId?: string;
subAgentId?: string;
Expand Down
Loading