Skip to content

Commit 822d98e

Browse files
authored
feat: appkit exposed apis (#69)
1 parent 4033095 commit 822d98e

19 files changed

Lines changed: 392 additions & 120 deletions

File tree

apps/dev-playground/server/reconnect-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface ReconnectStreamResponse {
1515

1616
export class ReconnectPlugin extends Plugin {
1717
public name = "reconnect";
18-
public envVars = [];
18+
protected envVars: string[] = [];
1919

2020
injectRoutes(router: IAppRouter): void {
2121
this.route<ReconnectResponse>(router, {

apps/dev-playground/server/telemetry-example-plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { Request, Response, Router } from "express";
1717

1818
class TelemetryExamples extends Plugin {
1919
public name = "telemetry-examples" as const;
20-
public envVars: string[] = [];
20+
protected envVars: string[] = [];
2121

2222
private requestCounter: Counter;
2323
private durationHistogram: Histogram;
@@ -516,5 +516,5 @@ class TelemetryExamples extends Plugin {
516516
export const telemetryExamples = toPlugin<
517517
typeof TelemetryExamples,
518518
BasePluginConfig,
519-
"telemetry-examples"
520-
>(TelemetryExamples, "telemetry-examples");
519+
"telemetryExamples"
520+
>(TelemetryExamples, "telemetryExamples");

docs/docs/api/appkit/Class.AppKitError.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ readonly optional cause: Error;
7676

7777
Optional cause of the error
7878

79+
#### Overrides
80+
81+
```ts
82+
Error.cause
83+
```
84+
7985
***
8086

8187
### code

docs/docs/api/appkit/Class.Plugin.md

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ asUser(req: Request): this;
143143
```
144144

145145
Execute operations using the user's identity from the request.
146-
147-
Returns a scoped instance of this plugin where all method calls
148-
will execute with the user's Databricks credentials instead of
149-
the service principal.
146+
Returns a proxy of this plugin where all method calls execute
147+
with the user's Databricks credentials instead of the service principal.
150148

151149
#### Parameters
152150

@@ -158,31 +156,12 @@ the service principal.
158156

159157
`this`
160158

161-
A scoped plugin instance that executes as the user
159+
A proxied plugin instance that executes as the user
162160

163161
#### Throws
164162

165163
Error if user token is not available in request headers
166164

167-
#### Example
168-
169-
```typescript
170-
// In route handler - execute query as the requesting user
171-
router.post('/users/me/query/:key', async (req, res) => {
172-
const result = await this.asUser(req).query(req.params.key)
173-
res.json(result)
174-
})
175-
176-
// Mixed execution in same handler
177-
router.post('/dashboard', async (req, res) => {
178-
const [systemData, userData] = await Promise.all([
179-
this.getSystemStats(), // Service principal
180-
this.asUser(req).getUserPreferences(), // User context
181-
])
182-
res.json({ systemData, userData })
183-
})
184-
```
185-
186165
***
187166

188167
### execute()
@@ -245,6 +224,28 @@ userKey?: string): Promise<void>;
245224

246225
***
247226

227+
### exports()
228+
229+
```ts
230+
exports(): unknown;
231+
```
232+
233+
Returns the public exports for this plugin.
234+
Override this to define a custom public API.
235+
By default, returns an empty object.
236+
237+
#### Returns
238+
239+
`unknown`
240+
241+
#### Implementation of
242+
243+
```ts
244+
BasePlugin.exports
245+
```
246+
247+
***
248+
248249
### getEndpoints()
249250

250251
```ts
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Function: getExecutionContext()
2+
3+
```ts
4+
function getExecutionContext(): ExecutionContext;
5+
```
6+
7+
Get the current execution context.
8+
9+
- If running inside a user context (via asUser), returns the user context
10+
- Otherwise, returns the service context
11+
12+
## Returns
13+
14+
`ExecutionContext`
15+
16+
## Throws
17+
18+
Error if ServiceContext is not initialized

docs/docs/api/appkit/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ plugin architecture, and React integration.
4646
| ------ | ------ |
4747
| [appKitTypesPlugin](Function.appKitTypesPlugin.md) | Vite plugin to generate types for AppKit queries. Calls generateFromEntryPoint under the hood. |
4848
| [createApp](Function.createApp.md) | Bootstraps AppKit with the provided configuration. |
49+
| [getExecutionContext](Function.getExecutionContext.md) | Get the current execution context. |
4950
| [isSQLTypeMarker](Function.isSQLTypeMarker.md) | Type guard to check if a value is a SQL type marker |

docs/docs/api/appkit/typedoc-sidebar.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ const typedocSidebar: SidebarsConfig = {
124124
id: "api/appkit/Function.createApp",
125125
label: "createApp"
126126
},
127+
{
128+
type: "doc",
129+
id: "api/appkit/Function.getExecutionContext",
130+
label: "getExecutionContext"
131+
},
127132
{
128133
type: "doc",
129134
id: "api/appkit/Function.isSQLTypeMarker",

docs/docs/plugins.md

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,25 @@ import type express from "express";
219219

220220
class MyPlugin extends Plugin {
221221
name = "myPlugin";
222-
envVars = []; // list required env vars here
223-
224-
injectRoutes(router: express.Router) {
225-
this.route(router, {
226-
name: "hello",
227-
method: "get",
228-
path: "/hello",
229-
handler: async (_req, res) => {
230-
res.json({ ok: true });
231-
},
232-
});
222+
envVars = ["MY_API_KEY"];
223+
224+
async setup() {
225+
// Initialize your plugin
226+
}
227+
228+
myCustomMethod() {
229+
// Some implementation
230+
}
231+
232+
async shutdown() {
233+
// Clean up resources
234+
}
235+
236+
exports() {
237+
// an object with the methods from this plugin to expose
238+
return {
239+
myCustomMethod: this.myCustomMethod
240+
}
233241
}
234242
}
235243

@@ -248,6 +256,23 @@ export const myPlugin = toPlugin<typeof MyPlugin, Record<string, never>, "myPlug
248256
- **Telemetry**: Instrument your plugin with traces and metrics via `this.telemetry`. See [`ITelemetry`](api/appkit/Interface.ITelemetry.md).
249257
- **Execution interceptors**: Use `execute()` and `executeStream()` with [`StreamExecutionSettings`](api/appkit/Interface.StreamExecutionSettings.md)
250258

259+
**Consuming your plugin programmatically**
260+
261+
Optionally, you may want to provide a way to consume your plugin programmatically using the AppKit object.
262+
To do that, your plugin needs to implement the `exports` method, returning an object with the methods you want to expose. From the previous example, the plugin could be consumed as follows:
263+
264+
```ts
265+
const AppKit = await createApp({
266+
plugins: [
267+
server({ port: 8000 }),
268+
analytics(),
269+
myPlugin(),
270+
],
271+
});
272+
273+
AppKit.myPlugin.myCustomMethod();
274+
```
275+
251276
See the [`Plugin`](api/appkit/Class.Plugin.md) API reference for complete documentation.
252277

253278
## Caching

packages/appkit/src/analytics/analytics.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const logger = createLogger("analytics");
2626

2727
export class AnalyticsPlugin extends Plugin {
2828
name = "analytics";
29-
envVars = [];
29+
protected envVars: string[] = [];
3030

3131
protected static description = "Analytics plugin for data analysis";
3232
protected declare config: IAnalyticsConfig;
@@ -264,6 +264,19 @@ export class AnalyticsPlugin extends Plugin {
264264
async shutdown(): Promise<void> {
265265
this.streamManager.abortAll();
266266
}
267+
268+
/**
269+
* Returns the public exports for the analytics plugin.
270+
* Note: `asUser()` is automatically added by AppKit.
271+
*/
272+
exports() {
273+
return {
274+
/**
275+
* Execute a SQL query using service principal credentials.
276+
*/
277+
query: this.query,
278+
};
279+
}
267280
}
268281

269282
/**

packages/appkit/src/core/appkit.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ import type { TelemetryConfig } from "../telemetry";
1313
import { TelemetryManager } from "../telemetry";
1414

1515
export class AppKit<TPlugins extends InputPluginMap> {
16-
private static _instance: AppKit<InputPluginMap> | null = null;
17-
private pluginInstances: Record<string, BasePlugin> = {};
18-
private setupPromises: Promise<void>[] = [];
16+
#pluginInstances: Record<string, BasePlugin> = {};
17+
#setupPromises: Promise<void>[] = [];
1918

2019
private constructor(config: { plugins: TPlugins }) {
2120
const { plugins, ...globalConfig } = config;
@@ -47,7 +46,7 @@ export class AppKit<TPlugins extends InputPluginMap> {
4746
for (const [name, pluginData] of deferredPlugins) {
4847
if (pluginData) {
4948
this.createAndRegisterPlugin(globalConfig, name, pluginData, {
50-
plugins: this.pluginInstances,
49+
plugins: this.#pluginInstances,
5150
});
5251
}
5352
}
@@ -69,20 +68,72 @@ export class AppKit<TPlugins extends InputPluginMap> {
6968
};
7069
const pluginInstance = new Plugin(baseConfig);
7170

72-
this.pluginInstances[name] = pluginInstance;
71+
this.#pluginInstances[name] = pluginInstance;
7372

7473
pluginInstance.validateEnv();
7574

76-
this.setupPromises.push(pluginInstance.setup());
75+
this.#setupPromises.push(pluginInstance.setup());
76+
77+
const self = this;
7778

7879
Object.defineProperty(this, name, {
7980
get() {
80-
return this.pluginInstances[name];
81+
const plugin = self.#pluginInstances[name];
82+
return self.wrapWithAsUser(plugin);
8183
},
8284
enumerable: true,
8385
});
8486
}
8587

88+
/**
89+
* Binds all function properties in an exports object to the given context.
90+
*/
91+
private bindExportMethods(
92+
exports: Record<string, unknown>,
93+
context: BasePlugin,
94+
) {
95+
for (const key in exports) {
96+
if (Object.hasOwn(exports, key) && typeof exports[key] === "function") {
97+
exports[key] = (exports[key] as (...args: unknown[]) => unknown).bind(
98+
context,
99+
);
100+
}
101+
}
102+
}
103+
104+
/**
105+
* Wraps a plugin's exports with an `asUser` method that returns
106+
* a user-scoped version of the exports.
107+
*/
108+
private wrapWithAsUser<T extends BasePlugin>(plugin: T) {
109+
// If plugin doesn't implement exports(), return empty object
110+
const pluginExports = (plugin.exports?.() ?? {}) as Record<string, unknown>;
111+
this.bindExportMethods(pluginExports, plugin);
112+
113+
// If plugin doesn't support asUser (no asUser method), return exports as-is
114+
if (typeof (plugin as any).asUser !== "function") {
115+
return pluginExports;
116+
}
117+
118+
return {
119+
...pluginExports,
120+
/**
121+
* Execute operations using the user's identity from the request.
122+
* Returns user-scoped exports where all methods execute with the
123+
* user's Databricks credentials instead of the service principal.
124+
*/
125+
asUser: (req: import("express").Request) => {
126+
const userPlugin = (plugin as any).asUser(req);
127+
const userExports = (userPlugin.exports?.() ?? {}) as Record<
128+
string,
129+
unknown
130+
>;
131+
this.bindExportMethods(userExports, userPlugin);
132+
return userExports;
133+
},
134+
};
135+
}
136+
86137
static async _createApp<
87138
T extends PluginData<PluginConstructor, unknown, string>[],
88139
>(
@@ -106,11 +157,11 @@ export class AppKit<TPlugins extends InputPluginMap> {
106157
plugins: preparedPlugins,
107158
};
108159

109-
AppKit._instance = new AppKit(mergedConfig);
160+
const instance = new AppKit(mergedConfig);
110161

111-
await Promise.all(AppKit._instance.setupPromises);
162+
await Promise.all(instance.#setupPromises);
112163

113-
return AppKit._instance as unknown as PluginMap<T>;
164+
return instance as unknown as PluginMap<T>;
114165
}
115166

116167
private static preparePlugins(

0 commit comments

Comments
 (0)