Skip to content

Commit c2434ea

Browse files
committed
Plugins support beforeQuery and afterQuery hooks
1 parent 4fd1db1 commit c2434ea

File tree

10 files changed

+293
-201
lines changed

10 files changed

+293
-201
lines changed

plugins/studio/index.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,29 @@ export class StudioPlugin extends StarbasePlugin {
66
private username: string
77
private password: string
88
private apiKey: string
9-
private prefix: string
109

1110
constructor(options: {
12-
username: string
13-
password: string
11+
username?: string
12+
password?: string
1413
apiKey: string
1514
prefix?: string
1615
}) {
17-
super('starbasedb:studio')
18-
this.username = options.username
19-
this.password = options.password
16+
super(
17+
'starbasedb:studio',
18+
{
19+
requiresAuth: false,
20+
},
21+
options.prefix ?? '/studio'
22+
)
23+
this.username = options.username || ''
24+
this.password = options.password || ''
2025
this.apiKey = options.apiKey
21-
this.prefix = options.prefix || '/studio'
2226
}
2327

2428
override async register(app: StarbaseApp) {
25-
app.get(this.prefix, async (c) => {
29+
if (!this.pathPrefix) return
30+
31+
app.get(this.pathPrefix, async (c) => {
2632
return handleStudioRequest(c.req.raw, {
2733
username: this.username,
2834
password: this.password,

plugins/websocket/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ export class WebSocketPlugin extends StarbasePlugin {
55
private prefix = '/socket'
66

77
constructor(opts?: { prefix?: string }) {
8-
super('starbasedb:websocket')
8+
super('starbasedb:websocket', {
9+
requiresAuth: true,
10+
})
911
this.prefix = opts?.prefix ?? this.prefix
1012
}
1113

src/allowlist/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function normalizeSQL(sql: string) {
1616

1717
async function loadAllowlist(dataSource: DataSource): Promise<string[]> {
1818
try {
19-
const statement = 'SELECT sql_statement FROM tmp_allowlist_queries'
19+
const statement = `SELECT sql_statement FROM tmp_allowlist_queries WHERE source="${dataSource.source}"`
2020
const result = (await dataSource.rpc.executeQuery({
2121
sql: statement,
2222
})) as QueryResult[]

src/cors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const corsHeaders = {
22
'Access-Control-Allow-Origin': '*',
3-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
3+
'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
44
'Access-Control-Allow-Headers':
55
'Authorization, Content-Type, X-Starbase-Source, X-Data-Source',
66
'Access-Control-Max-Age': '86400',

src/handler.ts

Lines changed: 134 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,13 @@ type HonoContext = {
4040
}
4141
}
4242

43-
const app = new Hono<HonoContext>()
44-
45-
export type StarbaseApp = typeof app
46-
export type StarbaseContext = Context<HonoContext>
47-
4843
export class StarbaseDB {
4944
private dataSource: DataSource
5045
private config: StarbaseDBConfiguration
5146
private liteREST: LiteREST
5247
private plugins: StarbasePlugin[]
48+
private initialized: boolean = false
49+
private app: StarbaseApp
5350

5451
constructor(options: {
5552
dataSource: DataSource
@@ -60,6 +57,7 @@ export class StarbaseDB {
6057
this.config = options.config
6158
this.liteREST = new LiteREST(this.dataSource, this.config)
6259
this.plugins = options.plugins || []
60+
this.app = new Hono<HonoContext>()
6361

6462
if (
6563
this.dataSource.source === 'external' &&
@@ -69,62 +67,11 @@ export class StarbaseDB {
6967
}
7068
}
7169

72-
/**
73-
* Middleware to check if the request is coming from an internal source.
74-
*/
75-
private get isInternalSource() {
76-
return createMiddleware(async (_, next) => {
77-
if (this.dataSource.source !== 'internal') {
78-
return createResponse(
79-
undefined,
80-
'Function is only available for internal data source.',
81-
400
82-
)
83-
}
84-
85-
return next()
86-
})
87-
}
88-
89-
/**
90-
* Validator middleware to check if the request path has a valid :tableName parameter.
91-
*/
92-
private get hasTableName() {
93-
return validator('param', (params) => {
94-
const tableName = params['tableName'].trim()
95-
96-
if (!tableName) {
97-
return createResponse(undefined, 'Table name is required', 400)
98-
}
99-
100-
return { tableName }
101-
})
102-
}
103-
104-
/**
105-
* Helper function to get a feature flag from the configuration.
106-
* @param key The feature key to get.
107-
* @param defaultValue The default value to return if the feature is not defined.
108-
* @returns
109-
*/
110-
private getFeature(
111-
key: keyof NonNullable<StarbaseDBConfiguration['features']>,
112-
defaultValue = true
113-
): boolean {
114-
return this.config.features?.[key] ?? !!defaultValue
115-
}
70+
private async initialize() {
71+
if (this.initialized) return
11672

117-
/**
118-
* Main handler function for the StarbaseDB.
119-
* @param request Request instance from the fetch event.
120-
* @returns Promise<Response>
121-
*/
122-
public async handle(
123-
request: Request,
124-
ctx: ExecutionContext
125-
): Promise<Response> {
126-
// Add context to the request
127-
app.use('*', async (c, next) => {
73+
// Set up middleware first
74+
this.app.use('*', async (c, next) => {
12875
c.set('config', this.config)
12976
c.set('dataSource', this.dataSource)
13077
c.set('operations', {
@@ -134,48 +81,30 @@ export class StarbaseDB {
13481
return next()
13582
})
13683

137-
// Non-blocking operation to remove expired cache entries from our DO
138-
ctx.waitUntil(this.expireCache())
139-
140-
// General 404 not found handler
141-
app.notFound(() => {
142-
return createResponse(undefined, 'Not found', 404)
143-
})
144-
145-
// Thrown error handler
146-
app.onError((error) => {
147-
return createResponse(
148-
undefined,
149-
error?.message || 'An unexpected error occurred.',
150-
500
151-
)
152-
})
153-
84+
// Initialize plugins
15485
const registry = new StarbasePluginRegistry({
155-
app,
86+
app: this.app,
15687
plugins: this.plugins,
15788
})
158-
15989
await registry.init()
16090

161-
// CORS preflight handler.
162-
app.options('*', () => corsPreflight())
163-
164-
app.post('/query/raw', async (c) => this.queryRoute(c.req.raw, true))
165-
app.post('/query', async (c) => this.queryRoute(c.req.raw, false))
91+
this.app.post('/query/raw', async (c) =>
92+
this.queryRoute(c.req.raw, true)
93+
)
94+
this.app.post('/query', async (c) => this.queryRoute(c.req.raw, false))
16695

16796
if (this.getFeature('rest')) {
168-
app.all('/rest/*', async (c) => {
97+
this.app.all('/rest/*', async (c) => {
16998
return this.liteREST.handleRequest(c.req.raw)
17099
})
171100
}
172101

173102
if (this.getFeature('export')) {
174-
app.get('/export/dump', this.isInternalSource, async () => {
103+
this.app.get('/export/dump', this.isInternalSource, async () => {
175104
return dumpDatabaseRoute(this.dataSource, this.config)
176105
})
177106

178-
app.get(
107+
this.app.get(
179108
'/export/json/:tableName',
180109
this.isInternalSource,
181110
this.hasTableName,
@@ -189,7 +118,7 @@ export class StarbaseDB {
189118
}
190119
)
191120

192-
app.get(
121+
this.app.get(
193122
'/export/csv/:tableName',
194123
this.isInternalSource,
195124
this.hasTableName,
@@ -205,44 +134,151 @@ export class StarbaseDB {
205134
}
206135

207136
if (this.getFeature('import')) {
208-
app.post('/import/dump', this.isInternalSource, async (c) => {
137+
this.app.post('/import/dump', this.isInternalSource, async (c) => {
209138
return importDumpRoute(c.req.raw, this.dataSource, this.config)
210139
})
211140

212-
app.post(
141+
this.app.post(
213142
'/import/json/:tableName',
214143
this.isInternalSource,
215144
this.hasTableName,
216145
async (c) => {
217146
const tableName = c.req.valid('param').tableName
218147
return importTableFromJsonRoute(
219148
tableName,
220-
request,
149+
c.req.raw,
221150
this.dataSource,
222151
this.config
223152
)
224153
}
225154
)
226155

227-
app.post(
156+
this.app.post(
228157
'/import/csv/:tableName',
229158
this.isInternalSource,
230159
this.hasTableName,
231160
async (c) => {
232161
const tableName = c.req.valid('param').tableName
233162
return importTableFromCsvRoute(
234163
tableName,
235-
request,
164+
c.req.raw,
236165
this.dataSource,
237166
this.config
238167
)
239168
}
240169
)
241170
}
242171

243-
app.all('/api/*', async (c) => handleApiRequest(c.req.raw))
172+
this.app.all('/api/*', async (c) => handleApiRequest(c.req.raw))
244173

245-
return app.fetch(request)
174+
// Set up error handlers
175+
this.app.notFound(() => {
176+
return createResponse(undefined, 'Not found', 404)
177+
})
178+
179+
this.app.onError((error) => {
180+
return createResponse(
181+
undefined,
182+
error?.message || 'An unexpected error occurred.',
183+
500
184+
)
185+
})
186+
187+
this.initialized = true
188+
}
189+
190+
public async handlePreAuth(
191+
request: Request,
192+
ctx: ExecutionContext
193+
): Promise<Response | undefined> {
194+
// Initialize everything once
195+
await this.initialize()
196+
197+
const authlessPlugin = this.plugins.find((plugin: StarbasePlugin) => {
198+
if (!plugin.opts.requiresAuth && request.url && plugin.pathPrefix) {
199+
// Extract the path from the full URL
200+
const urlPath = new URL(request.url).pathname
201+
202+
// Convert plugin path pattern to regex
203+
const pathPattern = plugin.pathPrefix
204+
.replace(/:[^/]+/g, '[^/]+') // Replace :param with regex pattern
205+
.replace(/\*/g, '.*') // Replace * with wildcard pattern
206+
207+
const regex = new RegExp(`^${pathPattern}$`)
208+
return regex.test(urlPath)
209+
}
210+
211+
return false
212+
})
213+
214+
if (authlessPlugin) {
215+
return this.app.fetch(request)
216+
}
217+
218+
return undefined
219+
}
220+
221+
public async handle(
222+
request: Request,
223+
ctx: ExecutionContext
224+
): Promise<Response> {
225+
// Initialize everything once
226+
await this.initialize()
227+
228+
// Non-blocking operation to remove expired cache entries from our DO
229+
ctx.waitUntil(this.expireCache())
230+
231+
// CORS preflight handler
232+
if (request.method === 'OPTIONS') {
233+
return corsPreflight()
234+
}
235+
236+
return this.app.fetch(request)
237+
}
238+
239+
/**
240+
* Middleware to check if the request is coming from an internal source.
241+
*/
242+
private get isInternalSource() {
243+
return createMiddleware(async (_, next) => {
244+
if (this.dataSource.source !== 'internal') {
245+
return createResponse(
246+
undefined,
247+
'Function is only available for internal data source.',
248+
400
249+
)
250+
}
251+
252+
return next()
253+
})
254+
}
255+
256+
/**
257+
* Validator middleware to check if the request path has a valid :tableName parameter.
258+
*/
259+
private get hasTableName() {
260+
return validator('param', (params) => {
261+
const tableName = params['tableName'].trim()
262+
263+
if (!tableName) {
264+
return createResponse(undefined, 'Table name is required', 400)
265+
}
266+
267+
return { tableName }
268+
})
269+
}
270+
271+
/**
272+
* Helper function to get a feature flag from the configuration.
273+
* @param key The feature key to get.
274+
* @param defaultValue The default value to return if the feature is not defined.
275+
* @returns
276+
*/
277+
private getFeature(
278+
key: keyof NonNullable<StarbaseDBConfiguration['features']>,
279+
defaultValue = true
280+
): boolean {
281+
return this.config.features?.[key] ?? !!defaultValue
246282
}
247283

248284
async queryRoute(request: Request, isRaw: boolean): Promise<Response> {
@@ -339,3 +375,6 @@ export class StarbaseDB {
339375
}
340376
}
341377
}
378+
379+
export type StarbaseApp = Hono<HonoContext>
380+
export type StarbaseContext = Context<HonoContext>

0 commit comments

Comments
 (0)