Skip to content
Open
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
12 changes: 7 additions & 5 deletions docs/docs/api/appkit/Class.Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,16 @@ BasePlugin.clientConfig
protected execute<T>(
fn: (signal?: AbortSignal) => Promise<T>,
options: PluginExecutionSettings,
userKey?: string): Promise<T | undefined>;
userKey?: string): Promise<ExecutionResult<T>>;
```

Execute a function with the plugin's interceptor chain.

All errors are caught and `undefined` is returned (production-safe).
Route handlers should check for `undefined` and respond with an
appropriate error status.
Returns an [ExecutionResult](TypeAlias.ExecutionResult.md) discriminated union:
- `{ ok: true, data: T }` on success
- `{ ok: false, status: number, message: string }` on failure

Errors are never thrown — the method is production-safe.

#### Type Parameters

Expand All @@ -338,7 +340,7 @@ appropriate error status.

#### Returns

`Promise`\<`T` \| `undefined`\>
`Promise`\<[`ExecutionResult`](TypeAlias.ExecutionResult.md)\<`T`\>\>

***

Expand Down
33 changes: 33 additions & 0 deletions docs/docs/api/appkit/TypeAlias.ExecutionResult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Type Alias: ExecutionResult\<T\>

```ts
type ExecutionResult<T> =
| {
data: T;
ok: true;
}
| {
message: string;
ok: false;
status: number;
};
```

Discriminated union for plugin execution results.

Replaces the previous `T | undefined` return type on `execute()`.

On failure, the HTTP status code is preserved from:
- `AppKitError` subclasses (via `statusCode`)
- Any `Error` with a numeric `statusCode` property (e.g. `ApiError`)
- All other errors default to status 500

In production, error messages from non-AppKitError sources are handled as:
- 4xx errors: original message is preserved (client-facing by design)
- 5xx errors: replaced with "Server error" to prevent information leakage

## Type Parameters

| Type Parameter |
| ------ |
| `T` |
1 change: 1 addition & 0 deletions docs/docs/api/appkit/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ plugin architecture, and React integration.
| Type Alias | Description |
| ------ | ------ |
| [ConfigSchema](TypeAlias.ConfigSchema.md) | Configuration schema definition for plugin config. Re-exported from the standard JSON Schema Draft 7 types. |
| [ExecutionResult](TypeAlias.ExecutionResult.md) | Discriminated union for plugin execution results. |
| [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration |
| [PluginData](TypeAlias.PluginData.md) | Tuple of plugin class, config, and name. Created by `toPlugin()` and passed to `createApp()`. |
| [ResourcePermission](TypeAlias.ResourcePermission.md) | Union of all possible permission levels across all resource types. |
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/api/appkit/typedoc-sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ const typedocSidebar: SidebarsConfig = {
id: "api/appkit/TypeAlias.ConfigSchema",
label: "ConfigSchema"
},
{
type: "doc",
id: "api/appkit/TypeAlias.ExecutionResult",
label: "ExecutionResult"
},
{
type: "doc",
id: "api/appkit/TypeAlias.IAppRouter",
Expand Down
28 changes: 25 additions & 3 deletions docs/static/appkit-ui/styles.gen.css
Original file line number Diff line number Diff line change
Expand Up @@ -831,9 +831,6 @@
.max-w-\[calc\(100\%-2rem\)\] {
max-width: calc(100% - 2rem);
}
.max-w-full {
max-width: 100%;
}
.max-w-max {
max-width: max-content;
}
Expand Down Expand Up @@ -4514,6 +4511,11 @@
width: calc(var(--spacing) * 5);
}
}
.\[\&_\[data-slot\=scroll-area-viewport\]\>div\]\:\!block {
& [data-slot=scroll-area-viewport]>div {
display: block !important;
}
}
.\[\&_a\]\:underline {
& a {
text-decoration-line: underline;
Expand Down Expand Up @@ -4637,11 +4639,26 @@
color: var(--muted-foreground);
}
}
.\[\&_table\]\:block {
& table {
display: block;
}
}
.\[\&_table\]\:max-w-full {
& table {
max-width: 100%;
}
}
.\[\&_table\]\:border-collapse {
& table {
border-collapse: collapse;
}
}
.\[\&_table\]\:overflow-x-auto {
& table {
overflow-x: auto;
}
}
.\[\&_table\]\:text-xs {
& table {
font-size: var(--text-xs);
Expand Down Expand Up @@ -4851,6 +4868,11 @@
width: 100%;
}
}
.\[\&\>\*\]\:min-w-0 {
&>* {
min-width: calc(var(--spacing) * 0);
}
}
.\[\&\>\*\]\:focus-visible\:relative {
&>* {
&:focus-visible {
Expand Down
7 changes: 6 additions & 1 deletion packages/appkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export {
ValidationError,
} from "./errors";
// Plugin authoring
export { Plugin, type ToPlugin, toPlugin } from "./plugin";
export {
type ExecutionResult,
Plugin,
type ToPlugin,
toPlugin,
} from "./plugin";
export { analytics, files, genie, lakebase, server } from "./plugins";
// Registry types and utilities for plugin manifests
export type {
Expand Down
17 changes: 17 additions & 0 deletions packages/appkit/src/plugin/execution-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Discriminated union for plugin execution results.
*
* Replaces the previous `T | undefined` return type on `execute()`.
*
* On failure, the HTTP status code is preserved from:
* - `AppKitError` subclasses (via `statusCode`)
* - Any `Error` with a numeric `statusCode` property (e.g. `ApiError`)
* - All other errors default to status 500
*
* In production, error messages from non-AppKitError sources are handled as:
* - 4xx errors: original message is preserved (client-facing by design)
* - 5xx errors: replaced with "Server error" to prevent information leakage
*/
export type ExecutionResult<T> =
| { ok: true; data: T }
| { ok: false; status: number; message: string };
1 change: 1 addition & 0 deletions packages/appkit/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { ToPlugin } from "shared";
export type { ExecutionResult } from "./execution-result";
export { Plugin } from "./plugin";
export { toPlugin } from "./to-plugin";
62 changes: 54 additions & 8 deletions packages/appkit/src/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
ServiceContext,
type UserContext,
} from "../context";
import { AuthenticationError } from "../errors";
import { AppKitError, AuthenticationError } from "../errors";
import { createLogger } from "../logging/logger";
import { StreamManager } from "../stream";
import {
Expand All @@ -29,6 +29,7 @@ import {
} from "../telemetry";
import { deepMerge } from "../utils";
import { DevFileReader } from "./dev-reader";
import type { ExecutionResult } from "./execution-result";
import { CacheInterceptor } from "./interceptors/cache";
import { RetryInterceptor } from "./interceptors/retry";
import { TelemetryInterceptor } from "./interceptors/telemetry";
Expand All @@ -40,6 +41,20 @@ import type {

const logger = createLogger("plugin");

/**
* Narrow an unknown thrown value to an Error that carries a numeric
* `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).
*/
function hasHttpStatusCode(
error: unknown,
): error is Error & { statusCode: number } {
return (
error instanceof Error &&
"statusCode" in error &&
typeof (error as Record<string, unknown>).statusCode === "number"
);
}

