Skip to content

Commit 0b7cc3c

Browse files
authored
Merge pull request #42 from Brayden/bwilmoth/before-query
beforeQuery functional hook
2 parents 638f103 + 54b12ad commit 0b7cc3c

File tree

5 files changed

+163
-129
lines changed

5 files changed

+163
-129
lines changed

src/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DataSource, Source } from ".";
1+
import { DataSource, Source } from "./types";
22
import { LiteREST } from "./literest";
33
import { executeQuery, executeTransaction } from "./operation";
44
import { createResponse, QueryRequest, QueryTransactionRequest } from "./utils";

src/index.ts

Lines changed: 62 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createResponse } from './utils';
22
import handleStudioRequest from "./studio";
33
import { Handler } from "./handler";
4-
import { QueryResponse } from "./operation";
4+
import { DatabaseStub, DataSource, RegionLocationHint, Source } from './types';
55
export { DatabaseDurableObject } from './do';
66

77
const DURABLE_OBJECT_ID = 'sql-durable-object';
@@ -37,43 +37,6 @@ export interface Env {
3737
// ## DO NOT REMOVE: TEMPLATE INTERFACE ##
3838
}
3939

40-
export enum Source {
41-
internal = 'internal', // Durable Object's SQLite instance
42-
external = 'external' // External data source (e.g. Outerbase)
43-
}
44-
45-
export type DataSource = {
46-
source: Source;
47-
request: Request;
48-
internalConnection?: InternalConnection;
49-
externalConnection?: {
50-
outerbaseApiKey: string;
51-
};
52-
}
53-
54-
interface InternalConnection {
55-
durableObject: DatabaseStub;
56-
}
57-
58-
type DatabaseStub = DurableObjectStub & {
59-
fetch: (init?: RequestInit | Request) => Promise<Response>;
60-
executeQuery(sql: string, params: any[] | undefined, isRaw: boolean): QueryResponse;
61-
executeTransaction(queries: { sql: string; params?: any[] }[], isRaw: boolean): any[];
62-
};
63-
64-
enum RegionLocationHint {
65-
AUTO = 'auto',
66-
WNAM = 'wnam', // Western North America
67-
ENAM = 'enam', // Eastern North America
68-
SAM = 'sam', // South America
69-
WEUR = 'weur', // Western Europe
70-
EEUR = 'eeur', // Eastern Europe
71-
APAC = 'apac', // Asia Pacific
72-
OC = 'oc', // Oceania
73-
AFR = 'afr', // Africa
74-
ME = 'me', // Middle East
75-
}
76-
7740
export default {
7841
/**
7942
* This is the standard fetch handler for a Cloudflare Worker
@@ -84,67 +47,73 @@ export default {
8447
* @returns The response to be sent back to the client
8548
*/
8649
async fetch(request, env, ctx): Promise<Response> {
87-
const url = new URL(request.url);
88-
const pathname = url.pathname;
89-
const isWebSocket = request.headers.get("Upgrade") === "websocket";
90-
91-
/**
92-
* If the request is a GET request to the /studio endpoint, we can handle the request
93-
* directly in the Worker to avoid the need to deploy a separate Worker for the Studio.
94-
* Studio provides a user interface to interact with the SQLite database in the Durable
95-
* Object.
96-
*/
97-
if (env.STUDIO_USER && env.STUDIO_PASS && request.method === 'GET' && pathname === '/studio') {
98-
return handleStudioRequest(request, {
99-
username: env.STUDIO_USER,
100-
password: env.STUDIO_PASS,
101-
apiToken: env.AUTHORIZATION_TOKEN
102-
});
103-
}
50+
try {
51+
const url = new URL(request.url);
52+
const pathname = url.pathname;
53+
const isWebSocket = request.headers.get("Upgrade") === "websocket";
10454

105-
/**
106-
* Prior to proceeding to the Durable Object, we can perform any necessary validation or
107-
* authorization checks here to ensure the request signature is valid and authorized to
108-
* interact with the Durable Object.
109-
*/
110-
if (request.headers.get('Authorization') !== `Bearer ${env.AUTHORIZATION_TOKEN}` && !isWebSocket) {
111-
return createResponse(undefined, 'Unauthorized request', 401)
112-
} else if (isWebSocket) {
11355
/**
114-
* Web socket connections cannot pass in an Authorization header into their requests,
115-
* so we can use a query parameter to validate the connection.
56+
* If the request is a GET request to the /studio endpoint, we can handle the request
57+
* directly in the Worker to avoid the need to deploy a separate Worker for the Studio.
58+
* Studio provides a user interface to interact with the SQLite database in the Durable
59+
* Object.
11660
*/
117-
const token = url.searchParams.get('token');
118-
119-
if (token !== env.AUTHORIZATION_TOKEN) {
120-
return new Response('WebSocket connections are not supported at this endpoint.', { status: 440 });
61+
if (env.STUDIO_USER && env.STUDIO_PASS && request.method === 'GET' && pathname === '/studio') {
62+
return handleStudioRequest(request, {
63+
username: env.STUDIO_USER,
64+
password: env.STUDIO_PASS,
65+
apiToken: env.AUTHORIZATION_TOKEN
66+
});
12167
}
122-
}
123-
124-
/**
125-
* Retrieve the Durable Object identifier from the environment bindings and instantiate a
126-
* Durable Object stub to interact with the Durable Object.
127-
*/
128-
const region = env.REGION ?? RegionLocationHint.AUTO;
129-
const id: DurableObjectId = env.DATABASE_DURABLE_OBJECT.idFromName(DURABLE_OBJECT_ID);
130-
const stub = region !== RegionLocationHint.AUTO ? env.DATABASE_DURABLE_OBJECT.get(id, { locationHint: region as DurableObjectLocationHint }) : env.DATABASE_DURABLE_OBJECT.get(id);
13168

132-
const source: Source = request.headers.get('X-Starbase-Source') as Source ?? url.searchParams.get('source') as Source ?? 'internal';
133-
const dataSource: DataSource = {
134-
source: source,
135-
request: request.clone(),
136-
internalConnection: {
137-
durableObject: stub as unknown as DatabaseStub,
138-
},
139-
externalConnection: {
140-
outerbaseApiKey: env.OUTERBASE_API_KEY ?? ''
69+
/**
70+
* Prior to proceeding to the Durable Object, we can perform any necessary validation or
71+
* authorization checks here to ensure the request signature is valid and authorized to
72+
* interact with the Durable Object.
73+
*/
74+
if (request.headers.get('Authorization') !== `Bearer ${env.AUTHORIZATION_TOKEN}` && !isWebSocket) {
75+
return createResponse(undefined, 'Unauthorized request', 401)
76+
} else if (isWebSocket) {
77+
/**
78+
* Web socket connections cannot pass in an Authorization header into their requests,
79+
* so we can use a query parameter to validate the connection.
80+
*/
81+
const token = url.searchParams.get('token');
82+
83+
if (token !== env.AUTHORIZATION_TOKEN) {
84+
return new Response('WebSocket connections are not supported at this endpoint.', { status: 440 });
85+
}
14186
}
142-
};
14387

144-
const response = await new Handler().handle(request, dataSource, env);
145-
146-
// ## DO NOT REMOVE: TEMPLATE ROUTING ##
147-
148-
return response;
88+
/**
89+
* Retrieve the Durable Object identifier from the environment bindings and instantiate a
90+
* Durable Object stub to interact with the Durable Object.
91+
*/
92+
const region = env.REGION ?? RegionLocationHint.AUTO;
93+
const id: DurableObjectId = env.DATABASE_DURABLE_OBJECT.idFromName(DURABLE_OBJECT_ID);
94+
const stub = region !== RegionLocationHint.AUTO ? env.DATABASE_DURABLE_OBJECT.get(id, { locationHint: region as DurableObjectLocationHint }) : env.DATABASE_DURABLE_OBJECT.get(id);
95+
96+
const source: Source = request.headers.get('X-Starbase-Source') as Source ?? url.searchParams.get('source') as Source ?? 'internal';
97+
const dataSource: DataSource = {
98+
source: source,
99+
request: request.clone(),
100+
internalConnection: {
101+
durableObject: stub as unknown as DatabaseStub,
102+
},
103+
externalConnection: {
104+
outerbaseApiKey: env.OUTERBASE_API_KEY ?? ''
105+
}
106+
};
107+
108+
const response = await new Handler().handle(request, dataSource, env);
109+
return response;
110+
} catch (error) {
111+
// Return error response to client
112+
return createResponse(
113+
undefined,
114+
error instanceof Error ? error.message : 'An unexpected error occurred',
115+
400
116+
);
117+
}
149118
},
150119
} satisfies ExportedHandler<Env>;

src/literest/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createResponse } from '../utils';
2-
import { DataSource, Source } from "..";
2+
import { DataSource, Source } from "../types";
33
import { executeQuery, executeTransaction } from "../operation";
44
import { Env } from "../index"
55

src/operation.ts

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createClient as createTursoConnection } from '@libsql/client/web';
55

