From a4e64bd17ae7c11028615e6e89849b329624b6fb Mon Sep 17 00:00:00 2001 From: Jeroen Peeters Date: Sun, 23 Feb 2025 20:28:20 +0100 Subject: [PATCH] feat: make feature flags a bit more granular --- src/handler.test.ts | 19 ++++++--- src/handler.ts | 15 +++++-- src/index.ts | 89 ++++++++++++++++++++++++++------------- src/operation.ts | 8 ++-- src/utils.ts | 12 ++++++ worker-configuration.d.ts | 8 ++++ wrangler.toml | 8 ++++ 7 files changed, 117 insertions(+), 42 deletions(-) diff --git a/src/handler.test.ts b/src/handler.test.ts index 86bb328..e995ef0 100644 --- a/src/handler.test.ts +++ b/src/handler.test.ts @@ -49,13 +49,18 @@ vi.mock('./plugin', () => ({ })), })) -vi.mock('./utils', () => ({ - createResponse: vi.fn((result, error, status) => ({ - result, - error, - status, - })), -})) +vi.mock('./utils', async () => { + const { getFeatureFromConfig } = await import('./utils') + + return { + createResponse: vi.fn((result, error, status) => ({ + result, + error, + status, + })), + getFeatureFromConfig, + } +}) let instance: StarbaseDB let mockDataSource: DataSource diff --git a/src/handler.ts b/src/handler.ts index fd459a9..dde41f9 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -5,7 +5,12 @@ import { validator } from 'hono/validator' import { DataSource } from './types' import { LiteREST } from './literest' import { executeQuery, executeTransaction } from './operation' -import { createResponse, QueryRequest, QueryTransactionRequest } from './utils' +import { + createResponse, + QueryRequest, + QueryTransactionRequest, + getFeatureFromConfig, +} from './utils' import { dumpDatabaseRoute } from './export/dump' import { exportTableToJsonRoute } from './export/json' import { exportTableToCsvRoute } from './export/csv' @@ -26,6 +31,10 @@ export interface StarbaseDBConfiguration { websocket?: boolean export?: boolean import?: boolean + studio?: boolean + cron?: boolean + cdc?: boolean + interface?: boolean } } @@ -283,9 +292,9 @@ export class StarbaseDB { */ private getFeature( key: keyof NonNullable, - defaultValue = true + defaultValue?: boolean ): boolean { - return this.config.features?.[key] ?? !!defaultValue + return getFeatureFromConfig(this.config.features)(key, defaultValue) } async queryRoute(request: Request, isRaw: boolean): Promise { diff --git a/src/index.ts b/src/index.ts index 2713b73..5831817 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { createResponse } from './utils' +import { createResponse, getFeatureFromConfig } from './utils' import { StarbaseDB, StarbaseDBConfiguration } from './handler' import { DataSource, RegionLocationHint } from './types' import { createRemoteJWKSet, jwtVerify } from 'jose' @@ -31,6 +31,14 @@ export interface Env { ENABLE_ALLOWLIST?: boolean ENABLE_RLS?: boolean + ENABLE_REST?: boolean + ENABLE_WEBSOCKET?: boolean + ENABLE_EXPORT?: boolean + ENABLE_IMPORT?: boolean + ENABLE_CRON?: boolean + ENABLE_CDC?: boolean + ENABLE_INTERFACE?: boolean + ENABLE_STUDIO?: boolean // External database source details OUTERBASE_API_KEY?: string @@ -171,43 +179,63 @@ export default { features: { allowlist: env.ENABLE_ALLOWLIST, rls: env.ENABLE_RLS, + rest: env.ENABLE_REST, + websocket: env.ENABLE_WEBSOCKET, + export: env.ENABLE_EXPORT, + import: env.ENABLE_IMPORT, + cron: env.ENABLE_CRON, + cdc: env.ENABLE_CDC, + interface: env.ENABLE_INTERFACE, + studio: env.ENABLE_STUDIO, }, } - const webSocketPlugin = new WebSocketPlugin() - const cronPlugin = new CronPlugin() - const cdcPlugin = new ChangeDataCapturePlugin({ + const getFeature = getFeatureFromConfig(config.features) + + /** + * Plugins + */ + const webSocketPlugin = getFeature('websocket') ? new WebSocketPlugin() : undefined + const studioPlugin = getFeature('studio') ? new StudioPlugin({ + username: env.STUDIO_USER, + password: env.STUDIO_PASS, + apiKey: env.ADMIN_AUTHORIZATION_TOKEN, + }) : undefined + const sqlMacrosPlugin = new SqlMacrosPlugin({ + preventSelectStar: false, + }) + const queryLogPlugin = new QueryLogPlugin({ ctx }) + const cdcPlugin = getFeature('cdc') ? new ChangeDataCapturePlugin({ stub, broadcastAllEvents: false, events: [], - }) - - cdcPlugin.onEvent(async ({ action, schema, table, data }) => { - // Include change data capture code here - }, ctx) - - cronPlugin.onEvent(async ({ name, cron_tab, payload }) => { - // Include cron event code here - }, ctx) - - const interfacePlugin = new InterfacePlugin() - - const plugins = [ + }) : undefined + const cronPlugin = getFeature('cron') ? new CronPlugin() : undefined + const statsPlugin = new StatsPlugin() + const interfacePlugin = getFeature('interface') ? new InterfacePlugin() : undefined + + const plugins: StarbasePlugin[] = [ webSocketPlugin, - new StudioPlugin({ - username: env.STUDIO_USER, - password: env.STUDIO_PASS, - apiKey: env.ADMIN_AUTHORIZATION_TOKEN, - }), - new SqlMacrosPlugin({ - preventSelectStar: false, - }), - new QueryLogPlugin({ ctx }), + studioPlugin, + sqlMacrosPlugin, + queryLogPlugin, cdcPlugin, cronPlugin, - new StatsPlugin(), + statsPlugin, interfacePlugin, - ] satisfies StarbasePlugin[] + ].filter(plugin => !!plugin) + + if (getFeature('cdc')) { + cdcPlugin?.onEvent(async ({ action, schema, table, data }) => { + // Include change data capture code here + }, ctx) + } + + if (getFeature('cron')) { + cronPlugin?.onEvent(async ({ name, cron_tab, payload }) => { + // Include cron event code here + }, ctx) + } const starbase = new StarbaseDB({ dataSource, @@ -227,7 +255,10 @@ export default { // next authentication checks happen. If a page is meant to have any // sort of authentication, it can provide Basic Auth itself or expose // itself in another plugin. - if (interfacePlugin.matchesRoute(url.pathname)) { + if ( + getFeature('interface') && + interfacePlugin?.matchesRoute(url.pathname) + ) { return await starbase.handle(request, ctx) } diff --git a/src/operation.ts b/src/operation.ts index c6163eb..182cb85 100644 --- a/src/operation.ts +++ b/src/operation.ts @@ -24,7 +24,7 @@ import { isQueryAllowed } from './allowlist' import { applyRLS } from './rls' import type { SqlConnection } from '@outerbase/sdk/dist/connections/sql-base' import { StarbasePlugin } from './plugin' - +import { getFeatureFromConfig } from './utils' export type OperationQueueItem = { queries: { sql: string; params?: any[] }[] isTransaction: boolean @@ -204,10 +204,12 @@ export async function executeQuery(opts: { return [] } + const getFeature = getFeatureFromConfig(config.features) + // If the allowlist feature is enabled, we should verify the query is allowed before proceeding. await isQueryAllowed({ sql: sql, - isEnabled: config?.features?.allowlist ?? false, + isEnabled: getFeature('allowlist', false), dataSource, config, }) @@ -215,7 +217,7 @@ export async function executeQuery(opts: { // If the row level security feature is enabled, we should apply our policies to this SQL statement. sql = await applyRLS({ sql, - isEnabled: config?.features?.rls ?? true, + isEnabled: getFeature('rls', true), dataSource, config, }) diff --git a/src/utils.ts b/src/utils.ts index 37d969d..77e455b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { corsHeaders } from './cors' +import { StarbaseDBConfiguration } from './handler' export type QueryTransactionRequest = { transaction?: QueryRequest[] @@ -22,3 +23,14 @@ export function createResponse( }, }) } + +export function getFeatureFromConfig( + features: StarbaseDBConfiguration['features'] +) { + return function getFeature( + key: keyof NonNullable, + defaultValue = true + ): boolean { + return features?.[key] ?? !!defaultValue + } +} diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 6c35c6f..4222f5a 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -8,6 +8,14 @@ interface Env { STUDIO_PASS: '123456' ENABLE_ALLOWLIST: 0 ENABLE_RLS: 0 + ENABLE_CRON: 0 + ENABLE_CDC: 0 + ENABLE_INTERFACE: 0 + ENABLE_STUDIO: 0 + ENABLE_REST: 1 + ENABLE_WEBSOCKET: 1 + ENABLE_EXPORT: 1 + ENABLE_IMPORT: 1 AUTH_ALGORITHM: 'RS256' AUTH_JWKS_ENDPOINT: '' DATABASE_DURABLE_OBJECT: DurableObjectNamespace< diff --git a/wrangler.toml b/wrangler.toml index 89ebca2..c2be81f 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -51,6 +51,14 @@ REGION = "auto" # Toggle to enable default features ENABLE_ALLOWLIST = 0 ENABLE_RLS = 0 +ENABLE_CRON = 1 +ENABLE_CDC = 1 +ENABLE_INTERFACE = 1 +ENABLE_STUDIO = 1 +ENABLE_REST = 1 +ENABLE_WEBSOCKET = 1 +ENABLE_EXPORT = 1 +ENABLE_IMPORT = 1 # External database source details # This enables Starbase to connect to an external data source