/**
* Methods that should not be proxied by asUser().
* These are lifecycle/internal methods that don't make sense
Expand Down Expand Up @@ -435,15 +450,17 @@ export abstract class Plugin<
/**
* Execute a function with the plugin's interceptor chain.
*
* All errors are caught and `undefined` is returned (production-safe).
* Route handlers should check for `undefined` and respond with an
* appropriate error status.
* Returns an {@link ExecutionResult} discriminated union:
* - `{ ok: true, data: T }` on success
* - `{ ok: false, status: number, message: string }` on failure
*
* Errors are never thrown — the method is production-safe.
*/
protected async execute<T>(
fn: (signal?: AbortSignal) => Promise<T>,
options: PluginExecutionSettings,
userKey?: string,
): Promise<T | undefined> {
): Promise<ExecutionResult<T>> {
const executeConfig = this._buildExecutionConfig(options);

const interceptors = this._buildInterceptors(executeConfig);
Expand All @@ -457,11 +474,40 @@ export abstract class Plugin<
};

try {
return await this._executeWithInterceptors(fn, interceptors, context);
const data = await this._executeWithInterceptors(
fn,
interceptors,
context,
);
return { ok: true, data };
} catch (error) {
// production-safe: swallow all errors, don't crash the app
logger.error("Plugin execution failed", { error, plugin: this.name });
return undefined;

if (error instanceof AppKitError) {
return {
ok: false,
status: error.statusCode,
message: error.message,
};
}

if (hasHttpStatusCode(error)) {
const isDev = process.env.NODE_ENV !== "production";
const isClientError = error.statusCode >= 400 && error.statusCode < 500;
return {
ok: false,
status: error.statusCode,
message: isDev || isClientError ? error.message : "Server error",
};
}

const isDev = process.env.NODE_ENV !== "production";
return {
ok: false,
status: 500,
message:
isDev && error instanceof Error ? error.message : "Server error",
};
}
}

Expand Down
Loading
Loading