66
// Import how we interact with the databases through the Outerbase SDK
77
import { CloudflareD1Connection, MongoDBConnection, MySQLConnection, PostgreSQLConnection, StarbaseConnection, TursoConnection } from '@outerbase/sdk';
8-
import { DataSource } from '.';
8+
import { DataSource } from './types';
99
import { Env } from './'
1010
import { MongoClient } from 'mongodb';
1111

@@ -33,14 +33,53 @@ export type ConnectionDetails = {
3333
defaultSchema: string,
3434
}
3535

36+
async function beforeQuery(sql: string, params?: any[], dataSource?: DataSource, env?: Env): Promise<{ sql: string, params?: any[] }> {
37+
// ## DO NOT REMOVE: PRE QUERY HOOK ##
38+
39+
return {
40+
sql,
41+
params
42+
}
43+
}
44+
3645
async function afterQuery(sql: string, result: any, isRaw: boolean, dataSource?: DataSource, env?: Env): Promise<any> {
37-
// ## DO NOT REMOVE: TEMPLATE AFTER QUERY HOOK ##
46+
result = isRaw ? transformRawResults(result, 'from') : result;
47+
48+
// ## DO NOT REMOVE: POST QUERY HOOK ##
3849

39-
return result;
50+
return isRaw ? transformRawResults(result, 'to') : result;
4051
}
4152

