Skip to content

Commit b0829b9

Browse files
committed
beforeQuery functional hook
1 parent 638f103 commit b0829b9

File tree

5 files changed

+128
-122
lines changed

5 files changed

+128
-122
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: 65 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';
@@ -35,43 +35,9 @@ export interface Env {
3535
EXTERNAL_DB_CLOUDFLARE_DATABASE_ID?: string;
3636

3737
// ## DO NOT REMOVE: TEMPLATE INTERFACE ##
38-
}
39-
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
38+
RLS: {
39+
applyRLS(sql: string, dialect?: string): Promise<string | Error>
40+
}
7541
}
7642

7743
export default {
@@ -84,67 +50,73 @@ export default {
8450
* @returns The response to be sent back to the client
8551
*/
8652
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-
}
53+
try {
54+
const url = new URL(request.url);
55+
const pathname = url.pathname;
56+
const isWebSocket = request.headers.get("Upgrade") === "websocket";
10457

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) {
11358
/**
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.
59+
* If the request is a GET request to the /studio endpoint, we can handle the request
60+
* directly in the Worker to avoid the need to deploy a separate Worker for the Studio.
61+
* Studio provides a user interface to interact with the SQLite database in the Durable
62+
* Object.
11663
*/
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 });
64+
if (env.STUDIO_USER && env.STUDIO_PASS && request.method === 'GET' && pathname === '/studio') {
65+
return handleStudioRequest(request, {
66+
username: env.STUDIO_USER,
67+
password: env.STUDIO_PASS,
68+
apiToken: env.AUTHORIZATION_TOKEN
69+
});
12170
}
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);
13171

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 ?? ''
72+
/**
73+
* Prior to proceeding to the Durable Object, we can perform any necessary validation or
74+
* authorization checks here to ensure the request signature is valid and authorized to
75+
* interact with the Durable Object.
76+
*/
77+
if (request.headers.get('Authorization') !== `Bearer ${env.AUTHORIZATION_TOKEN}` && !isWebSocket) {
78+
return createResponse(undefined, 'Unauthorized request', 401)
79+
} else if (isWebSocket) {
80+
/**
81+
* Web socket connections cannot pass in an Authorization header into their requests,
82+
* so we can use a query parameter to validate the connection.
83+
*/
84+
const token = url.searchParams.get('token');
85+
86+
if (token !== env.AUTHORIZATION_TOKEN) {
87+
return new Response('WebSocket connections are not supported at this endpoint.', { status: 440 });
88+
}
14189
}
142-
};
14390

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

3948
return result;
4049
}
@@ -68,6 +77,7 @@ async function executeExternalQuery(sql: string, params: any, isRaw: boolean, da
6877
sql = sql.replace(/\?/g, () => `:param${paramIndex++}`);
6978
}
7079

80+
7181
const API_URL = 'https://app.outerbase.com';
7282
const response = await fetch(`${API_URL}/api/v1/ezql/raw`, {
7383
method: 'POST',
@@ -92,11 +102,13 @@ export async function executeQuery(sql: string, params: any | undefined, isRaw:
92102
return []
93103
}
94104

105+
const { sql: updatedSQL, params: updatedParams } = await beforeQuery(sql, params, dataSource, env)
106+
95107
if (dataSource.source === 'internal') {
96-
const response = await dataSource.internalConnection?.durableObject.executeQuery(sql, params, isRaw);
97-
return await afterQuery(sql, response, isRaw, dataSource, env);
108+
const response = await dataSource.internalConnection?.durableObject.executeQuery(updatedSQL, updatedParams, isRaw);
109+
return await afterQuery(updatedSQL, response, isRaw, dataSource, env);
98110
} else {
99-
return executeExternalQuery(sql, params, isRaw, dataSource, env);
111+
return executeExternalQuery(updatedSQL, updatedParams, isRaw, dataSource, env);
100112
}
101113
}
102114

@@ -106,30 +118,14 @@ export async function executeTransaction(queries: { sql: string; params?: any[]
106118
return []
107119
}
108120

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 = [];
121+
const results = [];
125122

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;
123+
for (const query of queries) {
124+
const result = await executeQuery(query.sql, query.params, isRaw, dataSource, env);
125+
results.push(result);
132126
}
127+
128+
return results;
133129
}
134130

135131
async function createSDKPostgresConnection(env: Env): Promise<ConnectionDetails> {

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)