@databricks/appkit 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +2 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js +2 -8
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/connectors/files/client.js +223 -0
- package/dist/connectors/files/client.js.map +1 -0
- package/dist/connectors/files/defaults.js +131 -0
- package/dist/connectors/files/defaults.js.map +1 -0
- package/dist/connectors/files/index.js +4 -0
- package/dist/connectors/genie/client.js +18 -6
- package/dist/connectors/genie/client.js.map +1 -1
- package/dist/connectors/index.js +3 -0
- package/dist/context/execution-context.js +7 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +24 -4
- package/dist/core/appkit.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/plugin.d.ts +24 -5
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +43 -10
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugin/to-plugin.d.ts +5 -2
- package/dist/plugin/to-plugin.d.ts.map +1 -1
- package/dist/plugin/to-plugin.js +5 -2
- package/dist/plugin/to-plugin.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +1 -2
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +1 -2
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/files/defaults.d.ts +1 -0
- package/dist/plugins/files/defaults.js +56 -0
- package/dist/plugins/files/defaults.js.map +1 -0
- package/dist/plugins/files/helpers.js +30 -0
- package/dist/plugins/files/helpers.js.map +1 -0
- package/dist/plugins/files/index.d.ts +3 -0
- package/dist/plugins/files/index.js +5 -0
- package/dist/plugins/files/manifest.js +40 -0
- package/dist/plugins/files/manifest.js.map +1 -0
- package/dist/plugins/files/plugin.d.ts +105 -0
- package/dist/plugins/files/plugin.d.ts.map +1 -0
- package/dist/plugins/files/plugin.js +714 -0
- package/dist/plugins/files/plugin.js.map +1 -0
- package/dist/plugins/files/types.d.ts +105 -0
- package/dist/plugins/files/types.d.ts.map +1 -0
- package/dist/plugins/genie/genie.d.ts +1 -2
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +1 -2
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/lakebase/lakebase.d.ts +1 -2
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +1 -2
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/index.d.ts +2 -2
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +9 -4
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/registry/manifest-loader.js +1 -1
- package/dist/registry/manifest-loader.js.map +1 -1
- package/dist/registry/types.d.ts +3 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/shared/src/plugin.d.ts +12 -4
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/docs/api/appkit/Class.Plugin.md +60 -12
- package/docs/api/appkit/Class.ResourceRegistry.md +3 -3
- package/docs/api/appkit/Function.createApp.md +3 -3
- package/docs/api/appkit/Interface.PluginManifest.md +9 -3
- package/docs/api/appkit/TypeAlias.PluginData.md +45 -0
- package/docs/api/appkit/TypeAlias.ToPlugin.md +1 -1
- package/docs/api/appkit-ui/files/DirectoryList.md +36 -0
- package/docs/api/appkit-ui/files/FileBreadcrumb.md +27 -0
- package/docs/api/appkit-ui/files/FileEntry.md +27 -0
- package/docs/api/appkit-ui/files/FilePreviewPanel.md +32 -0
- package/docs/api/appkit-ui/files/NewFolderInput.md +30 -0
- package/docs/api/appkit.md +1 -0
- package/docs/configuration.md +15 -0
- package/docs/plugins/custom-plugins.md +4 -13
- package/docs/plugins/files.md +350 -0
- package/docs/plugins.md +2 -1
- package/llms.txt +8 -1
- package/package.json +1 -1
- package/dist/plugins/server/remote-tunnel/denied.html/denied.html +0 -68
- package/dist/plugins/server/remote-tunnel/index.html/index.html +0 -165
- package/dist/plugins/server/remote-tunnel/wait.html/wait.html +0 -158
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * name = 'myPlugin';\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * name = 'myPlugin';\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * name = \"myPlugin\";\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this._createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private _createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;cAoBoB;aAC4B;AAmBhD,MAAM,SAAS,aAAa,SAAS;;;;;;AAOrC,MAAM,sBAAsB,IAAI,IAAI;CAElC;CACA;CACA;CACA;CACA;CAEA;CAEA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFF,IAAsB,SAAtB,MAGA;CACE,AAAU,UAAU;CACpB,AAAU;CACV,AAAU;CACV,AAAU;CACV,AAAU;CACV,AAAU;;CAGV,AAAQ,sBAAyC,EAAE;;;;;;;CAQnD,OAAO,QAAqB;;;;CAK5B;CAEA,YAAY,AAAU,QAAiB;EAAjB;AACpB,OAAK,OAAO,OAAO,QAAQ;AAC3B,OAAK,YAAY,iBAAiB,YAAY,KAAK,MAAM,OAAO,UAAU;AAC1E,OAAK,gBAAgB,IAAI,eAAe;AACxC,OAAK,QAAQ,aAAa,iBAAiB;AAC3C,OAAK,MAAM,IAAI,YAAY;AAC3B,OAAK,gBAAgB,cAAc,aAAa;AAEhD,OAAK,UAAU;;CAGjB,aAAa,GAAmB;CAIhC,MAAM,QAAQ;CAEd,eAAkC;AAChC,SAAO,KAAK;;CAGd,wBAA8B;AAC5B,OAAK,cAAc,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4B/B,UAAmB;AACjB,SAAO,EAAE;;;;;;;;;;;CAYX,OAAO,KAA4B;EACjC,MAAM,QAAQ,IAAI,QAAQ;EAC1B,MAAM,SAAS,IAAI,QAAQ;EAC3B,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAIvC,MAAI,CAAC,SAAS,OAAO;AACnB,UAAO,KACL,mFACD;AAED,UAAO;;AAGT,MAAI,CAAC,MACH,OAAM,oBAAoB,aAAa,aAAa;AAGtD,MAAI,CAAC,UAAU,CAAC,MACd,OAAM,oBAAoB,eAAe;EAG3C,MAAM,kBAAkB,UAAU;EAElC,MAAM,cAAc,eAAe,kBACjC,OACA,gBACD;AAGD,SAAO,KAAK,wBAAwB,YAAY;;;;;;;CAQlD,AAAQ,wBAAwB,aAAgC;AAC9D,SAAO,IAAI,MAAM,MAAM,EACrB,MAAM,QAAQ,MAAM,aAAa;GAC/B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AAEjD,OAAI,OAAO,UAAU,WACnB,QAAO;AAGT,OAAI,OAAO,SAAS,YAAY,oBAAoB,IAAI,KAAK,CAC3D,QAAO;AAGT,WAAQ,GAAG,SAAoB;AAC7B,WAAO,iBAAiB,mBAAmB,MAAM,MAAM,QAAQ,KAAK,CAAC;;KAG1E,CAAC;;CAIJ,MAAgB,cACd,KACA,IACA,SACA,SACA;EAEA,MAAM,EACJ,QAAQ,cACR,SAAS,eACT,MAAM,eACJ;EAGJ,MAAM,gBAAgB,KAAK,sBAAsB;GAC/C,SAAS;GACT,MAAM;GACP,CAAC;EAGF,MAAM,mBAAmB,WAAW,kBAAkB;EAEtD,MAAM,OAAO;EAGb,MAAM,iBAAiB,iBAAiB,cAA4B;GAElE,MAAM,UAA8B;IAClC,QAAQ;IACR,0BAAU,IAAI,KAAK;IACnB,SAAS;IACV;GAGD,MAAM,eAAe,KAAK,mBAAmB,cAAc;GAG3D,MAAM,YAAY,YAAY;AAE5B,WADe,MAAM,GAAG,QAAQ,OAAO;;GAKzC,MAAM,SAAS,MAAM,KAAK,yBACxB,WACA,cACA,QACD;AAGD,OAAI,KAAK,kBAAkB,OAAO,CAChC,QAAO;OAEP,OAAM;;AAKV,QAAM,KAAK,cAAc,OAAO,KAAK,gBAAgB,aAAa;;CAIpE,MAAgB,QACd,IACA,SACA,SACwB;EACxB,MAAM,gBAAgB,KAAK,sBAAsB,QAAQ;EAEzD,MAAM,eAAe,KAAK,mBAAmB,cAAc;EAG3D,MAAM,mBAAmB,WAAW,kBAAkB;EAEtD,MAAM,UAA8B;GAClC,0BAAU,IAAI,KAAK;GACnB,SAAS;GACV;AAED,MAAI;AACF,UAAO,MAAM,KAAK,yBAAyB,IAAI,cAAc,QAAQ;WAC9D,QAAQ;AAEf;;;CAIJ,AAAU,iBAAiB,MAAc,MAAoB;AAC3D,OAAK,oBAAoB,QAAQ;;CAGnC,AAAU,MACR,QACA,QACM;EACN,MAAM,EAAE,MAAM,QAAQ,MAAM,YAAY;AAExC,SAAO,QAAQ,MAAM,QAAQ;AAE7B,OAAK,iBAAiB,MAAM,QAAQ,KAAK,OAAO,OAAO;;CAIzD,AAAQ,sBACN,SACqB;EACrB,MAAM,EAAE,SAAS,gBAAgB,MAAM,iBAAiB;AAGxD,SAAO,UACL,UAAU,gBAAgB,KAAK,OAAO,EACtC,gBAAgB,EAAE,CACnB;;CAIH,AAAQ,mBACN,SACwB;EACxB,MAAM,eAAuC,EAAE;AAK/C,MADwB,0BAA0B,KAAK,OAAO,UAAU,CAEtD,WACf,QAAQ,sBAAsB,WAAW,MAE1C,cAAa,KACX,IAAI,qBAAqB,KAAK,WAAW,QAAQ,qBAAqB,CACvE;AAGH,MAAI,QAAQ,WAAW,QAAQ,UAAU,EACvC,cAAa,KAAK,IAAI,mBAAmB,QAAQ,QAAQ,CAAC;AAG5D,MACE,QAAQ,OAAO,WACf,QAAQ,MAAM,YACd,QAAQ,MAAM,WAAW,EAEzB,cAAa,KAAK,IAAI,iBAAiB,QAAQ,MAAM,CAAC;AAGxD,MAAI,QAAQ,OAAO,WAAW,QAAQ,MAAM,UAAU,OACpD,cAAa,KAAK,IAAI,iBAAiB,KAAK,OAAO,QAAQ,MAAM,CAAC;AAGpE,SAAO;;CAIT,MAAc,yBACZ,IACA,cACA,SACY;AAEZ,MAAI,aAAa,WAAW,EAC1B,QAAO,GAAG,QAAQ,OAAO;EAG3B,IAAI,kBAAkB,GAAG,QAAQ,OAAO;AAGxC,OAAK,MAAM,eAAe,cAAc;GACtC,MAAM,aAAa;AACnB,qBAAkB,YAAY,UAAU,YAAY,QAAQ;;AAG9D,SAAO,WAAW;;CAGpB,AAAQ,kBACN,QAC8C;AAC9C,SACE,UAAU,OAAO,WAAW,YAAY,OAAO,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\");\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingToken(\n \"Missing x-forwarded-user header. Cannot resolve user ID.\",\n );\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), falls back to the service principal instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\");\n const userId = req.header(\"x-forwarded-user\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this._createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private _createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * All errors are caught and `undefined` is returned (production-safe).\n * Route handlers should check for `undefined` and respond with an\n * appropriate error status.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (error) {\n // production-safe: swallow all errors, don't crash the app\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n const fullPath = `/api/${this.name}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { ToPlugin } from "../shared/src/plugin.js";
|
|
1
|
+
import { PluginConstructor, ToPlugin } from "../shared/src/plugin.js";
|
|
2
2
|
import "../shared/src/index.js";
|
|
3
3
|
|
|
4
4
|
//#region src/plugin/to-plugin.d.ts
|
|
5
5
|
/**
|
|
6
|
+
* Wraps a plugin class so it can be passed to createApp with optional config.
|
|
7
|
+
* Infers config type from the constructor and plugin name from the static `name` property.
|
|
8
|
+
*
|
|
6
9
|
* @internal
|
|
7
10
|
*/
|
|
8
|
-
declare function toPlugin<T
|
|
11
|
+
declare function toPlugin<T extends PluginConstructor>(plugin: T): ToPlugin<T, ConstructorParameters<T>[0], T["manifest"]["name"]>;
|
|
9
12
|
//#endregion
|
|
10
13
|
export { toPlugin };
|
|
11
14
|
//# sourceMappingURL=to-plugin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-plugin.d.ts","names":[],"sources":["../../src/plugin/to-plugin.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"to-plugin.d.ts","names":[],"sources":["../../src/plugin/to-plugin.ts"],"mappings":";;;;;;;AAQA;;;iBAAgB,QAAA,WAAmB,iBAAA,CAAA,CACjC,MAAA,EAAQ,CAAA,GACP,QAAA,CAAS,CAAA,EAAG,qBAAA,CAAsB,CAAA,MAAO,CAAA"}
|
package/dist/plugin/to-plugin.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
//#region src/plugin/to-plugin.ts
|
|
2
2
|
/**
|
|
3
|
+
* Wraps a plugin class so it can be passed to createApp with optional config.
|
|
4
|
+
* Infers config type from the constructor and plugin name from the static `name` property.
|
|
5
|
+
*
|
|
3
6
|
* @internal
|
|
4
7
|
*/
|
|
5
|
-
function toPlugin(plugin
|
|
8
|
+
function toPlugin(plugin) {
|
|
6
9
|
return (config = {}) => ({
|
|
7
10
|
plugin,
|
|
8
11
|
config,
|
|
9
|
-
name
|
|
12
|
+
name: plugin.manifest.name
|
|
10
13
|
});
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-plugin.js","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":["import type { PluginData, ToPlugin } from \"shared\";\n\n/**\n * @internal\n */\nexport function toPlugin<T
|
|
1
|
+
{"version":3,"file":"to-plugin.js","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":["import type { PluginConstructor, PluginData, ToPlugin } from \"shared\";\n\n/**\n * Wraps a plugin class so it can be passed to createApp with optional config.\n * Infers config type from the constructor and plugin name from the static `name` property.\n *\n * @internal\n */\nexport function toPlugin<T extends PluginConstructor>(\n plugin: T,\n): ToPlugin<T, ConstructorParameters<T>[0], T[\"manifest\"][\"name\"]> {\n type Config = ConstructorParameters<T>[0];\n type Name = T[\"manifest\"][\"name\"];\n return (config: Config = {} as Config): PluginData<T, Config, Name> => ({\n plugin: plugin as T,\n config: config as Config,\n name: plugin.manifest.name as Name,\n });\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,SACd,QACiE;AAGjE,SAAQ,SAAiB,EAAE,MAA6C;EAC9D;EACA;EACR,MAAM,OAAO,SAAS;EACvB"}
|
|
@@ -11,9 +11,8 @@ import express from "express";
|
|
|
11
11
|
|
|
12
12
|
//#region src/plugins/analytics/analytics.d.ts
|
|
13
13
|
declare class AnalyticsPlugin extends Plugin {
|
|
14
|
-
name: string;
|
|
15
14
|
/** Plugin manifest declaring metadata and resource requirements */
|
|
16
|
-
static manifest: PluginManifest
|
|
15
|
+
static manifest: PluginManifest<"analytics">;
|
|
17
16
|
protected static description: string;
|
|
18
17
|
protected config: IAnalyticsConfig;
|
|
19
18
|
private SQLClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;cA4Ba,eAAA,SAAwB,MAAA
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;cA4Ba,eAAA,SAAwB,MAAA;;SAE5B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EApBS;;;;EA6CxB,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EADI;;;;EAuCD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA+G2B;;;;;;;;;;;;;;;EAFxB,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EA7MsC;;;EAAA,UAqOzB,YAAA,CACd,eAAA,EAAiB,eAAA,EACjB,KAAA,UACA,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,UAAA,aAAuB,SAAA,CAAU,YAAA;EAItC,QAAA,CAAA,GAAY,OAAA;EAzOD;;;;EAiPjB,OAAA,CAAA;;;;2BA5Ce,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAqDQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
|
|
@@ -14,7 +14,6 @@ import { QueryProcessor } from "./query.js";
|
|
|
14
14
|
init_context();
|
|
15
15
|
const logger = createLogger("analytics");
|
|
16
16
|
var AnalyticsPlugin = class extends Plugin {
|
|
17
|
-
name = "analytics";
|
|
18
17
|
/** Plugin manifest declaring metadata and resource requirements */
|
|
19
18
|
static manifest = manifest_default;
|
|
20
19
|
static description = "Analytics plugin for data analysis";
|
|
@@ -178,7 +177,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
178
177
|
/**
|
|
179
178
|
* @internal
|
|
180
179
|
*/
|
|
181
|
-
const analytics = toPlugin(AnalyticsPlugin
|
|
180
|
+
const analytics = toPlugin(AnalyticsPlugin);
|
|
182
181
|
|
|
183
182
|
//#endregion
|
|
184
183
|
export { AnalyticsPlugin, analytics };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\nexport class AnalyticsPlugin extends Plugin {\n name = \"analytics\";\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const userKey = getCurrentUserId();\n const executorKey = isAsUser ? userKey : \"global\";\n\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n executorKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await executor.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await executor.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin<\n typeof AnalyticsPlugin,\n IAnalyticsConfig,\n \"analytics\"\n>(AnalyticsPlugin, \"analytics\");\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\nexport class AnalyticsPlugin extends Plugin {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const userKey = getCurrentUserId();\n const executorKey = isAsUser ? userKey : \"global\";\n\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n executorKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await executor.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await executor.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../../shared/src/index.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { EXTENSION_CONTENT_TYPES, FILES_MAX_READ_SIZE } from "../../connectors/files/defaults.js";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/files/defaults.ts
|
|
4
|
+
/**
|
|
5
|
+
* Execution defaults for read-tier operations (list, read, exists, metadata, preview).
|
|
6
|
+
* Cache 60s (ttl in seconds)
|
|
7
|
+
* Retry 3x with 1s backoff
|
|
8
|
+
* Timeout 30s
|
|
9
|
+
**/
|
|
10
|
+
const FILES_READ_DEFAULTS = {
|
|
11
|
+
cache: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
ttl: 60
|
|
14
|
+
},
|
|
15
|
+
retry: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
initialDelay: 1e3,
|
|
18
|
+
attempts: 3
|
|
19
|
+
},
|
|
20
|
+
timeout: 3e4
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Execution defaults for download-tier operations (download, raw).
|
|
24
|
+
* No cache
|
|
25
|
+
* Retry 3x with 1s backoff
|
|
26
|
+
* Timeout 30s (stream start only)
|
|
27
|
+
**/
|
|
28
|
+
const FILES_DOWNLOAD_DEFAULTS = {
|
|
29
|
+
cache: { enabled: false },
|
|
30
|
+
retry: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
initialDelay: 1e3,
|
|
33
|
+
attempts: 3
|
|
34
|
+
},
|
|
35
|
+
timeout: 3e4
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Execution defaults for write-tier operations (upload, mkdir, delete).
|
|
39
|
+
* No cache
|
|
40
|
+
* No retry
|
|
41
|
+
* Timeout 600s.
|
|
42
|
+
**/
|
|
43
|
+
const FILES_WRITE_DEFAULTS = {
|
|
44
|
+
cache: { enabled: false },
|
|
45
|
+
retry: { enabled: false },
|
|
46
|
+
timeout: 6e5
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Default maximum upload size in bytes (5 GB).
|
|
50
|
+
* This matches the Databricks Files API v2 per-file limit.
|
|
51
|
+
*/
|
|
52
|
+
const FILES_MAX_UPLOAD_SIZE = 5 * 1024 * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { FILES_DOWNLOAD_DEFAULTS, FILES_MAX_UPLOAD_SIZE, FILES_READ_DEFAULTS, FILES_WRITE_DEFAULTS };
|
|
56
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/files/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\n/**\n * Execution defaults for read-tier operations (list, read, exists, metadata, preview).\n * Cache 60s (ttl in seconds)\n * Retry 3x with 1s backoff\n * Timeout 30s\n **/\nexport const FILES_READ_DEFAULTS: PluginExecuteConfig = {\n cache: {\n enabled: true,\n ttl: 60,\n },\n retry: {\n enabled: true,\n initialDelay: 1000,\n attempts: 3,\n },\n timeout: 30_000,\n};\n\n/**\n * Execution defaults for download-tier operations (download, raw).\n * No cache\n * Retry 3x with 1s backoff\n * Timeout 30s (stream start only)\n **/\nexport const FILES_DOWNLOAD_DEFAULTS: PluginExecuteConfig = {\n cache: {\n enabled: false,\n },\n retry: {\n enabled: true,\n initialDelay: 1000,\n attempts: 3,\n },\n /**\n * @info this timeout is for the stream to start, not for the full download.\n */\n timeout: 30_000,\n};\n\n/**\n * Execution defaults for write-tier operations (upload, mkdir, delete).\n * No cache\n * No retry\n * Timeout 600s.\n **/\nexport const FILES_WRITE_DEFAULTS: PluginExecuteConfig = {\n cache: {\n enabled: false,\n },\n retry: {\n enabled: false,\n },\n timeout: 600_000,\n};\n\n/**\n * Default maximum upload size in bytes (5 GB).\n * This matches the Databricks Files API v2 per-file limit.\n */\nexport const FILES_MAX_UPLOAD_SIZE = 5 * 1024 * 1024 * 1024; // 5 GB\n\nexport {\n EXTENSION_CONTENT_TYPES,\n FILES_MAX_READ_SIZE,\n} from \"../../connectors/files/defaults\";\n"],"mappings":";;;;;;;;;AAQA,MAAa,sBAA2C;CACtD,OAAO;EACL,SAAS;EACT,KAAK;EACN;CACD,OAAO;EACL,SAAS;EACT,cAAc;EACd,UAAU;EACX;CACD,SAAS;CACV;;;;;;;AAQD,MAAa,0BAA+C;CAC1D,OAAO,EACL,SAAS,OACV;CACD,OAAO;EACL,SAAS;EACT,cAAc;EACd,UAAU;EACX;CAID,SAAS;CACV;;;;;;;AAQD,MAAa,uBAA4C;CACvD,OAAO,EACL,SAAS,OACV;CACD,OAAO,EACL,SAAS,OACV;CACD,SAAS;CACV;;;;;AAMD,MAAa,wBAAwB,IAAI,OAAO,OAAO"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { contentTypeFromPath, isTextContentType } from "../../connectors/files/defaults.js";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/files/helpers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Extract the parent directory from a file or directory path.
|
|
6
|
+
*
|
|
7
|
+
* Handles edge cases such as root-level files (`"/file.txt"` → `"/"`),
|
|
8
|
+
* paths without slashes (`"file.txt"` → `""`), and trailing slashes.
|
|
9
|
+
*/
|
|
10
|
+
function parentDirectory(path) {
|
|
11
|
+
const normalized = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
12
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
13
|
+
if (lastSlash > 0) return normalized.substring(0, lastSlash);
|
|
14
|
+
if (normalized.startsWith("/")) return "/";
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize a filename for use in a `Content-Disposition` HTTP header.
|
|
19
|
+
*
|
|
20
|
+
* Redundancy check – Unity Catalog is unlikely to allow filenames with
|
|
21
|
+
* quotes or control characters, but we sanitize defensively to prevent
|
|
22
|
+
* HTTP header injection if upstream constraints ever change.
|
|
23
|
+
*/
|
|
24
|
+
function sanitizeFilename(raw) {
|
|
25
|
+
return raw.replace(/["\\]/g, "\\$&").replace(/[\x00-\x1f]/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { parentDirectory, sanitizeFilename };
|
|
30
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../../../src/plugins/files/helpers.ts"],"sourcesContent":["export {\n contentTypeFromPath,\n isTextContentType,\n} from \"../../connectors/files/defaults\";\n\n/**\n * Extract the parent directory from a file or directory path.\n *\n * Handles edge cases such as root-level files (`\"/file.txt\"` → `\"/\"`),\n * paths without slashes (`\"file.txt\"` → `\"\"`), and trailing slashes.\n */\nexport function parentDirectory(path: string): string {\n const normalized =\n path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n const lastSlash = normalized.lastIndexOf(\"/\");\n\n if (lastSlash > 0) return normalized.substring(0, lastSlash);\n if (normalized.startsWith(\"/\")) return \"/\";\n return \"\";\n}\n\n/**\n * Sanitize a filename for use in a `Content-Disposition` HTTP header.\n *\n * Redundancy check – Unity Catalog is unlikely to allow filenames with\n * quotes or control characters, but we sanitize defensively to prevent\n * HTTP header injection if upstream constraints ever change.\n */\nexport function sanitizeFilename(raw: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally stripping control chars for security\n return raw.replace(/[\"\\\\]/g, \"\\\\$&\").replace(/[\\x00-\\x1f]/g, \"\");\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,gBAAgB,MAAsB;CACpD,MAAM,aACJ,KAAK,SAAS,KAAK,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG;CAC9D,MAAM,YAAY,WAAW,YAAY,IAAI;AAE7C,KAAI,YAAY,EAAG,QAAO,WAAW,UAAU,GAAG,UAAU;AAC5D,KAAI,WAAW,WAAW,IAAI,CAAE,QAAO;AACvC,QAAO;;;;;;;;;AAUT,SAAgB,iBAAiB,KAAqB;AAEpD,QAAO,IAAI,QAAQ,UAAU,OAAO,CAAC,QAAQ,gBAAgB,GAAG"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EXTENSION_CONTENT_TYPES, FILES_MAX_READ_SIZE } from "../../connectors/files/defaults.js";
|
|
2
|
+
import { FILES_DOWNLOAD_DEFAULTS, FILES_MAX_UPLOAD_SIZE, FILES_READ_DEFAULTS, FILES_WRITE_DEFAULTS } from "./defaults.js";
|
|
3
|
+
import { FilesPlugin, files } from "./plugin.js";
|
|
4
|
+
|
|
5
|
+
export { };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/plugins/files/manifest.json
|
|
2
|
+
var manifest_default = {
|
|
3
|
+
$schema: "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
|
|
4
|
+
name: "files",
|
|
5
|
+
displayName: "Files Plugin",
|
|
6
|
+
description: "File operations against Databricks Volumes and Unity Catalog",
|
|
7
|
+
resources: {
|
|
8
|
+
"required": [{
|
|
9
|
+
"type": "volume",
|
|
10
|
+
"alias": "Files",
|
|
11
|
+
"resourceKey": "files",
|
|
12
|
+
"description": "Permission to write to volumes",
|
|
13
|
+
"permission": "WRITE_VOLUME",
|
|
14
|
+
"fields": { "path": {
|
|
15
|
+
"env": "DATABRICKS_VOLUME_FILES",
|
|
16
|
+
"description": "Volume path for file storage (e.g. /Volumes/catalog/schema/volume_name)"
|
|
17
|
+
} }
|
|
18
|
+
}],
|
|
19
|
+
"optional": []
|
|
20
|
+
},
|
|
21
|
+
config: { "schema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"timeout": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"default": 3e4,
|
|
27
|
+
"description": "File operation timeout in milliseconds"
|
|
28
|
+
},
|
|
29
|
+
"maxUploadSize": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"default": 5368709120,
|
|
32
|
+
"description": "Maximum upload size in bytes. Defaults to 5 GB (Databricks Files API v2 limit)."
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { manifest_default as default };
|
|
40
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","names":[],"sources":["../../../src/plugins/files/manifest.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
2
|
+
import "../../shared/src/index.js";
|
|
3
|
+
import { Plugin } from "../../plugin/plugin.js";
|
|
4
|
+
import "../../plugin/index.js";
|
|
5
|
+
import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
|
|
6
|
+
import "../../registry/index.js";
|
|
7
|
+
import { FilesExport, IFilesConfig, VolumeAPI, VolumeConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
//#region src/plugins/files/plugin.d.ts
|
|
10
|
+
declare class FilesPlugin extends Plugin {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Plugin manifest declaring metadata and resource requirements. */
|
|
13
|
+
static manifest: PluginManifest;
|
|
14
|
+
protected static description: string;
|
|
15
|
+
protected config: IFilesConfig;
|
|
16
|
+
private volumeConnectors;
|
|
17
|
+
private volumeConfigs;
|
|
18
|
+
private volumeKeys;
|
|
19
|
+
/**
|
|
20
|
+
* Scans `process.env` for `DATABRICKS_VOLUME_*` keys and merges them with
|
|
21
|
+
* any explicitly configured volumes. Explicit config wins for per-volume
|
|
22
|
+
* overrides; auto-discovered volumes get default `{}` config.
|
|
23
|
+
*/
|
|
24
|
+
static discoverVolumes(config: IFilesConfig): Record<string, VolumeConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* Generates resource requirements dynamically from discovered + configured volumes.
|
|
27
|
+
* Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.
|
|
28
|
+
*/
|
|
29
|
+
static getResourceRequirements(config: IFilesConfig): ResourceRequirement[];
|
|
30
|
+
/**
|
|
31
|
+
* Warns when a method is called without a user context (i.e. as service principal).
|
|
32
|
+
* OBO access via `asUser(req)` is strongly recommended.
|
|
33
|
+
*/
|
|
34
|
+
private warnIfNoUserContext;
|
|
35
|
+
/**
|
|
36
|
+
* Throws when a method is called without a user context (i.e. as service principal).
|
|
37
|
+
* OBO access via `asUser(req)` is enforced for now.
|
|
38
|
+
*/
|
|
39
|
+
private throwIfNoUserContext;
|
|
40
|
+
constructor(config: IFilesConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Creates a VolumeAPI for a specific volume key.
|
|
43
|
+
* Each method warns if called outside a user context (service principal).
|
|
44
|
+
*/
|
|
45
|
+
protected createVolumeAPI(volumeKey: string): VolumeAPI;
|
|
46
|
+
injectRoutes(router: IAppRouter): void;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `:volumeKey` from the request. Returns the connector and key,
|
|
49
|
+
* or sends a 404 and returns `{ connector: undefined }`.
|
|
50
|
+
*/
|
|
51
|
+
private _resolveVolume;
|
|
52
|
+
/**
|
|
53
|
+
* Validate a file/directory path from user input.
|
|
54
|
+
* Returns `true` if valid, or an error message string if invalid.
|
|
55
|
+
*/
|
|
56
|
+
private _isValidPath;
|
|
57
|
+
private _readSettings;
|
|
58
|
+
/**
|
|
59
|
+
* Invalidate cached list entries for a directory after a write operation.
|
|
60
|
+
* Uses the same cache-key format as `_handleList`: resolved path for
|
|
61
|
+
* subdirectories, `"__root__"` for the volume root.
|
|
62
|
+
*/
|
|
63
|
+
private _invalidateListCache;
|
|
64
|
+
private _handleApiError;
|
|
65
|
+
private _handleList;
|
|
66
|
+
private _handleRead;
|
|
67
|
+
private _handleDownload;
|
|
68
|
+
private _handleRaw;
|
|
69
|
+
/**
|
|
70
|
+
* Shared handler for `/download` and `/raw` endpoints.
|
|
71
|
+
* - `download`: always forces `Content-Disposition: attachment`.
|
|
72
|
+
* - `raw`: adds CSP sandbox; forces attachment only for unsafe content types.
|
|
73
|
+
*/
|
|
74
|
+
private _serveFile;
|
|
75
|
+
private _handleExists;
|
|
76
|
+
private _handleMetadata;
|
|
77
|
+
private _handlePreview;
|
|
78
|
+
private _handleUpload;
|
|
79
|
+
private _handleMkdir;
|
|
80
|
+
private _handleDelete;
|
|
81
|
+
private inflightWrites;
|
|
82
|
+
private trackWrite;
|
|
83
|
+
shutdown(): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Returns the programmatic API for the Files plugin.
|
|
86
|
+
* Callable with a volume key to get a volume-scoped handle.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* // OBO access (recommended)
|
|
91
|
+
* appKit.files("uploads").asUser(req).list()
|
|
92
|
+
*
|
|
93
|
+
* // Service principal access (logs a warning)
|
|
94
|
+
* appKit.files("uploads").list()
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
exports(): FilesExport;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
declare const files: ToPlugin<typeof FilesPlugin, IFilesConfig, string>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { FilesPlugin, files };
|
|
105
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;cAmCa,WAAA,SAAoB,MAAA;EAC/B,IAAA;;SAGO,QAAA,EAAuB,cAAA;EAAA,iBACb,WAAA;EAAA,UACC,MAAA,EAAQ,YAAA;EAAA,QAElB,gBAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAJkB;;;;;EAAA,OAWnB,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,MAAA,SAAe,YAAA;EAkEzC;;;;EAAA,OA3Cb,uBAAA,CAAwB,MAAA,EAAQ,YAAA,GAAe,mBAAA;EAxCvB;;;;EAAA,QA8DvB,mBAAA;EA1DD;;;;EAAA,QAuEC,oBAAA;cAQI,MAAA,EAAQ,YAAA;EA1EZ;;;;EAAA,UA+GE,eAAA,CAAgB,SAAA,WAAoB,SAAA;EAmD9C,YAAA,CAAa,MAAA,EAAQ,UAAA;EA1JwC;;;;EAAA,QAwRrD,cAAA;EA3OA;;;;EAAA,QAkQA,YAAA;EAAA,QAQA,aAAA;EAhNkB;;;;;EAAA,QAgOlB,oBAAA;EAAA,QAiBA,eAAA;EAAA,QA8BM,WAAA;EAAA,QA+BA,WAAA;EAAA,QAoCA,eAAA;EAAA,QAWA,UAAA;EA/CA;;;;;EAAA,QA+DA,UAAA;EAAA,QAgFA,aAAA;EAAA,QAsCA,eAAA;EAAA,QAsCA,cAAA;EAAA,QAoCA,aAAA;EAAA,QA2GA,YAAA;EAAA,QA+CA,aAAA;EAAA,QA6CN,cAAA;EAAA,QAEA,UAAA;EAOF,QAAA,CAAA,GAAY,OAAA;EAgCP;;;AA+Bb;;;;;;;;;;EA/BE,OAAA,CAAA,GAAW,WAAA;AAAA;;;;cA+BA,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA"}
|