42-
function cleanseQuery(sql: string): string {
43-
return sql.replaceAll('\n', ' ')
53+
function transformRawResults(result: any, direction: 'to' | 'from'): Record<string, any> {
54+
if (direction === 'from') {
55+
// Convert our result from the `raw` output to a traditional object
56+
result = {
57+
...result,
58+
rows: result.rows.map((row: any) =>
59+
result.columns.reduce((obj: any, column: string, index: number) => {
60+
obj[column] = row[index];
61+
return obj;
62+
}, {})
63+
)
64+
};
65+
66+
return result.rows
67+
} else if (direction === 'to') {
68+
// Convert our traditional object to the `raw` output format
69+
const columns = Object.keys(result[0] || {});
70+
const rows = result.map((row: any) => columns.map(col => row[col]));
71+
72+
return {
73+
columns,
74+
rows,
75+
meta: {
76+
rows_read: rows.length,
77+
rows_written: 0
78+
}
79+
};
80+
}
81+
82+
return result
4483
}
4584

4685
// Outerbase API supports more data sources than can be supported via Cloudflare Workers. For those data
@@ -68,6 +107,7 @@ async function executeExternalQuery(sql: string, params: any, isRaw: boolean, da
68107
sql = sql.replace(/\?/g, () => `:param${paramIndex++}`);
69108
}
70109

110+
71111
const API_URL = 'https://app.outerbase.com';
72112
const response = await fetch(`${API_URL}/api/v1/ezql/raw`, {
73113
method: 'POST',
@@ -76,14 +116,13 @@ async function executeExternalQuery(sql: string, params: any, isRaw: boolean, da
76116
'X-Source-Token': dataSource.externalConnection.outerbaseApiKey,
77117
},
78118
body: JSON.stringify({
79-
query: cleanseQuery(sql),
119+
query: sql.replaceAll('\n', ' '),
80120
params: convertedParams,
81121
}),
82122
});
83123

84124
const results: any = await response.json();
85-
const items = results.response.results?.items;
86-
return await afterQuery(sql, items, isRaw, dataSource, env);
125+
return results.response.results?.items;
87126
}
88127

