@databricks/appkit 0.19.0 → 0.20.0
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 +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/storage/defaults.js +1 -0
- package/dist/cache/storage/defaults.js.map +1 -1
- package/dist/cache/storage/persistent.js +45 -21
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/connectors/genie/client.js +18 -6
- package/dist/connectors/genie/client.js.map +1 -1
- package/dist/plugins/lakebase/manifest.js +48 -2
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js +1 -0
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/registry/types.d.ts +11 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.generated.d.ts +4 -1
- package/dist/registry/types.generated.d.ts.map +1 -1
- package/dist/registry/types.generated.js +2 -0
- package/dist/registry/types.generated.js.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/schemas/plugin-manifest.schema.json +42 -9
- package/dist/shared/src/plugin.d.ts +11 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/docs/api/appkit/Enumeration.ResourceType.md +9 -0
- package/docs/api/appkit/Interface.ResourceFieldEntry.md +57 -2
- package/docs/api/appkit/TypeAlias.ResourcePermission.md +1 -0
- package/docs/plugins/lakebase.md +120 -113
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -46,7 +46,7 @@ npx @databricks/appkit docs <query>
|
|
|
46
46
|
- [Execution context](./docs/plugins/execution-context.md): AppKit manages Databricks authentication via two contexts:
|
|
47
47
|
- [Files plugin](./docs/plugins/files.md): File operations against Databricks Unity Catalog Volumes. Supports listing, reading, downloading, uploading, deleting, and previewing files with built-in caching, retry, and timeout handling via the execution interceptor pipeline.
|
|
48
48
|
- [Genie plugin](./docs/plugins/genie.md): Integrates Databricks AI/BI Genie spaces into your AppKit application, enabling natural language data queries via a conversational interface.
|
|
49
|
-
- [Lakebase plugin](./docs/plugins/lakebase.md):
|
|
49
|
+
- [Lakebase plugin](./docs/plugins/lakebase.md): Provides a PostgreSQL connection pool for Databricks Lakebase Autoscaling with automatic OAuth token refresh.
|
|
50
50
|
- [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
|
|
51
51
|
- [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
|
|
52
52
|
|
package/dist/appkit/package.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/cache/storage/defaults.ts"],"sourcesContent":["/** Default configuration for in-memory storage */\nexport const inMemoryStorageDefaults = {\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n};\n\n/** Default configuration for Lakebase storage */\nexport const lakebaseStorageDefaults = {\n /** Table name for the cache */\n tableName: \"appkit_cache_entries\",\n /** Maximum number of bytes in the cache */\n maxBytes: 256 * 1024 * 1024, // 256MB\n /** Maximum number of bytes per entry in the cache */\n maxEntryBytes: 10 * 1024 * 1024, // 10MB\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n /** Number of entries to evict when cache is full */\n evictionBatchSize: 100,\n /** Probability (0-1) of checking total bytes on each write operation */\n evictionCheckProbability: 0.1,\n};\n"],"mappings":";;AACA,MAAa,0BAA0B,EAErC,SAAS,KACV;;AAGD,MAAa,0BAA0B;CAErC,WAAW;CAEX,UAAU,MAAM,OAAO;CAEvB,eAAe,KAAK,OAAO;CAE3B,SAAS;CAET,mBAAmB;CAEnB,0BAA0B;CAC3B"}
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/cache/storage/defaults.ts"],"sourcesContent":["/** Default configuration for in-memory storage */\nexport const inMemoryStorageDefaults = {\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n};\n\n/** Default configuration for Lakebase storage */\nexport const lakebaseStorageDefaults = {\n /** Schema name for the cache tables */\n schemaName: \"appkit\",\n /** Table name for the cache */\n tableName: \"appkit_cache_entries\",\n /** Maximum number of bytes in the cache */\n maxBytes: 256 * 1024 * 1024, // 256MB\n /** Maximum number of bytes per entry in the cache */\n maxEntryBytes: 10 * 1024 * 1024, // 10MB\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n /** Number of entries to evict when cache is full */\n evictionBatchSize: 100,\n /** Probability (0-1) of checking total bytes on each write operation */\n evictionCheckProbability: 0.1,\n};\n"],"mappings":";;AACA,MAAa,0BAA0B,EAErC,SAAS,KACV;;AAGD,MAAa,0BAA0B;CAErC,YAAY;CAEZ,WAAW;CAEX,UAAU,MAAM,OAAO;CAEvB,eAAe,KAAK,OAAO;CAE3B,SAAS;CAET,mBAAmB;CAEnB,0BAA0B;CAC3B"}
|
|
@@ -25,7 +25,9 @@ const logger = createLogger("cache:persistent");
|
|
|
25
25
|
*/
|
|
26
26
|
var PersistentStorage = class {
|
|
27
27
|
pool;
|
|
28
|
+
schemaName;
|
|
28
29
|
tableName;
|
|
30
|
+
qualifiedTableName;
|
|
29
31
|
maxBytes;
|
|
30
32
|
maxEntryBytes;
|
|
31
33
|
evictionBatchSize;
|
|
@@ -37,7 +39,9 @@ var PersistentStorage = class {
|
|
|
37
39
|
this.maxEntryBytes = config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;
|
|
38
40
|
this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;
|
|
39
41
|
this.evictionCheckProbability = config.evictionCheckProbability ?? lakebaseStorageDefaults.evictionCheckProbability;
|
|
42
|
+
this.schemaName = lakebaseStorageDefaults.schemaName;
|
|
40
43
|
this.tableName = lakebaseStorageDefaults.tableName;
|
|
44
|
+
this.qualifiedTableName = `${this.schemaName}.${this.tableName}`;
|
|
41
45
|
this.initialized = false;
|
|
42
46
|
}
|
|
43
47
|
/** Initialize the persistent storage and run migrations if necessary */
|
|
@@ -59,10 +63,10 @@ var PersistentStorage = class {
|
|
|
59
63
|
async get(key) {
|
|
60
64
|
await this.ensureInitialized();
|
|
61
65
|
const keyHash = this.hashKey(key);
|
|
62
|
-
const result = await this.pool.query(`SELECT value, expiry FROM ${this.
|
|
66
|
+
const result = await this.pool.query(`SELECT value, expiry FROM ${this.qualifiedTableName} WHERE key_hash = $1`, [keyHash]);
|
|
63
67
|
if (result.rows.length === 0) return null;
|
|
64
68
|
const entry = result.rows[0];
|
|
65
|
-
this.pool.query(`UPDATE ${this.
|
|
69
|
+
this.pool.query(`UPDATE ${this.qualifiedTableName} SET last_accessed = NOW() WHERE key_hash = $1`, [keyHash]).catch(() => {
|
|
66
70
|
logger.debug("Error updating last_accessed time for key: %s", key);
|
|
67
71
|
});
|
|
68
72
|
return {
|
|
@@ -86,7 +90,7 @@ var PersistentStorage = class {
|
|
|
86
90
|
if (Math.random() < this.evictionCheckProbability) {
|
|
87
91
|
if (await this.totalBytes() + byteSize > this.maxBytes) await this.evictBySize(byteSize);
|
|
88
92
|
}
|
|
89
|
-
await this.pool.query(`INSERT INTO ${this.
|
|
93
|
+
await this.pool.query(`INSERT INTO ${this.qualifiedTableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)
|
|
90
94
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
|
91
95
|
ON CONFLICT (key_hash)
|
|
92
96
|
DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()
|
|
@@ -106,12 +110,12 @@ var PersistentStorage = class {
|
|
|
106
110
|
async delete(key) {
|
|
107
111
|
await this.ensureInitialized();
|
|
108
112
|
const keyHash = this.hashKey(key);
|
|
109
|
-
await this.pool.query(`DELETE FROM ${this.
|
|
113
|
+
await this.pool.query(`DELETE FROM ${this.qualifiedTableName} WHERE key_hash = $1`, [keyHash]);
|
|
110
114
|
}
|
|
111
115
|
/** Clear the persistent storage */
|
|
112
116
|
async clear() {
|
|
113
117
|
await this.ensureInitialized();
|
|
114
|
-
await this.pool.query(`TRUNCATE TABLE ${this.
|
|
118
|
+
await this.pool.query(`TRUNCATE TABLE ${this.qualifiedTableName}`);
|
|
115
119
|
}
|
|
116
120
|
/**
|
|
117
121
|
* Check if a value exists in the persistent storage
|
|
@@ -121,7 +125,7 @@ var PersistentStorage = class {
|
|
|
121
125
|
async has(key) {
|
|
122
126
|
await this.ensureInitialized();
|
|
123
127
|
const keyHash = this.hashKey(key);
|
|
124
|
-
return (await this.pool.query(`SELECT EXISTS(SELECT 1 FROM ${this.
|
|
128
|
+
return (await this.pool.query(`SELECT EXISTS(SELECT 1 FROM ${this.qualifiedTableName} WHERE key_hash = $1) as exists`, [keyHash])).rows[0]?.exists ?? false;
|
|
125
129
|
}
|
|
126
130
|
/**
|
|
127
131
|
* Get the size of the persistent storage
|
|
@@ -129,13 +133,13 @@ var PersistentStorage = class {
|
|
|
129
133
|
*/
|
|
130
134
|
async size() {
|
|
131
135
|
await this.ensureInitialized();
|
|
132
|
-
const result = await this.pool.query(`SELECT COUNT(*) as count FROM ${this.
|
|
136
|
+
const result = await this.pool.query(`SELECT COUNT(*) as count FROM ${this.qualifiedTableName}`);
|
|
133
137
|
return parseInt(result.rows[0]?.count ?? "0", 10);
|
|
134
138
|
}
|
|
135
139
|
/** Get the total number of bytes in the persistent storage */
|
|
136
140
|
async totalBytes() {
|
|
137
141
|
await this.ensureInitialized();
|
|
138
|
-
const result = await this.pool.query(`SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.
|
|
142
|
+
const result = await this.pool.query(`SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.qualifiedTableName}`);
|
|
139
143
|
return parseInt(result.rows[0]?.total ?? "0", 10);
|
|
140
144
|
}
|
|
141
145
|
/**
|
|
@@ -167,7 +171,7 @@ var PersistentStorage = class {
|
|
|
167
171
|
*/
|
|
168
172
|
async cleanupExpired() {
|
|
169
173
|
await this.ensureInitialized();
|
|
170
|
-
const result = await this.pool.query(`WITH deleted as (DELETE FROM ${this.
|
|
174
|
+
const result = await this.pool.query(`WITH deleted as (DELETE FROM ${this.qualifiedTableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`, [Date.now()]);
|
|
171
175
|
return parseInt(result.rows[0]?.count ?? "0", 10);
|
|
172
176
|
}
|
|
173
177
|
/** Evict entries from the persistent storage by size */
|
|
@@ -175,8 +179,8 @@ var PersistentStorage = class {
|
|
|
175
179
|
if (await this.cleanupExpired() > 0) {
|
|
176
180
|
if (await this.totalBytes() + requiredBytes <= this.maxBytes) return;
|
|
177
181
|
}
|
|
178
|
-
await this.pool.query(`DELETE FROM ${this.
|
|
179
|
-
(SELECT key_hash FROM ${this.
|
|
182
|
+
await this.pool.query(`DELETE FROM ${this.qualifiedTableName} WHERE key_hash IN
|
|
183
|
+
(SELECT key_hash FROM ${this.qualifiedTableName} ORDER BY last_accessed ASC LIMIT $1)`, [this.evictionBatchSize]);
|
|
180
184
|
}
|
|
181
185
|
/** Ensure the persistent storage is initialized */
|
|
182
186
|
async ensureInitialized() {
|
|
@@ -197,9 +201,14 @@ var PersistentStorage = class {
|
|
|
197
201
|
}
|
|
198
202
|
/** Run migrations for the persistent storage */
|
|
199
203
|
async runMigrations() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
const steps = [
|
|
205
|
+
{
|
|
206
|
+
name: "create schema",
|
|
207
|
+
query: `CREATE SCHEMA IF NOT EXISTS ${this.schemaName}`
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "create table",
|
|
211
|
+
query: `CREATE TABLE IF NOT EXISTS ${this.qualifiedTableName} (
|
|
203
212
|
id BIGSERIAL PRIMARY KEY,
|
|
204
213
|
key_hash BIGINT NOT NULL,
|
|
205
214
|
key BYTEA NOT NULL,
|
|
@@ -208,14 +217,29 @@ var PersistentStorage = class {
|
|
|
208
217
|
expiry BIGINT NOT NULL,
|
|
209
218
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
210
219
|
last_accessed TIMESTAMP NOT NULL DEFAULT NOW()
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
)`
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "create index (key_hash)",
|
|
224
|
+
query: `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.qualifiedTableName} (key_hash)`
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "create index (expiry)",
|
|
228
|
+
query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.qualifiedTableName} (expiry)`
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "create index (last_accessed)",
|
|
232
|
+
query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.qualifiedTableName} (last_accessed)`
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "create index (byte_size)",
|
|
236
|
+
query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.qualifiedTableName} (byte_size)`
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
for (const step of steps) try {
|
|
240
|
+
await this.pool.query(step.query);
|
|
217
241
|
} catch (error) {
|
|
218
|
-
logger.error("
|
|
242
|
+
logger.error("Migration step '%s' failed: %O", step.name, error);
|
|
219
243
|
throw InitializationError.migrationFailed(error);
|
|
220
244
|
}
|
|
221
245
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistent.js","names":[],"sources":["../../../src/cache/storage/persistent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type pg from \"pg\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { InitializationError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { lakebaseStorageDefaults } from \"./defaults\";\n\nconst logger = createLogger(\"cache:persistent\");\n\n/**\n * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n *\n * @example\n * const pool = createLakebasePool({ workspaceClient });\n * const persistentStorage = new PersistentStorage(config, pool);\n * await persistentStorage.initialize();\n * await persistentStorage.get(\"my-key\");\n * await persistentStorage.set(\"my-key\", \"my-value\");\n * await persistentStorage.delete(\"my-key\");\n * await persistentStorage.clear();\n * await persistentStorage.has(\"my-key\");\n *\n */\nexport class PersistentStorage implements CacheStorage {\n private readonly pool: pg.Pool;\n private readonly tableName: string;\n private readonly maxBytes: number;\n private readonly maxEntryBytes: number;\n private readonly evictionBatchSize: number;\n private readonly evictionCheckProbability: number;\n private initialized: boolean;\n\n constructor(config: CacheConfig, pool: pg.Pool) {\n this.pool = pool;\n this.maxBytes = config.maxBytes ?? lakebaseStorageDefaults.maxBytes;\n this.maxEntryBytes =\n config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;\n this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;\n this.evictionCheckProbability =\n config.evictionCheckProbability ??\n lakebaseStorageDefaults.evictionCheckProbability;\n this.tableName = lakebaseStorageDefaults.tableName; // hardcoded, safe for now\n this.initialized = false;\n }\n\n /** Initialize the persistent storage and run migrations if necessary */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n await this.runMigrations();\n this.initialized = true;\n } catch (error) {\n logger.error(\"Error in persistent storage initialization: %O\", error);\n throw error;\n }\n }\n\n /**\n * Get a cached value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the cached value or null if not found\n */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n\n const result = await this.pool.query<{\n value: Buffer;\n expiry: string;\n }>(`SELECT value, expiry FROM ${this.tableName} WHERE key_hash = $1`, [\n keyHash,\n ]);\n\n if (result.rows.length === 0) return null;\n\n const entry = result.rows[0];\n\n // fire-and-forget update\n this.pool\n .query(\n `UPDATE ${this.tableName} SET last_accessed = NOW() WHERE key_hash = $1`,\n [keyHash],\n )\n .catch(() => {\n logger.debug(\"Error updating last_accessed time for key: %s\", key);\n });\n\n return {\n value: this.deserializeValue<T>(entry.value),\n expiry: Number(entry.expiry),\n };\n }\n\n /**\n * Set a value in the persistent storage\n * @param key - Cache key\n * @param entry - Cache entry\n * @returns Promise of the result\n */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n const keyBytes = Buffer.from(key, \"utf-8\");\n const valueBytes = this.serializeValue(entry.value);\n const byteSize = keyBytes.length + valueBytes.length;\n\n if (byteSize > this.maxEntryBytes) {\n throw ValidationError.invalidValue(\n \"cache entry size\",\n byteSize,\n `maximum ${this.maxEntryBytes} bytes`,\n );\n }\n\n // probabilistic eviction check\n if (Math.random() < this.evictionCheckProbability) {\n const totalBytes = await this.totalBytes();\n if (totalBytes + byteSize > this.maxBytes) {\n await this.evictBySize(byteSize);\n }\n }\n\n await this.pool.query(\n `INSERT INTO ${this.tableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)\n VALUES ($1, $2, $3, $4, $5, NOW(), NOW())\n ON CONFLICT (key_hash)\n DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()\n `,\n [keyHash, keyBytes, valueBytes, byteSize, entry.expiry],\n );\n }\n\n /**\n * Delete a value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n await this.pool.query(`DELETE FROM ${this.tableName} WHERE key_hash = $1`, [\n keyHash,\n ]);\n }\n\n /** Clear the persistent storage */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.pool.query(`TRUNCATE TABLE ${this.tableName}`);\n }\n\n /**\n * Check if a value exists in the persistent storage\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n\n const result = await this.pool.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.tableName} WHERE key_hash = $1) as exists`,\n [keyHash],\n );\n\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * Get the size of the persistent storage\n * @returns Promise of the size of the storage\n */\n async size(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.pool.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Get the total number of bytes in the persistent storage */\n async totalBytes(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.pool.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.total ?? \"0\", 10);\n }\n\n /**\n * Check if the persistent storage is persistent\n * @returns true if the storage is persistent, false otherwise\n */\n isPersistent(): boolean {\n return true;\n }\n\n /**\n * Check if the persistent storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async healthCheck(): Promise<boolean> {\n try {\n await this.pool.query(\"SELECT 1\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.pool.end();\n }\n\n /**\n * Cleanup expired entries from the persistent storage\n * @returns Promise of the number of expired entries\n */\n async cleanupExpired(): Promise<number> {\n await this.ensureInitialized();\n const result = await this.pool.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.tableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`,\n [Date.now()],\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Evict entries from the persistent storage by size */\n private async evictBySize(requiredBytes: number): Promise<void> {\n const freedByExpiry = await this.cleanupExpired();\n if (freedByExpiry > 0) {\n const currentBytes = await this.totalBytes();\n if (currentBytes + requiredBytes <= this.maxBytes) {\n return;\n }\n }\n\n await this.pool.query(\n `DELETE FROM ${this.tableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.tableName} ORDER BY last_accessed ASC LIMIT $1)`,\n [this.evictionBatchSize],\n );\n }\n\n /** Ensure the persistent storage is initialized */\n private async ensureInitialized(): Promise<void> {\n if (!this.initialized) {\n await this.initialize();\n }\n }\n\n /** Generate a 64-bit hash for the cache key using SHA256 */\n private hashKey(key: string): bigint {\n if (!key) throw ValidationError.missingField(\"key\");\n const hash = createHash(\"sha256\").update(key).digest();\n return hash.readBigInt64BE(0);\n }\n\n /** Serialize a value to a buffer */\n private serializeValue<T>(value: T): Buffer {\n return Buffer.from(JSON.stringify(value), \"utf-8\");\n }\n\n /** Deserialize a value from a buffer */\n private deserializeValue<T>(buffer: Buffer): T {\n return JSON.parse(buffer.toString(\"utf-8\")) as T;\n }\n\n /** Run migrations for the persistent storage */\n private async runMigrations(): Promise<void> {\n try {\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id BIGSERIAL PRIMARY KEY,\n key_hash BIGINT NOT NULL,\n key BYTEA NOT NULL,\n value BYTEA NOT NULL,\n byte_size INTEGER NOT NULL,\n expiry BIGINT NOT NULL,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n last_accessed TIMESTAMP NOT NULL DEFAULT NOW()\n )\n `);\n\n // unique index on key_hash for fast lookups\n await this.pool.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.tableName} (key_hash);`,\n );\n\n // index on expiry for cleanup queries\n await this.pool.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.tableName} (expiry); `,\n );\n\n // index on last_accessed for LRU eviction\n await this.pool.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.tableName} (last_accessed); `,\n );\n\n // index on byte_size for monitoring\n await this.pool.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.tableName} (byte_size); `,\n );\n } catch (error) {\n logger.error(\n \"Error in running migrations for persistent storage: %O\",\n error,\n );\n throw InitializationError.migrationFailed(error as Error);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"persistent.js","names":[],"sources":["../../../src/cache/storage/persistent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type pg from \"pg\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { InitializationError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { lakebaseStorageDefaults } from \"./defaults\";\n\nconst logger = createLogger(\"cache:persistent\");\n\n/**\n * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n *\n * @example\n * const pool = createLakebasePool({ workspaceClient });\n * const persistentStorage = new PersistentStorage(config, pool);\n * await persistentStorage.initialize();\n * await persistentStorage.get(\"my-key\");\n * await persistentStorage.set(\"my-key\", \"my-value\");\n * await persistentStorage.delete(\"my-key\");\n * await persistentStorage.clear();\n * await persistentStorage.has(\"my-key\");\n *\n */\nexport class PersistentStorage implements CacheStorage {\n private readonly pool: pg.Pool;\n private readonly schemaName: string;\n private readonly tableName: string;\n private readonly qualifiedTableName: string;\n private readonly maxBytes: number;\n private readonly maxEntryBytes: number;\n private readonly evictionBatchSize: number;\n private readonly evictionCheckProbability: number;\n private initialized: boolean;\n\n constructor(config: CacheConfig, pool: pg.Pool) {\n this.pool = pool;\n this.maxBytes = config.maxBytes ?? lakebaseStorageDefaults.maxBytes;\n this.maxEntryBytes =\n config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;\n this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;\n this.evictionCheckProbability =\n config.evictionCheckProbability ??\n lakebaseStorageDefaults.evictionCheckProbability;\n this.schemaName = lakebaseStorageDefaults.schemaName;\n this.tableName = lakebaseStorageDefaults.tableName;\n this.qualifiedTableName = `${this.schemaName}.${this.tableName}`;\n this.initialized = false;\n }\n\n /** Initialize the persistent storage and run migrations if necessary */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n await this.runMigrations();\n this.initialized = true;\n } catch (error) {\n logger.error(\"Error in persistent storage initialization: %O\", error);\n throw error;\n }\n }\n\n /**\n * Get a cached value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the cached value or null if not found\n */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n\n const result = await this.pool.query<{\n value: Buffer;\n expiry: string;\n }>(\n `SELECT value, expiry FROM ${this.qualifiedTableName} WHERE key_hash = $1`,\n [keyHash],\n );\n\n if (result.rows.length === 0) return null;\n\n const entry = result.rows[0];\n\n // fire-and-forget update\n this.pool\n .query(\n `UPDATE ${this.qualifiedTableName} SET last_accessed = NOW() WHERE key_hash = $1`,\n [keyHash],\n )\n .catch(() => {\n logger.debug(\"Error updating last_accessed time for key: %s\", key);\n });\n\n return {\n value: this.deserializeValue<T>(entry.value),\n expiry: Number(entry.expiry),\n };\n }\n\n /**\n * Set a value in the persistent storage\n * @param key - Cache key\n * @param entry - Cache entry\n * @returns Promise of the result\n */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n const keyBytes = Buffer.from(key, \"utf-8\");\n const valueBytes = this.serializeValue(entry.value);\n const byteSize = keyBytes.length + valueBytes.length;\n\n if (byteSize > this.maxEntryBytes) {\n throw ValidationError.invalidValue(\n \"cache entry size\",\n byteSize,\n `maximum ${this.maxEntryBytes} bytes`,\n );\n }\n\n // probabilistic eviction check\n if (Math.random() < this.evictionCheckProbability) {\n const totalBytes = await this.totalBytes();\n if (totalBytes + byteSize > this.maxBytes) {\n await this.evictBySize(byteSize);\n }\n }\n\n await this.pool.query(\n `INSERT INTO ${this.qualifiedTableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)\n VALUES ($1, $2, $3, $4, $5, NOW(), NOW())\n ON CONFLICT (key_hash)\n DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()\n `,\n [keyHash, keyBytes, valueBytes, byteSize, entry.expiry],\n );\n }\n\n /**\n * Delete a value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n await this.pool.query(\n `DELETE FROM ${this.qualifiedTableName} WHERE key_hash = $1`,\n [keyHash],\n );\n }\n\n /** Clear the persistent storage */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.pool.query(`TRUNCATE TABLE ${this.qualifiedTableName}`);\n }\n\n /**\n * Check if a value exists in the persistent storage\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n\n const result = await this.pool.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.qualifiedTableName} WHERE key_hash = $1) as exists`,\n [keyHash],\n );\n\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * Get the size of the persistent storage\n * @returns Promise of the size of the storage\n */\n async size(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.pool.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.qualifiedTableName}`,\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Get the total number of bytes in the persistent storage */\n async totalBytes(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.pool.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.qualifiedTableName}`,\n );\n return parseInt(result.rows[0]?.total ?? \"0\", 10);\n }\n\n /**\n * Check if the persistent storage is persistent\n * @returns true if the storage is persistent, false otherwise\n */\n isPersistent(): boolean {\n return true;\n }\n\n /**\n * Check if the persistent storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async healthCheck(): Promise<boolean> {\n try {\n await this.pool.query(\"SELECT 1\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.pool.end();\n }\n\n /**\n * Cleanup expired entries from the persistent storage\n * @returns Promise of the number of expired entries\n */\n async cleanupExpired(): Promise<number> {\n await this.ensureInitialized();\n const result = await this.pool.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.qualifiedTableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`,\n [Date.now()],\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Evict entries from the persistent storage by size */\n private async evictBySize(requiredBytes: number): Promise<void> {\n const freedByExpiry = await this.cleanupExpired();\n if (freedByExpiry > 0) {\n const currentBytes = await this.totalBytes();\n if (currentBytes + requiredBytes <= this.maxBytes) {\n return;\n }\n }\n\n await this.pool.query(\n `DELETE FROM ${this.qualifiedTableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.qualifiedTableName} ORDER BY last_accessed ASC LIMIT $1)`,\n [this.evictionBatchSize],\n );\n }\n\n /** Ensure the persistent storage is initialized */\n private async ensureInitialized(): Promise<void> {\n if (!this.initialized) {\n await this.initialize();\n }\n }\n\n /** Generate a 64-bit hash for the cache key using SHA256 */\n private hashKey(key: string): bigint {\n if (!key) throw ValidationError.missingField(\"key\");\n const hash = createHash(\"sha256\").update(key).digest();\n return hash.readBigInt64BE(0);\n }\n\n /** Serialize a value to a buffer */\n private serializeValue<T>(value: T): Buffer {\n return Buffer.from(JSON.stringify(value), \"utf-8\");\n }\n\n /** Deserialize a value from a buffer */\n private deserializeValue<T>(buffer: Buffer): T {\n return JSON.parse(buffer.toString(\"utf-8\")) as T;\n }\n\n /** Run migrations for the persistent storage */\n private async runMigrations(): Promise<void> {\n const steps = [\n {\n name: \"create schema\",\n query: `CREATE SCHEMA IF NOT EXISTS ${this.schemaName}`,\n },\n {\n name: \"create table\",\n query: `CREATE TABLE IF NOT EXISTS ${this.qualifiedTableName} (\n id BIGSERIAL PRIMARY KEY,\n key_hash BIGINT NOT NULL,\n key BYTEA NOT NULL,\n value BYTEA NOT NULL,\n byte_size INTEGER NOT NULL,\n expiry BIGINT NOT NULL,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n last_accessed TIMESTAMP NOT NULL DEFAULT NOW()\n )`,\n },\n {\n name: \"create index (key_hash)\",\n query: `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.qualifiedTableName} (key_hash)`,\n },\n {\n name: \"create index (expiry)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.qualifiedTableName} (expiry)`,\n },\n {\n name: \"create index (last_accessed)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.qualifiedTableName} (last_accessed)`,\n },\n {\n name: \"create index (byte_size)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.qualifiedTableName} (byte_size)`,\n },\n ];\n\n for (const step of steps) {\n try {\n await this.pool.query(step.query);\n } catch (error) {\n logger.error(\"Migration step '%s' failed: %O\", step.name, error);\n throw InitializationError.migrationFailed(error as Error);\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -8,6 +8,12 @@ import * as SDK from "@databricks/sdk-experimental";
|
|
|
8
8
|
const { TimeUnits } = SDK;
|
|
9
9
|
const Time = SDK.Time ?? SDK.default.Time;
|
|
10
10
|
const logger = createLogger("connectors:genie");
|
|
11
|
+
const GenieErrors = {
|
|
12
|
+
SPACE_ACCESS_DENIED: "You don't have access to this Genie Space.",
|
|
13
|
+
TABLE_PERMISSIONS: "You may not have access to the data tables. Please verify your table permissions.",
|
|
14
|
+
REQUEST_FAILED: "Genie request failed",
|
|
15
|
+
QUERY_RESULT_FAILED: "Failed to fetch query result"
|
|
16
|
+
};
|
|
11
17
|
function mapAttachments(message) {
|
|
12
18
|
return message.attachments?.map((att) => ({
|
|
13
19
|
attachmentId: att.attachment_id,
|
|
@@ -32,6 +38,12 @@ function toMessageResponse(message) {
|
|
|
32
38
|
error: message.error?.error
|
|
33
39
|
};
|
|
34
40
|
}
|
|
41
|
+
function classifyGenieError(error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
if (message.includes("RESOURCE_DOES_NOT_EXIST")) return GenieErrors.SPACE_ACCESS_DENIED;
|
|
44
|
+
if (message.includes("failed to reach COMPLETED state") && message.includes("FAILED")) return GenieErrors.TABLE_PERMISSIONS;
|
|
45
|
+
return message || GenieErrors.REQUEST_FAILED;
|
|
46
|
+
}
|
|
35
47
|
var GenieConnector = class {
|
|
36
48
|
config;
|
|
37
49
|
constructor(config = {}) {
|
|
@@ -113,10 +125,10 @@ var GenieConnector = class {
|
|
|
113
125
|
};
|
|
114
126
|
yield* this.emitQueryResults(workspaceClient, spaceId, resultConversationId, messageResponse.messageId, messageResponse);
|
|
115
127
|
} catch (error) {
|
|
116
|
-
logger.error("Genie message error: %O", error);
|
|
128
|
+
logger.error("Genie message error (spaceId=%s, conversationId=%s): %O", spaceId, conversationId ?? "new", error);
|
|
117
129
|
yield {
|
|
118
130
|
type: "error",
|
|
119
|
-
error: error
|
|
131
|
+
error: classifyGenieError(error)
|
|
120
132
|
};
|
|
121
133
|
}
|
|
122
134
|
}
|
|
@@ -136,7 +148,7 @@ var GenieConnector = class {
|
|
|
136
148
|
logger.error("Failed to fetch query result for attachment %s: %O", att.attachmentId, error);
|
|
137
149
|
yield {
|
|
138
150
|
type: "error",
|
|
139
|
-
error:
|
|
151
|
+
error: `${GenieErrors.QUERY_RESULT_FAILED} for attachment ${att.attachmentId}`
|
|
140
152
|
};
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -184,15 +196,15 @@ var GenieConnector = class {
|
|
|
184
196
|
logger.error("Failed to fetch query result: %O", result.reason);
|
|
185
197
|
yield {
|
|
186
198
|
type: "error",
|
|
187
|
-
error: result.reason instanceof Error ? result.reason.message :
|
|
199
|
+
error: result.reason instanceof Error ? result.reason.message : GenieErrors.QUERY_RESULT_FAILED
|
|
188
200
|
};
|
|
189
201
|
}
|
|
190
202
|
}
|
|
191
203
|
} catch (error) {
|
|
192
|
-
logger.error("Genie getConversation error: %O", error);
|
|
204
|
+
logger.error("Genie getConversation error (spaceId=%s, conversationId=%s): %O", spaceId, conversationId, error);
|
|
193
205
|
yield {
|
|
194
206
|
type: "error",
|
|
195
|
-
error: error
|
|
207
|
+
error: classifyGenieError(error)
|
|
196
208
|
};
|
|
197
209
|
}
|
|
198
210
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/genie/client.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport * as SDK from \"@databricks/sdk-experimental\";\nimport type { GenieMessage } from \"@databricks/sdk-experimental/dist/apis/dashboards\";\nimport type { Waiter } from \"@databricks/sdk-experimental/dist/wait\";\nimport { createLogger } from \"../../logging\";\nimport { genieConnectorDefaults } from \"./defaults\";\nimport { pollWaiter } from \"./poll-waiter\";\nimport type {\n GenieAttachmentResponse,\n GenieConversationHistoryResponse,\n GenieMessageResponse,\n GenieStatementResponse,\n GenieStreamEvent,\n} from \"./types\";\n\nconst { TimeUnits } = SDK;\nconst Time = SDK.Time ?? (SDK as any).default.Time;\n\nconst logger = createLogger(\"connectors:genie\");\n\ntype CreateMessageWaiter = Waiter<GenieMessage, GenieMessage>;\n\nexport interface GenieConnectorConfig {\n timeout?: number;\n maxMessages?: number;\n}\n\nfunction mapAttachments(message: GenieMessage): GenieAttachmentResponse[] {\n return (\n message.attachments?.map((att) => ({\n attachmentId: att.attachment_id,\n query: att.query\n ? {\n title: att.query.title,\n description: att.query.description,\n query: att.query.query,\n statementId: att.query.statement_id,\n }\n : undefined,\n text: att.text ? { content: att.text.content } : undefined,\n suggestedQuestions: att.suggested_questions?.questions,\n })) ?? []\n );\n}\n\nfunction toMessageResponse(message: GenieMessage): GenieMessageResponse {\n return {\n messageId: message.message_id,\n conversationId: message.conversation_id,\n spaceId: message.space_id,\n status: message.status ?? \"COMPLETED\",\n content: message.content,\n attachments: mapAttachments(message),\n error: message.error?.error,\n };\n}\n\nexport class GenieConnector {\n private readonly config: Required<GenieConnectorConfig>;\n\n constructor(config: GenieConnectorConfig = {}) {\n this.config = {\n timeout: config.timeout ?? genieConnectorDefaults.timeout,\n maxMessages: config.maxMessages ?? genieConnectorDefaults.maxMessages,\n };\n }\n\n async startMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<{\n messageWaiter: CreateMessageWaiter;\n conversationId: string;\n messageId: string;\n }> {\n if (conversationId) {\n const waiter = await workspaceClient.genie.createMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n content,\n });\n return {\n messageWaiter: waiter,\n conversationId,\n messageId: waiter.message_id ?? \"\",\n };\n }\n const start = await workspaceClient.genie.startConversation({\n space_id: spaceId,\n content,\n });\n return {\n messageWaiter: start as unknown as CreateMessageWaiter,\n conversationId: start.conversation_id,\n messageId: start.message_id,\n };\n }\n\n async waitForMessage(\n messageWaiter: CreateMessageWaiter,\n options?: { timeout?: number },\n ): Promise<GenieMessage> {\n const timeout = options?.timeout ?? this.config.timeout;\n const waitOptions =\n timeout > 0 ? { timeout: new Time(timeout, TimeUnits.milliseconds) } : {};\n return messageWaiter.wait(waitOptions);\n }\n\n async listConversationMessages(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: { pageSize?: number; pageToken?: string },\n ): Promise<{\n messages: GenieMessageResponse[];\n nextPageToken: string | null;\n }> {\n const pageSize =\n options?.pageSize ?? genieConnectorDefaults.initialPageSize;\n\n const response = await workspaceClient.genie.listConversationMessages({\n space_id: spaceId,\n conversation_id: conversationId,\n page_size: pageSize,\n ...(options?.pageToken ? { page_token: options.pageToken } : {}),\n });\n\n const messages = (response.messages ?? []).reverse().map(toMessageResponse);\n\n return {\n messages,\n nextPageToken: response.next_page_token ?? null,\n };\n }\n\n async getMessageAttachmentQueryResult(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n attachmentId: string,\n _signal?: AbortSignal,\n ): Promise<GenieStatementResponse> {\n const response =\n await workspaceClient.genie.getMessageAttachmentQueryResult({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n attachment_id: attachmentId,\n });\n return response.statement_response as GenieStatementResponse;\n }\n\n async *streamSendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n options?: { timeout?: number },\n ): AsyncGenerator<GenieStreamEvent> {\n try {\n const {\n messageWaiter,\n conversationId: resultConversationId,\n messageId: resultMessageId,\n } = await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n\n yield {\n type: \"message_start\",\n conversationId: resultConversationId,\n messageId: resultMessageId,\n spaceId,\n };\n\n const timeout =\n options?.timeout != null ? options.timeout : this.config.timeout;\n const waitOptions =\n timeout > 0\n ? { timeout: new Time(timeout, TimeUnits.milliseconds) }\n : {};\n\n let completedMessage!: GenieMessage;\n for await (const event of pollWaiter(messageWaiter, waitOptions)) {\n if (event.type === \"progress\" && event.value.status) {\n yield { type: \"status\", status: event.value.status };\n } else if (event.type === \"completed\") {\n completedMessage = event.value;\n }\n }\n\n const messageResponse = toMessageResponse(completedMessage);\n yield { type: \"message_result\", message: messageResponse };\n\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n resultConversationId,\n messageResponse.messageId,\n messageResponse,\n );\n } catch (error) {\n logger.error(\"Genie message error: %O\", error);\n yield {\n type: \"error\",\n error: error instanceof Error ? error.message : \"Genie request failed\",\n };\n }\n }\n\n private async *emitQueryResults(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n messageResponse: GenieMessageResponse,\n ): AsyncGenerator<\n Extract<GenieStreamEvent, { type: \"query_result\" } | { type: \"error\" }>\n > {\n const attachments = messageResponse.attachments ?? [];\n for (const att of attachments) {\n if (!att.query?.statementId || !att.attachmentId) continue;\n try {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n att.attachmentId,\n );\n yield {\n type: \"query_result\",\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n data,\n };\n } catch (error) {\n logger.error(\n \"Failed to fetch query result for attachment %s: %O\",\n att.attachmentId,\n error,\n );\n yield {\n type: \"error\",\n error: `Failed to fetch query result for attachment ${att.attachmentId}`,\n };\n }\n }\n }\n\n async *streamConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: {\n includeQueryResults?: boolean;\n pageSize?: number;\n pageToken?: string;\n },\n ): AsyncGenerator<GenieStreamEvent> {\n const includeQueryResults = options?.includeQueryResults !== false;\n\n try {\n const { messages: messageResponses, nextPageToken } =\n await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n { pageSize: options?.pageSize, pageToken: options?.pageToken },\n );\n\n for (const messageResponse of messageResponses) {\n yield { type: \"message_result\", message: messageResponse };\n }\n\n yield {\n type: \"history_info\",\n conversationId,\n spaceId,\n nextPageToken,\n loadedCount: messageResponses.length,\n };\n\n if (includeQueryResults) {\n const queryAttachments: Array<{\n messageId: string;\n attachmentId: string;\n statementId: string;\n }> = [];\n\n for (const msg of messageResponses) {\n for (const att of msg.attachments ?? []) {\n if (att.query?.statementId && att.attachmentId) {\n queryAttachments.push({\n messageId: msg.messageId,\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n });\n }\n }\n }\n\n const results = await Promise.allSettled(\n queryAttachments.map(async (att) => {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n att.messageId,\n att.attachmentId,\n );\n return {\n attachmentId: att.attachmentId,\n statementId: att.statementId,\n data,\n };\n }),\n );\n\n for (const result of results) {\n if (result.status === \"fulfilled\") {\n yield {\n type: \"query_result\",\n attachmentId: result.value.attachmentId,\n statementId: result.value.statementId,\n data: result.value.data,\n };\n } else {\n logger.error(\"Failed to fetch query result: %O\", result.reason);\n yield {\n type: \"error\",\n error:\n result.reason instanceof Error\n ? result.reason.message\n : \"Failed to fetch query result\",\n };\n }\n }\n }\n } catch (error) {\n logger.error(\"Genie getConversation error: %O\", error);\n yield {\n type: \"error\",\n error:\n error instanceof Error\n ? error.message\n : \"Failed to fetch conversation\",\n };\n }\n }\n\n async sendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<GenieMessageResponse> {\n const { messageWaiter, conversationId: resultConversationId } =\n await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n const completedMessage = await this.waitForMessage(messageWaiter);\n const messageResponse = toMessageResponse(completedMessage);\n return {\n ...messageResponse,\n conversationId: resultConversationId,\n };\n }\n\n async getConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const allMessages: GenieMessageResponse[] = [];\n let pageToken: string | undefined;\n\n do {\n const { messages, nextPageToken } = await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n {\n pageSize: genieConnectorDefaults.pageSize,\n pageToken,\n },\n );\n allMessages.push(...messages);\n pageToken = nextPageToken ?? undefined;\n } while (pageToken && allMessages.length < this.config.maxMessages);\n\n return {\n conversationId,\n spaceId,\n messages: allMessages.slice(0, this.config.maxMessages),\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/genie/client.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport * as SDK from \"@databricks/sdk-experimental\";\nimport type { GenieMessage } from \"@databricks/sdk-experimental/dist/apis/dashboards\";\nimport type { Waiter } from \"@databricks/sdk-experimental/dist/wait\";\nimport { createLogger } from \"../../logging\";\nimport { genieConnectorDefaults } from \"./defaults\";\nimport { pollWaiter } from \"./poll-waiter\";\nimport type {\n GenieAttachmentResponse,\n GenieConversationHistoryResponse,\n GenieMessageResponse,\n GenieStatementResponse,\n GenieStreamEvent,\n} from \"./types\";\n\nconst { TimeUnits } = SDK;\nconst Time = SDK.Time ?? (SDK as any).default.Time;\n\nconst logger = createLogger(\"connectors:genie\");\n\nconst GenieErrors = {\n SPACE_ACCESS_DENIED: \"You don't have access to this Genie Space.\",\n TABLE_PERMISSIONS:\n \"You may not have access to the data tables. Please verify your table permissions.\",\n REQUEST_FAILED: \"Genie request failed\",\n QUERY_RESULT_FAILED: \"Failed to fetch query result\",\n} as const;\n\ntype CreateMessageWaiter = Waiter<GenieMessage, GenieMessage>;\n\nexport interface GenieConnectorConfig {\n timeout?: number;\n maxMessages?: number;\n}\n\nfunction mapAttachments(message: GenieMessage): GenieAttachmentResponse[] {\n return (\n message.attachments?.map((att) => ({\n attachmentId: att.attachment_id,\n query: att.query\n ? {\n title: att.query.title,\n description: att.query.description,\n query: att.query.query,\n statementId: att.query.statement_id,\n }\n : undefined,\n text: att.text ? { content: att.text.content } : undefined,\n suggestedQuestions: att.suggested_questions?.questions,\n })) ?? []\n );\n}\n\nfunction toMessageResponse(message: GenieMessage): GenieMessageResponse {\n return {\n messageId: message.message_id,\n conversationId: message.conversation_id,\n spaceId: message.space_id,\n status: message.status ?? \"COMPLETED\",\n content: message.content,\n attachments: mapAttachments(message),\n error: message.error?.error,\n };\n}\n\nfunction classifyGenieError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n\n if (message.includes(\"RESOURCE_DOES_NOT_EXIST\")) {\n return GenieErrors.SPACE_ACCESS_DENIED;\n }\n\n if (\n message.includes(\"failed to reach COMPLETED state\") &&\n message.includes(\"FAILED\")\n ) {\n return GenieErrors.TABLE_PERMISSIONS;\n }\n\n return message || GenieErrors.REQUEST_FAILED;\n}\n\nexport class GenieConnector {\n private readonly config: Required<GenieConnectorConfig>;\n\n constructor(config: GenieConnectorConfig = {}) {\n this.config = {\n timeout: config.timeout ?? genieConnectorDefaults.timeout,\n maxMessages: config.maxMessages ?? genieConnectorDefaults.maxMessages,\n };\n }\n\n async startMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<{\n messageWaiter: CreateMessageWaiter;\n conversationId: string;\n messageId: string;\n }> {\n if (conversationId) {\n const waiter = await workspaceClient.genie.createMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n content,\n });\n return {\n messageWaiter: waiter,\n conversationId,\n messageId: waiter.message_id ?? \"\",\n };\n }\n const start = await workspaceClient.genie.startConversation({\n space_id: spaceId,\n content,\n });\n return {\n messageWaiter: start as unknown as CreateMessageWaiter,\n conversationId: start.conversation_id,\n messageId: start.message_id,\n };\n }\n\n async waitForMessage(\n messageWaiter: CreateMessageWaiter,\n options?: { timeout?: number },\n ): Promise<GenieMessage> {\n const timeout = options?.timeout ?? this.config.timeout;\n const waitOptions =\n timeout > 0 ? { timeout: new Time(timeout, TimeUnits.milliseconds) } : {};\n return messageWaiter.wait(waitOptions);\n }\n\n async listConversationMessages(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: { pageSize?: number; pageToken?: string },\n ): Promise<{\n messages: GenieMessageResponse[];\n nextPageToken: string | null;\n }> {\n const pageSize =\n options?.pageSize ?? genieConnectorDefaults.initialPageSize;\n\n const response = await workspaceClient.genie.listConversationMessages({\n space_id: spaceId,\n conversation_id: conversationId,\n page_size: pageSize,\n ...(options?.pageToken ? { page_token: options.pageToken } : {}),\n });\n\n const messages = (response.messages ?? []).reverse().map(toMessageResponse);\n\n return {\n messages,\n nextPageToken: response.next_page_token ?? null,\n };\n }\n\n async getMessageAttachmentQueryResult(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n attachmentId: string,\n _signal?: AbortSignal,\n ): Promise<GenieStatementResponse> {\n const response =\n await workspaceClient.genie.getMessageAttachmentQueryResult({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n attachment_id: attachmentId,\n });\n return response.statement_response as GenieStatementResponse;\n }\n\n async *streamSendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n options?: { timeout?: number },\n ): AsyncGenerator<GenieStreamEvent> {\n try {\n const {\n messageWaiter,\n conversationId: resultConversationId,\n messageId: resultMessageId,\n } = await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n\n yield {\n type: \"message_start\",\n conversationId: resultConversationId,\n messageId: resultMessageId,\n spaceId,\n };\n\n const timeout =\n options?.timeout != null ? options.timeout : this.config.timeout;\n const waitOptions =\n timeout > 0\n ? { timeout: new Time(timeout, TimeUnits.milliseconds) }\n : {};\n\n let completedMessage!: GenieMessage;\n for await (const event of pollWaiter(messageWaiter, waitOptions)) {\n if (event.type === \"progress\" && event.value.status) {\n yield { type: \"status\", status: event.value.status };\n } else if (event.type === \"completed\") {\n completedMessage = event.value;\n }\n }\n\n const messageResponse = toMessageResponse(completedMessage);\n yield { type: \"message_result\", message: messageResponse };\n\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n resultConversationId,\n messageResponse.messageId,\n messageResponse,\n );\n } catch (error) {\n logger.error(\n \"Genie message error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId ?? \"new\",\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n private async *emitQueryResults(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n messageResponse: GenieMessageResponse,\n ): AsyncGenerator<\n Extract<GenieStreamEvent, { type: \"query_result\" } | { type: \"error\" }>\n > {\n const attachments = messageResponse.attachments ?? [];\n for (const att of attachments) {\n if (!att.query?.statementId || !att.attachmentId) continue;\n try {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n att.attachmentId,\n );\n yield {\n type: \"query_result\",\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n data,\n };\n } catch (error) {\n logger.error(\n \"Failed to fetch query result for attachment %s: %O\",\n att.attachmentId,\n error,\n );\n yield {\n type: \"error\",\n error: `${GenieErrors.QUERY_RESULT_FAILED} for attachment ${att.attachmentId}`,\n };\n }\n }\n }\n\n async *streamConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: {\n includeQueryResults?: boolean;\n pageSize?: number;\n pageToken?: string;\n },\n ): AsyncGenerator<GenieStreamEvent> {\n const includeQueryResults = options?.includeQueryResults !== false;\n\n try {\n const { messages: messageResponses, nextPageToken } =\n await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n { pageSize: options?.pageSize, pageToken: options?.pageToken },\n );\n\n for (const messageResponse of messageResponses) {\n yield { type: \"message_result\", message: messageResponse };\n }\n\n yield {\n type: \"history_info\",\n conversationId,\n spaceId,\n nextPageToken,\n loadedCount: messageResponses.length,\n };\n\n if (includeQueryResults) {\n const queryAttachments: Array<{\n messageId: string;\n attachmentId: string;\n statementId: string;\n }> = [];\n\n for (const msg of messageResponses) {\n for (const att of msg.attachments ?? []) {\n if (att.query?.statementId && att.attachmentId) {\n queryAttachments.push({\n messageId: msg.messageId,\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n });\n }\n }\n }\n\n const results = await Promise.allSettled(\n queryAttachments.map(async (att) => {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n att.messageId,\n att.attachmentId,\n );\n return {\n attachmentId: att.attachmentId,\n statementId: att.statementId,\n data,\n };\n }),\n );\n\n for (const result of results) {\n if (result.status === \"fulfilled\") {\n yield {\n type: \"query_result\",\n attachmentId: result.value.attachmentId,\n statementId: result.value.statementId,\n data: result.value.data,\n };\n } else {\n logger.error(\"Failed to fetch query result: %O\", result.reason);\n yield {\n type: \"error\",\n error:\n result.reason instanceof Error\n ? result.reason.message\n : GenieErrors.QUERY_RESULT_FAILED,\n };\n }\n }\n }\n } catch (error) {\n logger.error(\n \"Genie getConversation error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId,\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n async sendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<GenieMessageResponse> {\n const { messageWaiter, conversationId: resultConversationId } =\n await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n const completedMessage = await this.waitForMessage(messageWaiter);\n const messageResponse = toMessageResponse(completedMessage);\n return {\n ...messageResponse,\n conversationId: resultConversationId,\n };\n }\n\n async getConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const allMessages: GenieMessageResponse[] = [];\n let pageToken: string | undefined;\n\n do {\n const { messages, nextPageToken } = await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n {\n pageSize: genieConnectorDefaults.pageSize,\n pageToken,\n },\n );\n allMessages.push(...messages);\n pageToken = nextPageToken ?? undefined;\n } while (pageToken && allMessages.length < this.config.maxMessages);\n\n return {\n conversationId,\n spaceId,\n messages: allMessages.slice(0, this.config.maxMessages),\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,10 +4,56 @@ var manifest_default = {
|
|
|
4
4
|
name: "lakebase",
|
|
5
5
|
displayName: "Lakebase",
|
|
6
6
|
description: "SQL query execution against Databricks Lakebase Autoscaling",
|
|
7
|
-
onSetupMessage: "Configure environment variables before running or deploying the app.\nSee: https://databricks.github.io/appkit/docs/plugins/lakebase",
|
|
8
7
|
hidden: false,
|
|
9
8
|
resources: {
|
|
10
|
-
"required": [
|
|
9
|
+
"required": [{
|
|
10
|
+
"type": "postgres",
|
|
11
|
+
"alias": "Postgres",
|
|
12
|
+
"resourceKey": "postgres",
|
|
13
|
+
"description": "Lakebase Postgres database for persistent storage",
|
|
14
|
+
"permission": "CAN_CONNECT_AND_CREATE",
|
|
15
|
+
"fields": {
|
|
16
|
+
"branch": {
|
|
17
|
+
"description": "Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches projects/{project-id}`, select the desired item from the output array and use its .name value.",
|
|
18
|
+
"examples": ["projects/{project-id}/branches/{branch-id}"]
|
|
19
|
+
},
|
|
20
|
+
"database": {
|
|
21
|
+
"description": "Full Lakebase Postgres database resource name. Obtain by running `databricks postgres list-databases {branch-name}`, select the desired item from the output array and use its .name value. Requires the branch resource name.",
|
|
22
|
+
"examples": ["projects/{project-id}/branches/{branch-id}/databases/{database-id}"]
|
|
23
|
+
},
|
|
24
|
+
"host": {
|
|
25
|
+
"env": "PGHOST",
|
|
26
|
+
"localOnly": true,
|
|
27
|
+
"resolve": "postgres:host",
|
|
28
|
+
"description": "Postgres host for local development. Auto-injected by the platform at deploy time."
|
|
29
|
+
},
|
|
30
|
+
"databaseName": {
|
|
31
|
+
"env": "PGDATABASE",
|
|
32
|
+
"localOnly": true,
|
|
33
|
+
"resolve": "postgres:databaseName",
|
|
34
|
+
"description": "Postgres database name for local development. Auto-injected by the platform at deploy time."
|
|
35
|
+
},
|
|
36
|
+
"endpointPath": {
|
|
37
|
+
"env": "LAKEBASE_ENDPOINT",
|
|
38
|
+
"bundleIgnore": true,
|
|
39
|
+
"resolve": "postgres:endpointPath",
|
|
40
|
+
"description": "Lakebase endpoint resource name. Auto-injected at runtime via app.yaml valueFrom: postgres. For local development, obtain by running `databricks postgres list-endpoints {branch-name}`, select the desired item from the output array and use its .name value.",
|
|
41
|
+
"examples": ["projects/{project-id}/branches/{branch-id}/endpoints/{endpoint-id}"]
|
|
42
|
+
},
|
|
43
|
+
"port": {
|
|
44
|
+
"env": "PGPORT",
|
|
45
|
+
"localOnly": true,
|
|
46
|
+
"value": "5432",
|
|
47
|
+
"description": "Postgres port. Auto-injected by the platform at deploy time."
|
|
48
|
+
},
|
|
49
|
+
"sslmode": {
|
|
50
|
+
"env": "PGSSLMODE",
|
|
51
|
+
"localOnly": true,
|
|
52
|
+
"value": "require",
|
|
53
|
+
"description": "Postgres SSL mode. Auto-injected by the platform at deploy time."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}],
|
|
11
57
|
"optional": []
|
|
12
58
|
}
|
|
13
59
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"mappings":";;;;;;;;;cAqDa,gBAAA;EAAA,QACH,SAAA;
|
|
1
|
+
{"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"mappings":";;;;;;;;;cAqDa,gBAAA;EAAA,QACH,SAAA;EAiYsC;;;;;;;;;;;;EAnXvC,QAAA,CAAS,MAAA,UAAgB,QAAA,EAAU,mBAAA;EAiKnC;;;;;;;EAtIA,gBAAA,CACL,UAAA,EAAY,UAAA,CAAW,iBAAA;EAyKlB;;;;EAAA,QAjIC,cAAA;EAqJD;;;;;;EAxDA,MAAA,CAAA,GAAU,aAAA;EA6L6B;;;;;;;EAlLvC,GAAA,CAAI,IAAA,UAAc,WAAA,WAAsB,aAAA;;;;;EAQxC,KAAA,CAAA;;;;EAOA,IAAA,CAAA;;;;;;;EAUA,WAAA,CAAY,UAAA,WAAqB,aAAA;;;;;;EAWjC,WAAA,CAAA,GAAe,aAAA;;;;;;EASf,WAAA,CAAA,GAAe,aAAA;;;;;;;;;;;;;;;;;;;;;;EAyBf,QAAA,CAAA,GAAY,gBAAA;;;;;;;;;;;;EA+DZ,iBAAA,CAAA,GAAqB,gBAAA;;;;;;;SA6Cd,sBAAA,CAAuB,OAAA,EAAS,aAAA;;;;;;;;SAqBhC,sBAAA,CAAuB,OAAA,EAAS,aAAA;AAAA"}
|
|
@@ -203,6 +203,7 @@ var ResourceRegistry = class ResourceRegistry {
|
|
|
203
203
|
const values = {};
|
|
204
204
|
let allSet = true;
|
|
205
205
|
for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {
|
|
206
|
+
if (!fieldDef.env) continue;
|
|
206
207
|
const val = process.env[fieldDef.env];
|
|
207
208
|
if (val !== void 0 && val !== "") values[fieldName] = val;
|
|
208
209
|
else allSet = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-registry.js","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":["/**\n * Resource Registry\n *\n * Central registry that tracks all resource requirements across all plugins.\n * Provides visibility into Databricks resources needed by the application\n * and handles deduplication when multiple plugins require the same resource\n * (dedup key: type + resourceKey).\n *\n * Use `new ResourceRegistry()` for instance-scoped usage (e.g. createApp).\n * getInstance() / resetInstance() remain for backward compatibility in tests.\n */\n\nimport type { BasePluginConfig, PluginConstructor, PluginData } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { getPluginManifest } from \"./manifest-loader\";\nimport type {\n ResourceEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./types\";\nimport { PERMISSION_HIERARCHY_BY_TYPE, type ResourceType } from \"./types\";\n\nconst logger = createLogger(\"resource-registry\");\n\n/**\n * Dedup key for registry: type + resourceKey (machine-stable).\n * alias is for UI/display only.\n */\nfunction getDedupKey(type: string, resourceKey: string): string {\n return `${type}:${resourceKey}`;\n}\n\n/**\n * Returns the most permissive permission for a given resource type.\n * Uses per-type hierarchy; unknown permissions are treated as least permissive.\n */\nfunction getMostPermissivePermission(\n resourceType: ResourceType,\n p1: ResourcePermission,\n p2: ResourcePermission,\n): ResourcePermission {\n const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType as ResourceType];\n const index1 = hierarchy?.indexOf(p1) ?? -1;\n const index2 = hierarchy?.indexOf(p2) ?? -1;\n return index1 > index2 ? p1 : p2;\n}\n\n/**\n * Central registry for tracking plugin resource requirements.\n * Deduplication uses type + resourceKey (machine-stable); alias is for display only.\n */\nexport class ResourceRegistry {\n private resources: Map<string, ResourceEntry> = new Map();\n\n /**\n * Registers a resource requirement for a plugin.\n * If a resource with the same type+resourceKey already exists, merges them:\n * - Combines plugin names (comma-separated)\n * - Uses the most permissive permission (per-type hierarchy)\n * - Marks as required if any plugin requires it\n * - Combines descriptions if they differ\n * - Merges fields; warns when same field name uses different env vars\n *\n * @param plugin - Name of the plugin registering the resource\n * @param resource - Resource requirement specification\n */\n public register(plugin: string, resource: ResourceRequirement): void {\n const key = getDedupKey(resource.type, resource.resourceKey);\n const existing = this.resources.get(key);\n\n if (existing) {\n // Merge with existing resource\n const merged = this.mergeResources(existing, plugin, resource);\n this.resources.set(key, merged);\n } else {\n // Create new resource entry with permission source tracking\n const entry: ResourceEntry = {\n ...resource,\n plugin,\n resolved: false,\n permissionSources: { [plugin]: resource.permission },\n };\n this.resources.set(key, entry);\n }\n }\n\n /**\n * Collects and registers resource requirements from an array of plugins.\n * For each plugin, loads its manifest (required) and runtime resource requirements.\n *\n * @param rawPlugins - Array of plugin data entries from createApp configuration\n * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid\n */\n public collectResources(\n rawPlugins: PluginData<PluginConstructor, unknown, string>[],\n ): void {\n for (const pluginData of rawPlugins) {\n if (!pluginData?.plugin) continue;\n\n const pluginName = pluginData.name;\n const manifest = getPluginManifest(pluginData.plugin);\n\n // Register required resources\n for (const resource of manifest.resources.required) {\n this.register(pluginName, { ...resource, required: true });\n }\n\n // Register optional resources\n for (const resource of manifest.resources.optional || []) {\n this.register(pluginName, { ...resource, required: false });\n }\n\n // Check for runtime resource requirements\n if (typeof pluginData.plugin.getResourceRequirements === \"function\") {\n const runtimeResources = pluginData.plugin.getResourceRequirements(\n pluginData.config as BasePluginConfig,\n );\n for (const resource of runtimeResources) {\n this.register(pluginName, resource as ResourceRequirement);\n }\n }\n\n logger.debug(\n \"Collected resources from plugin %s: %d total\",\n pluginName,\n this.getByPlugin(pluginName).length,\n );\n }\n }\n\n /**\n * Merges a new resource requirement with an existing entry.\n * Applies intelligent merging logic for conflicting properties.\n */\n private mergeResources(\n existing: ResourceEntry,\n newPlugin: string,\n newResource: ResourceRequirement,\n ): ResourceEntry {\n // Combine plugin names if not already included\n const plugins = existing.plugin.split(\", \");\n if (!plugins.includes(newPlugin)) {\n plugins.push(newPlugin);\n }\n\n // Track per-plugin permission sources\n const permissionSources: Record<string, ResourcePermission> = {\n ...(existing.permissionSources ?? {}),\n [newPlugin]: newResource.permission,\n };\n\n // Use the most permissive permission for this resource type; warn when escalating\n const permission = getMostPermissivePermission(\n existing.type as ResourceType,\n existing.permission,\n newResource.permission,\n );\n\n if (permission !== existing.permission) {\n logger.warn(\n 'Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" ' +\n \"(previously requested by: %s). Review plugin permissions to ensure least-privilege.\",\n existing.type,\n existing.resourceKey,\n existing.permission,\n permission,\n newPlugin,\n existing.plugin,\n );\n }\n\n // Mark as required if any plugin requires it\n const required = existing.required || newResource.required;\n\n // Combine descriptions if they differ\n let description = existing.description;\n if (\n newResource.description &&\n newResource.description !== existing.description\n ) {\n if (!existing.description.includes(newResource.description)) {\n description = `${existing.description}; ${newResource.description}`;\n }\n }\n\n // Merge fields: union of field names; warn when same field name uses different env\n const fields = { ...(existing.fields ?? {}) };\n for (const [fieldName, newField] of Object.entries(\n newResource.fields ?? {},\n )) {\n const existingField = fields[fieldName];\n if (existingField) {\n if (existingField.env !== newField.env) {\n logger.warn(\n 'Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.',\n existing.type,\n existing.resourceKey,\n fieldName,\n existingField.env,\n existing.plugin,\n newField.env,\n newPlugin,\n );\n }\n // keep existing\n } else {\n fields[fieldName] = newField;\n }\n }\n\n return {\n ...existing,\n plugin: plugins.join(\", \"),\n permission,\n permissionSources,\n required,\n description,\n fields,\n };\n }\n\n /**\n * Retrieves all registered resources.\n * Returns a copy of the array to prevent external mutations.\n *\n * @returns Array of all registered resource entries\n */\n public getAll(): ResourceEntry[] {\n return Array.from(this.resources.values());\n }\n\n /**\n * Gets a specific resource by type and resourceKey (dedup key).\n *\n * @param type - Resource type\n * @param resourceKey - Stable machine key (not alias; alias is for display only)\n * @returns The resource entry if found, undefined otherwise\n */\n public get(type: string, resourceKey: string): ResourceEntry | undefined {\n return this.resources.get(getDedupKey(type, resourceKey));\n }\n\n /**\n * Clears all registered resources.\n * Useful for testing or when rebuilding the registry.\n */\n public clear(): void {\n this.resources.clear();\n }\n\n /**\n * Returns the number of registered resources.\n */\n public size(): number {\n return this.resources.size;\n }\n\n /**\n * Gets all resources required by a specific plugin.\n *\n * @param pluginName - Name of the plugin\n * @returns Array of resources where the plugin is listed as a requester\n */\n public getByPlugin(pluginName: string): ResourceEntry[] {\n return this.getAll().filter((entry) =>\n entry.plugin.split(\", \").includes(pluginName),\n );\n }\n\n /**\n * Gets all required resources (where required=true).\n *\n * @returns Array of required resource entries\n */\n public getRequired(): ResourceEntry[] {\n return this.getAll().filter((entry) => entry.required);\n }\n\n /**\n * Gets all optional resources (where required=false).\n *\n * @returns Array of optional resource entries\n */\n public getOptional(): ResourceEntry[] {\n return this.getAll().filter((entry) => !entry.required);\n }\n\n /**\n * Validates all registered resources against the environment.\n *\n * Checks each resource's field environment variables to determine if it's resolved.\n * Updates the `resolved` and `values` fields on each resource entry.\n *\n * Only required resources affect the `valid` status - optional resources\n * are checked but don't cause validation failure.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n *\n * @example\n * ```typescript\n * const registry = ResourceRegistry.getInstance();\n * const result = registry.validate();\n *\n * if (!result.valid) {\n * console.error(\"Missing resources:\", result.missing.map(r => Object.values(r.fields).map(f => f.env)));\n * }\n * ```\n */\n public validate(): ValidationResult {\n const missing: ResourceEntry[] = [];\n\n for (const entry of this.resources.values()) {\n const values: Record<string, string> = {};\n let allSet = true;\n for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {\n const val = process.env[fieldDef.env];\n if (val !== undefined && val !== \"\") {\n values[fieldName] = val;\n } else {\n allSet = false;\n }\n }\n if (allSet) {\n entry.resolved = true;\n entry.values = values;\n logger.debug(\n \"Resource %s:%s resolved from fields\",\n entry.type,\n entry.alias,\n );\n } else {\n entry.resolved = false;\n entry.values = Object.keys(values).length > 0 ? values : undefined;\n if (entry.required) {\n missing.push(entry);\n logger.debug(\n \"Required resource %s:%s missing (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n } else {\n logger.debug(\n \"Optional resource %s:%s not configured (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n }\n }\n }\n\n return {\n valid: missing.length === 0,\n missing,\n all: this.getAll(),\n };\n }\n\n /**\n * Validates all registered resources and enforces the result.\n *\n * - In production: throws a {@link ConfigurationError} if any required resources are missing.\n * - In development (`NODE_ENV=development`): logs a warning but continues, unless\n * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.\n * - When all resources are valid: logs a debug message with the count.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true\n */\n public enforceValidation(): ValidationResult {\n const validation = this.validate();\n const isDevelopment = process.env.NODE_ENV === \"development\";\n const strictValidation =\n process.env.APPKIT_STRICT_VALIDATION === \"true\" ||\n process.env.APPKIT_STRICT_VALIDATION === \"1\";\n\n if (!validation.valid) {\n const errorMessage = ResourceRegistry.formatMissingResources(\n validation.missing,\n );\n\n const shouldThrow = !isDevelopment || strictValidation;\n\n if (shouldThrow) {\n throw new ConfigurationError(errorMessage, {\n context: {\n missingResources: validation.missing.map((r) => ({\n type: r.type,\n alias: r.alias,\n plugin: r.plugin,\n envVars: Object.values(r.fields).map((f) => f.env),\n })),\n },\n });\n }\n\n // Dev mode without strict: use a visually prominent box so the warning can't be missed\n const banner = ResourceRegistry.formatDevWarningBanner(\n validation.missing,\n );\n logger.warn(\"\\n%s\", banner);\n } else if (this.size() > 0) {\n logger.debug(\"All %d resources validated successfully\", this.size());\n }\n\n return validation;\n }\n\n /**\n * Formats missing resources into a human-readable error message.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted error message string\n */\n public static formatMissingResources(missing: ResourceEntry[]): string {\n if (missing.length === 0) {\n return \"No missing resources\";\n }\n\n const lines = missing.map((entry) => {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n const envHint = ` (set ${envVars.join(\", \")})`;\n return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;\n });\n\n return `Missing required resources:\\n${lines.join(\"\\n\")}`;\n }\n\n /**\n * Formats a highly visible warning banner for dev-mode missing resources.\n * Uses box drawing to ensure the message is impossible to miss in scrolling logs.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted banner string\n */\n public static formatDevWarningBanner(missing: ResourceEntry[]): string {\n const contentLines: string[] = [\n \"MISSING REQUIRED RESOURCES (dev mode — would fail in production)\",\n \"\",\n ];\n\n for (const entry of missing) {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n const maxLen = Math.max(...contentLines.map((l) => l.length));\n const border = \"=\".repeat(maxLen + 4);\n\n const boxed = contentLines.map((line) => `| ${line.padEnd(maxLen)} |`);\n\n return [border, ...boxed, border].join(\"\\n\");\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resource-registry.js","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":["/**\n * Resource Registry\n *\n * Central registry that tracks all resource requirements across all plugins.\n * Provides visibility into Databricks resources needed by the application\n * and handles deduplication when multiple plugins require the same resource\n * (dedup key: type + resourceKey).\n *\n * Use `new ResourceRegistry()` for instance-scoped usage (e.g. createApp).\n * getInstance() / resetInstance() remain for backward compatibility in tests.\n */\n\nimport type { BasePluginConfig, PluginConstructor, PluginData } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { getPluginManifest } from \"./manifest-loader\";\nimport type {\n ResourceEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./types\";\nimport { PERMISSION_HIERARCHY_BY_TYPE, type ResourceType } from \"./types\";\n\nconst logger = createLogger(\"resource-registry\");\n\n/**\n * Dedup key for registry: type + resourceKey (machine-stable).\n * alias is for UI/display only.\n */\nfunction getDedupKey(type: string, resourceKey: string): string {\n return `${type}:${resourceKey}`;\n}\n\n/**\n * Returns the most permissive permission for a given resource type.\n * Uses per-type hierarchy; unknown permissions are treated as least permissive.\n */\nfunction getMostPermissivePermission(\n resourceType: ResourceType,\n p1: ResourcePermission,\n p2: ResourcePermission,\n): ResourcePermission {\n const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType as ResourceType];\n const index1 = hierarchy?.indexOf(p1) ?? -1;\n const index2 = hierarchy?.indexOf(p2) ?? -1;\n return index1 > index2 ? p1 : p2;\n}\n\n/**\n * Central registry for tracking plugin resource requirements.\n * Deduplication uses type + resourceKey (machine-stable); alias is for display only.\n */\nexport class ResourceRegistry {\n private resources: Map<string, ResourceEntry> = new Map();\n\n /**\n * Registers a resource requirement for a plugin.\n * If a resource with the same type+resourceKey already exists, merges them:\n * - Combines plugin names (comma-separated)\n * - Uses the most permissive permission (per-type hierarchy)\n * - Marks as required if any plugin requires it\n * - Combines descriptions if they differ\n * - Merges fields; warns when same field name uses different env vars\n *\n * @param plugin - Name of the plugin registering the resource\n * @param resource - Resource requirement specification\n */\n public register(plugin: string, resource: ResourceRequirement): void {\n const key = getDedupKey(resource.type, resource.resourceKey);\n const existing = this.resources.get(key);\n\n if (existing) {\n // Merge with existing resource\n const merged = this.mergeResources(existing, plugin, resource);\n this.resources.set(key, merged);\n } else {\n // Create new resource entry with permission source tracking\n const entry: ResourceEntry = {\n ...resource,\n plugin,\n resolved: false,\n permissionSources: { [plugin]: resource.permission },\n };\n this.resources.set(key, entry);\n }\n }\n\n /**\n * Collects and registers resource requirements from an array of plugins.\n * For each plugin, loads its manifest (required) and runtime resource requirements.\n *\n * @param rawPlugins - Array of plugin data entries from createApp configuration\n * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid\n */\n public collectResources(\n rawPlugins: PluginData<PluginConstructor, unknown, string>[],\n ): void {\n for (const pluginData of rawPlugins) {\n if (!pluginData?.plugin) continue;\n\n const pluginName = pluginData.name;\n const manifest = getPluginManifest(pluginData.plugin);\n\n // Register required resources\n for (const resource of manifest.resources.required) {\n this.register(pluginName, { ...resource, required: true });\n }\n\n // Register optional resources\n for (const resource of manifest.resources.optional || []) {\n this.register(pluginName, { ...resource, required: false });\n }\n\n // Check for runtime resource requirements\n if (typeof pluginData.plugin.getResourceRequirements === \"function\") {\n const runtimeResources = pluginData.plugin.getResourceRequirements(\n pluginData.config as BasePluginConfig,\n );\n for (const resource of runtimeResources) {\n this.register(pluginName, resource as ResourceRequirement);\n }\n }\n\n logger.debug(\n \"Collected resources from plugin %s: %d total\",\n pluginName,\n this.getByPlugin(pluginName).length,\n );\n }\n }\n\n /**\n * Merges a new resource requirement with an existing entry.\n * Applies intelligent merging logic for conflicting properties.\n */\n private mergeResources(\n existing: ResourceEntry,\n newPlugin: string,\n newResource: ResourceRequirement,\n ): ResourceEntry {\n // Combine plugin names if not already included\n const plugins = existing.plugin.split(\", \");\n if (!plugins.includes(newPlugin)) {\n plugins.push(newPlugin);\n }\n\n // Track per-plugin permission sources\n const permissionSources: Record<string, ResourcePermission> = {\n ...(existing.permissionSources ?? {}),\n [newPlugin]: newResource.permission,\n };\n\n // Use the most permissive permission for this resource type; warn when escalating\n const permission = getMostPermissivePermission(\n existing.type as ResourceType,\n existing.permission,\n newResource.permission,\n );\n\n if (permission !== existing.permission) {\n logger.warn(\n 'Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" ' +\n \"(previously requested by: %s). Review plugin permissions to ensure least-privilege.\",\n existing.type,\n existing.resourceKey,\n existing.permission,\n permission,\n newPlugin,\n existing.plugin,\n );\n }\n\n // Mark as required if any plugin requires it\n const required = existing.required || newResource.required;\n\n // Combine descriptions if they differ\n let description = existing.description;\n if (\n newResource.description &&\n newResource.description !== existing.description\n ) {\n if (!existing.description.includes(newResource.description)) {\n description = `${existing.description}; ${newResource.description}`;\n }\n }\n\n // Merge fields: union of field names; warn when same field name uses different env\n const fields = { ...(existing.fields ?? {}) };\n for (const [fieldName, newField] of Object.entries(\n newResource.fields ?? {},\n )) {\n const existingField = fields[fieldName];\n if (existingField) {\n if (existingField.env !== newField.env) {\n logger.warn(\n 'Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.',\n existing.type,\n existing.resourceKey,\n fieldName,\n existingField.env,\n existing.plugin,\n newField.env,\n newPlugin,\n );\n }\n // keep existing\n } else {\n fields[fieldName] = newField;\n }\n }\n\n return {\n ...existing,\n plugin: plugins.join(\", \"),\n permission,\n permissionSources,\n required,\n description,\n fields,\n };\n }\n\n /**\n * Retrieves all registered resources.\n * Returns a copy of the array to prevent external mutations.\n *\n * @returns Array of all registered resource entries\n */\n public getAll(): ResourceEntry[] {\n return Array.from(this.resources.values());\n }\n\n /**\n * Gets a specific resource by type and resourceKey (dedup key).\n *\n * @param type - Resource type\n * @param resourceKey - Stable machine key (not alias; alias is for display only)\n * @returns The resource entry if found, undefined otherwise\n */\n public get(type: string, resourceKey: string): ResourceEntry | undefined {\n return this.resources.get(getDedupKey(type, resourceKey));\n }\n\n /**\n * Clears all registered resources.\n * Useful for testing or when rebuilding the registry.\n */\n public clear(): void {\n this.resources.clear();\n }\n\n /**\n * Returns the number of registered resources.\n */\n public size(): number {\n return this.resources.size;\n }\n\n /**\n * Gets all resources required by a specific plugin.\n *\n * @param pluginName - Name of the plugin\n * @returns Array of resources where the plugin is listed as a requester\n */\n public getByPlugin(pluginName: string): ResourceEntry[] {\n return this.getAll().filter((entry) =>\n entry.plugin.split(\", \").includes(pluginName),\n );\n }\n\n /**\n * Gets all required resources (where required=true).\n *\n * @returns Array of required resource entries\n */\n public getRequired(): ResourceEntry[] {\n return this.getAll().filter((entry) => entry.required);\n }\n\n /**\n * Gets all optional resources (where required=false).\n *\n * @returns Array of optional resource entries\n */\n public getOptional(): ResourceEntry[] {\n return this.getAll().filter((entry) => !entry.required);\n }\n\n /**\n * Validates all registered resources against the environment.\n *\n * Checks each resource's field environment variables to determine if it's resolved.\n * Updates the `resolved` and `values` fields on each resource entry.\n *\n * Only required resources affect the `valid` status - optional resources\n * are checked but don't cause validation failure.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n *\n * @example\n * ```typescript\n * const registry = ResourceRegistry.getInstance();\n * const result = registry.validate();\n *\n * if (!result.valid) {\n * console.error(\"Missing resources:\", result.missing.map(r => Object.values(r.fields).map(f => f.env)));\n * }\n * ```\n */\n public validate(): ValidationResult {\n const missing: ResourceEntry[] = [];\n\n for (const entry of this.resources.values()) {\n const values: Record<string, string> = {};\n let allSet = true;\n for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {\n if (!fieldDef.env) continue;\n const val = process.env[fieldDef.env];\n if (val !== undefined && val !== \"\") {\n values[fieldName] = val;\n } else {\n allSet = false;\n }\n }\n if (allSet) {\n entry.resolved = true;\n entry.values = values;\n logger.debug(\n \"Resource %s:%s resolved from fields\",\n entry.type,\n entry.alias,\n );\n } else {\n entry.resolved = false;\n entry.values = Object.keys(values).length > 0 ? values : undefined;\n if (entry.required) {\n missing.push(entry);\n logger.debug(\n \"Required resource %s:%s missing (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n } else {\n logger.debug(\n \"Optional resource %s:%s not configured (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n }\n }\n }\n\n return {\n valid: missing.length === 0,\n missing,\n all: this.getAll(),\n };\n }\n\n /**\n * Validates all registered resources and enforces the result.\n *\n * - In production: throws a {@link ConfigurationError} if any required resources are missing.\n * - In development (`NODE_ENV=development`): logs a warning but continues, unless\n * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.\n * - When all resources are valid: logs a debug message with the count.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true\n */\n public enforceValidation(): ValidationResult {\n const validation = this.validate();\n const isDevelopment = process.env.NODE_ENV === \"development\";\n const strictValidation =\n process.env.APPKIT_STRICT_VALIDATION === \"true\" ||\n process.env.APPKIT_STRICT_VALIDATION === \"1\";\n\n if (!validation.valid) {\n const errorMessage = ResourceRegistry.formatMissingResources(\n validation.missing,\n );\n\n const shouldThrow = !isDevelopment || strictValidation;\n\n if (shouldThrow) {\n throw new ConfigurationError(errorMessage, {\n context: {\n missingResources: validation.missing.map((r) => ({\n type: r.type,\n alias: r.alias,\n plugin: r.plugin,\n envVars: Object.values(r.fields).map((f) => f.env),\n })),\n },\n });\n }\n\n // Dev mode without strict: use a visually prominent box so the warning can't be missed\n const banner = ResourceRegistry.formatDevWarningBanner(\n validation.missing,\n );\n logger.warn(\"\\n%s\", banner);\n } else if (this.size() > 0) {\n logger.debug(\"All %d resources validated successfully\", this.size());\n }\n\n return validation;\n }\n\n /**\n * Formats missing resources into a human-readable error message.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted error message string\n */\n public static formatMissingResources(missing: ResourceEntry[]): string {\n if (missing.length === 0) {\n return \"No missing resources\";\n }\n\n const lines = missing.map((entry) => {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n const envHint = ` (set ${envVars.join(\", \")})`;\n return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;\n });\n\n return `Missing required resources:\\n${lines.join(\"\\n\")}`;\n }\n\n /**\n * Formats a highly visible warning banner for dev-mode missing resources.\n * Uses box drawing to ensure the message is impossible to miss in scrolling logs.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted banner string\n */\n public static formatDevWarningBanner(missing: ResourceEntry[]): string {\n const contentLines: string[] = [\n \"MISSING REQUIRED RESOURCES (dev mode — would fail in production)\",\n \"\",\n ];\n\n for (const entry of missing) {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n const maxLen = Math.max(...contentLines.map((l) => l.length));\n const border = \"=\".repeat(maxLen + 4);\n\n const boxed = contentLines.map((line) => `| ${line.padEnd(maxLen)} |`);\n\n return [border, ...boxed, border].join(\"\\n\");\n }\n}\n"],"mappings":"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"}
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -8,9 +8,19 @@ import { JSONSchema7 } from "json-schema";
|
|
|
8
8
|
*/
|
|
9
9
|
interface ResourceFieldEntry {
|
|
10
10
|
/** Environment variable name for this field */
|
|
11
|
-
env
|
|
11
|
+
env?: string;
|
|
12
12
|
/** Human-readable description for this field */
|
|
13
13
|
description?: string;
|
|
14
|
+
/** When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. */
|
|
15
|
+
bundleIgnore?: boolean;
|
|
16
|
+
/** Example values showing the expected format for this field */
|
|
17
|
+
examples?: string[];
|
|
18
|
+
/** When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time. */
|
|
19
|
+
localOnly?: boolean;
|
|
20
|
+
/** Static value for this field. Used when no prompted or resolved value exists. */
|
|
21
|
+
value?: string;
|
|
22
|
+
/** Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. */
|
|
23
|
+
resolve?: string;
|
|
14
24
|
}
|
|
15
25
|
/**
|
|
16
26
|
* Declares a resource requirement for a plugin.
|