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
13 changes: 6 additions & 7 deletions apps/dev-playground/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const adminOnly: FilePolicy = (action, _resource, user) => {

createApp({
plugins: [
server({ autoStart: false }),
server(),
reconnect(),
telemetryExamples(),
analytics({}),
Expand Down Expand Up @@ -95,9 +95,8 @@ createApp({
// }),
],
...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }),
}).then((appkit) => {
appkit.server
.extend((app) => {
onPluginsReady(appkit) {
appkit.server.extend((app) => {
app.get("/sp", (_req, res) => {
appkit.analytics
.query("SELECT * FROM samples.nyctaxi.trips;")
Expand Down Expand Up @@ -195,9 +194,9 @@ createApp({
results,
});
});
})
.start();
});
});
},
}).catch(console.error);

type ProbeResult = {
volume: string;
Expand Down
8 changes: 4 additions & 4 deletions apps/dev-playground/shared/appkit-types/analytics.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ declare module "@databricks/appkit-ui/react" {
result: Array<{
/** @sqlType STRING */
string_value: string;
/** @sqlType STRING */
number_value: string;
/** @sqlType STRING */
boolean_value: string;
/** @sqlType INT */
number_value: number;
/** @sqlType BOOLEAN */
boolean_value: boolean;
/** @sqlType STRING */
date_value: string;
/** @sqlType STRING */
Expand Down
21 changes: 0 additions & 21 deletions docs/docs/api/appkit/Class.ServerError.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Use for server start/stop issues, configuration conflicts, etc.
## Example

```typescript
throw new ServerError("Cannot get server when autoStart is true");
throw new ServerError("Server not started");
```

Expand Down Expand Up @@ -151,26 +150,6 @@ Create a human-readable string representation

***

### autoStartConflict()

```ts
static autoStartConflict(operation: string): ServerError;
```

Create a server error for autoStart conflict

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `operation` | `string` |

#### Returns

`ServerError`

***

### clientDirectoryNotFound()

```ts
Expand Down
21 changes: 13 additions & 8 deletions docs/docs/api/appkit/Function.createApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
function createApp<T>(config: {
cache?: CacheConfig;
client?: WorkspaceClient;
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
plugins?: T;
telemetry?: TelemetryConfig;
}): Promise<PluginMap<T>>;
Expand All @@ -13,6 +14,9 @@ Bootstraps AppKit with the provided configuration.

Initializes telemetry, cache, and service context, then registers plugins
in phase order (core, normal, deferred) and awaits their setup.
If a `onPluginsReady` callback is provided it runs after plugin setup but
before the server starts, giving you access to the full appkit handle
for registering custom routes or performing async setup.
The returned object maps each plugin name to its `exports()` API,
with an `asUser(req)` method for user-scoped execution.

Expand All @@ -26,9 +30,10 @@ with an `asUser(req)` method for user-scoped execution.

| Parameter | Type |
| ------ | ------ |
| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} |
| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `onPluginsReady?`: (`appkit`: `PluginMap`\<`T`\>) => `void` \| `Promise`\<`void`\>; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} |
| `config.cache?` | [`CacheConfig`](Interface.CacheConfig.md) |
| `config.client?` | `WorkspaceClient` |
| `config.onPluginsReady?` | (`appkit`: `PluginMap`\<`T`\>) => `void` \| `Promise`\<`void`\> |
| `config.plugins?` | `T` |
| `config.telemetry?` | [`TelemetryConfig`](Interface.TelemetryConfig.md) |

Expand All @@ -51,12 +56,12 @@ await createApp({
```ts
import { createApp, server, analytics } from "@databricks/appkit";

const appkit = await createApp({
plugins: [server({ autoStart: false }), analytics({})],
});

appkit.server.extend((app) => {
app.get("/custom", (_req, res) => res.json({ ok: true }));
await createApp({
plugins: [server(), analytics({})],
onPluginsReady(appkit) {
appkit.server.extend((app) => {
app.get("/custom", (_req, res) => res.json({ ok: true }));
});
},
});
await appkit.server.start();
```
33 changes: 24 additions & 9 deletions docs/docs/plugins/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,38 @@ await createApp({
});
```

## Manual server start example
## Custom routes example

When you need to extend Express with custom routes:
Use the `onPluginsReady` callback to extend Express with custom routes before the server starts:

```ts
import { createApp, server } from "@databricks/appkit";

const appkit = await createApp({
plugins: [server({ autoStart: false })],
await createApp({
plugins: [server()],
onPluginsReady(appkit) {
appkit.server.extend((app) => {
app.get("/custom", (_req, res) => res.json({ ok: true }));
});
},
});
```

appkit.server.extend((app) => {
app.get("/custom", (_req, res) => res.json({ ok: true }));
});
The `onPluginsReady` callback also supports async operations:

await appkit.server.start();
```ts
await createApp({
plugins: [server()],
async onPluginsReady(appkit) {
const pool = await initializeDatabase();
appkit.server.extend((app) => {
app.get("/data", async (_req, res) => {
const result = await pool.query("SELECT 1");
res.json(result);
});
});
},
});
```

## Configuration options
Expand All @@ -64,7 +80,6 @@ await createApp({
server({
port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000
host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0"
autoStart: true, // default: true
staticPath: "dist", // optional: force a specific static directory
}),
],
Expand Down
39 changes: 30 additions & 9 deletions packages/appkit/src/core/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import type {
} from "shared";
import { CacheManager } from "../cache";
import { ServiceContext } from "../context";
import { createLogger } from "../logging/logger";
import { ResourceRegistry, ResourceType } from "../registry";
import type { TelemetryConfig } from "../telemetry";
import { TelemetryManager } from "../telemetry";

const logger = createLogger("appkit");

export class AppKit<TPlugins extends InputPluginMap> {
#pluginInstances: Record<string, BasePlugin> = {};
#setupPromises: Promise<void>[] = [];
Expand Down Expand Up @@ -167,6 +170,7 @@ export class AppKit<TPlugins extends InputPluginMap> {
telemetry?: TelemetryConfig;
cache?: CacheConfig;
client?: WorkspaceClient;
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
} = {},
): Promise<PluginMap<T>> {
// Initialize core services
Expand Down Expand Up @@ -200,7 +204,20 @@ export class AppKit<TPlugins extends InputPluginMap> {

await Promise.all(instance.#setupPromises);

return instance as unknown as PluginMap<T>;
const handle = instance as unknown as PluginMap<T>;

if (config.onPluginsReady) {
logger.debug("Running onPluginsReady hook");
await config.onPluginsReady(handle);
logger.debug("onPluginsReady hook completed");
}

const serverPlugin = instance.#pluginInstances.server;
if (serverPlugin && typeof (serverPlugin as any).start === "function") {
await (serverPlugin as any).start();
}

return handle;
}

private static preparePlugins(
Expand All @@ -222,6 +239,9 @@ export class AppKit<TPlugins extends InputPluginMap> {
*
* Initializes telemetry, cache, and service context, then registers plugins
* in phase order (core, normal, deferred) and awaits their setup.
* If a `onPluginsReady` callback is provided it runs after plugin setup but
* before the server starts, giving you access to the full appkit handle
* for registering custom routes or performing async setup.
* The returned object maps each plugin name to its `exports()` API,
* with an `asUser(req)` method for user-scoped execution.
*
Expand All @@ -236,18 +256,18 @@ export class AppKit<TPlugins extends InputPluginMap> {
* });
* ```
*
* @example Extended Server with analytics and custom endpoint
* @example Server with custom routes via onPluginsReady
* ```ts
* import { createApp, server, analytics } from "@databricks/appkit";
*
* const appkit = await createApp({
* plugins: [server({ autoStart: false }), analytics({})],
* });
*
* appkit.server.extend((app) => {
* app.get("/custom", (_req, res) => res.json({ ok: true }));
* await createApp({
* plugins: [server(), analytics({})],
* onPluginsReady(appkit) {
* appkit.server.extend((app) => {
* app.get("/custom", (_req, res) => res.json({ ok: true }));
* });
* },
* });
* await appkit.server.start();
* ```
*/
export async function createApp<
Expand All @@ -258,6 +278,7 @@ export async function createApp<
telemetry?: TelemetryConfig;
cache?: CacheConfig;
client?: WorkspaceClient;
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
} = {},
): Promise<PluginMap<T>> {
return AppKit._createApp(config);
Expand Down
10 changes: 0 additions & 10 deletions packages/appkit/src/errors/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { AppKitError } from "./base";
*
* @example
* ```typescript
* throw new ServerError("Cannot get server when autoStart is true");
* throw new ServerError("Server not started");
* ```
*/
Expand All @@ -15,15 +14,6 @@ export class ServerError extends AppKitError {
readonly statusCode = 500;
readonly isRetryable = false;

/**
* Create a server error for autoStart conflict
*/
static autoStartConflict(operation: string): ServerError {
return new ServerError(`Cannot ${operation} when autoStart is true`, {
context: { operation },
});
}

/**
* Create a server error for server not started
*/
Expand Down
6 changes: 0 additions & 6 deletions packages/appkit/src/errors/tests/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,6 @@ describe("ServerError", () => {
expect(error.isRetryable).toBe(false);
});

test("autoStartConflict should create proper error", () => {
const error = ServerError.autoStartConflict("get server");
expect(error.message).toBe("Cannot get server when autoStart is true");
expect(error.context?.operation).toBe("get server");
});

test("notStarted should create proper error", () => {
const error = ServerError.notStarted();
expect(error.message).toContain("Server not started");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,11 @@ describe("Analytics Plugin Integration", () => {
serverPlugin({
port: TEST_PORT,
host: "127.0.0.1",
autoStart: false,
}),
analytics({}),
],
});

await app.server.start();
server = app.server.getServer();
baseUrl = `http://127.0.0.1:${TEST_PORT}`;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,11 @@ describe("Files Plugin Integration", () => {
serverPlugin({
port: TEST_PORT,
host: "127.0.0.1",
autoStart: false,
}),
files(),
],
});

await appkit.server.start();
server = appkit.server.getServer();
baseUrl = `http://127.0.0.1:${TEST_PORT}`;
});
Expand Down
Loading
Loading