89128
export async function executeQuery(sql: string, params: any | undefined, isRaw: boolean, dataSource?: DataSource, env?: Env): Promise<QueryResponse> {
@@ -92,12 +131,16 @@ export async function executeQuery(sql: string, params: any | undefined, isRaw:
92131
return []
93132
}
94133

134+
const { sql: updatedSQL, params: updatedParams } = await beforeQuery(sql, params, dataSource, env)
135+
let response;
136+
95137
if (dataSource.source === 'internal') {
96-
const response = await dataSource.internalConnection?.durableObject.executeQuery(sql, params, isRaw);
97-
return await afterQuery(sql, response, isRaw, dataSource, env);
138+
response = await dataSource.internalConnection?.durableObject.executeQuery(updatedSQL, updatedParams, isRaw);
98139
} else {
99-
return executeExternalQuery(sql, params, isRaw, dataSource, env);
140+
response = await executeExternalQuery(updatedSQL, updatedParams, isRaw, dataSource, env);
100141
}
142+
143+
return await afterQuery(updatedSQL, response, isRaw, dataSource, env);
101144
}
102145

103146
export async function executeTransaction(queries: { sql: string; params?: any[] }[], isRaw: boolean, dataSource?: DataSource, env?: Env): Promise<QueryResponse> {
@@ -106,30 +149,14 @@ export async function executeTransaction(queries: { sql: string; params?: any[]
106149
return []
107150
}
108151

109-
if (dataSource.source === 'internal') {
110-
const results: any[] = [];
111-
112-
for (const query of queries) {
113-
const result = await dataSource.internalConnection?.durableObject.executeTransaction(queries, isRaw);
114-
if (result) {
115-
const processedResults = await Promise.all(
116-
result.map(r => afterQuery(query.sql, r, isRaw, dataSource, env))
117-
);
118-
results.push(...processedResults);
119-
}
120-
}
121-
122-
return results;
123-
} else {
124-
const results = [];
152+
const results = [];
125153

126-
for (const query of queries) {
127-
const result = await executeExternalQuery(query.sql, query.params, isRaw, dataSource, env);
128-
results.push(result);
129-
}
130-
131-
return results;
154+
for (const query of queries) {
155+
const result = await executeQuery(query.sql, query.params, isRaw, dataSource, env);
156+
results.push(result);
132157
}
158+
159+
return results;
133160
}
134161

135162
async function createSDKPostgresConnection(env: Env): Promise<ConnectionDetails> {
@@ -246,5 +273,5 @@ export async function executeSDKQuery(sql: string, params: any | undefined, isRa
246273
await db.connect();
247274
const { data } = await db.raw(sql, params);
248275

249-
return await afterQuery(sql, data, isRaw, dataSource, env);
276+
return data;
250277
}

src/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { QueryResponse } from "./operation";
2+
3+
export enum Source {
4+
internal = 'internal', // Durable Object's SQLite instance
5+
external = 'external' // External data source (e.g. Outerbase)
6+
}
7+
8+
export type DataSource = {
9+
source: Source;
10+
request: Request;
11+
internalConnection?: InternalConnection;
12+
externalConnection?: {
13+
outerbaseApiKey: string;
14+
};
15+
}
16+
17+
export interface InternalConnection {
18+
durableObject: DatabaseStub;
19+
}
20+
21+
export type DatabaseStub = DurableObjectStub & {
22+
fetch: (init?: RequestInit | Request) => Promise<Response>;
23+
executeQuery(sql: string, params: any[] | undefined, isRaw: boolean): QueryResponse;
24+
executeTransaction(queries: { sql: string; params?: any[] }[], isRaw: boolean): any[];
25+
};
26+
27+
export enum RegionLocationHint {
28+
AUTO = 'auto',
29+
WNAM = 'wnam', // Western North America
30+
ENAM = 'enam', // Eastern North America
31+
SAM = 'sam', // South America
32+
WEUR = 'weur', // Western Europe
33+
EEUR = 'eeur', // Eastern Europe
34+
APAC = 'apac', // Asia Pacific
35+
OC = 'oc', // Oceania
36+
AFR = 'afr', // Africa
37+
ME = 'me', // Middle East
38+
}

0 commit comments

Comments
 (0)