Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/export/dump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { enqueueOperation, processNextOperation } from '../operation';
import { createResponse } from '../utils';

export async function dumpDatabaseRoute(
sql: any,
operationQueue: any,
ctx: any,
processingOperation: { value: boolean }
): Promise<Response> {
try {
// Get all table names
const tablesResult = await enqueueOperation(
[{ sql: "SELECT name FROM sqlite_master WHERE type='table';" }],
false,
false,
operationQueue,
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
);

const tables = tablesResult.result.map((row: any) => row.name);
let dumpContent = "SQLite format 3\0"; // SQLite file header

// Iterate through all tables
for (const table of tables) {
// Get table schema
const schemaResult = await enqueueOperation(
[{ sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}';` }],
false,
false,
operationQueue,
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
);

if (schemaResult.result.length) {
const schema = schemaResult.result[0].sql;
dumpContent += `\n-- Table: ${table}\n${schema};\n\n`;
}

// Get table data
const dataResult = await enqueueOperation(
[{ sql: `SELECT * FROM ${table};` }],
false,
false,
operationQueue,
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
);

for (const row of dataResult.result) {
const values = Object.values(row).map(value =>
typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value
);
dumpContent += `INSERT INTO ${table} VALUES (${values.join(', ')});\n`;
}

dumpContent += '\n';
}

// Create a Blob from the dump content
const blob = new Blob([dumpContent], { type: 'application/x-sqlite3' });

const headers = new Headers({
'Content-Type': 'application/x-sqlite3',
'Content-Disposition': 'attachment; filename="database_dump.sql"',
});

return new Response(blob, { headers });
} catch (error: any) {
console.error('Database Dump Error:', error);
return createResponse(undefined, 'Failed to create database dump', 500);
}
}
69 changes: 3 additions & 66 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createResponse, createResponseFromOperationResponse, QueryRequest, Quer
import { enqueueOperation, OperationQueueItem, processNextOperation } from './operation';
import { LiteREST } from './literest';
import handleStudioRequest from "./studio";
import { dumpDatabaseRoute } from './export/dump';

const DURABLE_OBJECT_ID = 'sql-durable-object';

Expand Down Expand Up @@ -122,10 +123,10 @@ export class DatabaseDurableObject extends DurableObject {
return this.clientConnected();
} else if (request.method === 'GET' && url.pathname === '/status') {
return this.statusRoute(request);
} else if (url.pathname.startsWith('/lite')) {
} else if (url.pathname.startsWith('/rest')) {
return await this.liteREST.handleRequest(request);
} else if (request.method === 'GET' && url.pathname === '/dump') {
return this.dumpDatabaseRoute(request);
return dumpDatabaseRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation);
} else {
return createResponse(undefined, 'Unknown operation', 400);
}
Expand Down Expand Up @@ -170,70 +171,6 @@ export class DatabaseDurableObject extends DurableObject {
this.connections.delete(wsSessionId);
}
}

async dumpDatabaseRoute(_: Request): Promise<Response> {
try {
// Get all table names
const tablesResult = await enqueueOperation(
[{ sql: "SELECT name FROM sqlite_master WHERE type='table';" }],
false,
false,
this.operationQueue,
() => processNextOperation(this.sql, this.operationQueue, this.ctx, this.processingOperation)
);

const tables = tablesResult.result.map((row: any) => row.name);
let dumpContent = "SQLite format 3\0"; // SQLite file header

// Iterate through all tables
for (const table of tables) {
// Get table schema
const schemaResult = await enqueueOperation(
[{ sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}';` }],
false,
false,
this.operationQueue,
() => processNextOperation(this.sql, this.operationQueue, this.ctx, this.processingOperation)
);

if (schemaResult.result.length) {
const schema = schemaResult.result[0].sql;
dumpContent += `\n-- Table: ${table}\n${schema};\n\n`;
}

// Get table data
const dataResult = await enqueueOperation(
[{ sql: `SELECT * FROM ${table};` }],
false,
false,
this.operationQueue,
() => processNextOperation(this.sql, this.operationQueue, this.ctx, this.processingOperation)
);

for (const row of dataResult.result) {
const values = Object.values(row).map(value =>
typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value
);
dumpContent += `INSERT INTO ${table} VALUES (${values.join(', ')});\n`;
}

dumpContent += '\n';
}

// Create a Blob from the dump content
const blob = new Blob([dumpContent], { type: 'application/x-sqlite3' });

const headers = new Headers({
'Content-Type': 'application/x-sqlite3',
'Content-Disposition': 'attachment; filename="database_dump.sql"',
});

return new Response(blob, { headers });
} catch (error: any) {
console.error('Database Dump Error:', error);
return createResponse(undefined, 'Failed to create database dump', 500);
}
}
}

export default {
Expand Down
30 changes: 15 additions & 15 deletions src/literest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Fetch data from the database.
## Equals
Get any entry that matches the column named `name` inside the `users` table where name = `Alice`.
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?name=Alice' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?name=Alice' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
Expand All @@ -13,7 +13,7 @@ curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/
## Not Equals
Get any result that does NOT equal the provided value.
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?name.ne=Alice' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?name.ne=Alice' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
Expand All @@ -22,7 +22,7 @@ curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/
## LIKE
The URL has `%25` appended to it which represents the `%` character. We need the `%` character to represent in SQL any number of characters can appear here to be considered "LIKE".
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?name.like=Al%25' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?name.like=Al%25' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
Expand All @@ -31,71 +31,71 @@ curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/
## IN
Get all results that match the names in the IN criteria, which the example below includes `Alice` and `Bob`.
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?name.in=Alice,Bob' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?name.in=Alice,Bob' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## Greater Than
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?user_id.gt=0' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?user_id.gt=0' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## Greater Than or Equal
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?user_id.gte=1' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?user_id.gte=1' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## Less Than
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?user_id.lt=3' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?user_id.lt=3' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## Less Than or Equal
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?user_id.lte=3' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?user_id.lte=3' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## SORT BY & ORDER
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?sort_by=user_id&order=DESC' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?sort_by=user_id&order=DESC' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## LIMIT & OFFSET
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?limit=2&offset=1' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?limit=2&offset=1' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

## A bit of everything
```
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users?name.in=Alice%2CBob&user_id.gte=0&email.like=%25example.com&sort_by=user_id&order=DESC&limit=10&offset=0' \
curl --location --request GET 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users?name.in=Alice%2CBob&user_id.gte=0&email.like=%25example.com&sort_by=user_id&order=DESC&limit=10&offset=0' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'Content-type=application/json'
```

# POST
```
curl --location 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users' \
curl --location 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: text/plain' \
--header 'Content-type: application/json' \
Expand All @@ -107,14 +107,14 @@ curl --location 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users' \

# DELETE
```
curl --location --request DELETE 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users/4' \
curl --location --request DELETE 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users/4' \
--header 'Authorization: Bearer ABC123'
```

# PUT
A PUT command is to do a FULL replacement of the entry in the table. For partial updates see PATCH
```
curl --location --request PUT 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users/4' \
curl --location --request PUT 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users/4' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: text/plain' \
--header 'Content-type: application/json' \
Expand All @@ -127,7 +127,7 @@ curl --location --request PUT 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/
# PATCH
A PATCH command is to do a PARTIAL replacement of the entry in the table. For full updates see PUT
```
curl --location --request PATCH 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/lite/users/4' \
curl --location --request PATCH 'https://starbasedb.{YOUR-IDENTIFIER}.workers.dev/rest/users/4' \
--header 'Authorization: Bearer ABC123' \
--header 'Content-Type: text/plain' \
--header 'Content-type: application/json' \
Expand Down
2 changes: 1 addition & 1 deletion src/literest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class LiteREST {
* @returns An object containing the method, table name, id, search parameters, and body.
*/
private async parseRequest(request: Request): Promise<{ method: string, tableName: string, id?: string, searchParams: URLSearchParams, body?: any }> {
const liteRequest = new Request(request.url.replace('/lite', ''), request);
const liteRequest = new Request(request.url.replace('/rest', ''), request);
const url = new URL(liteRequest.url);
const pathParts = url.pathname.split('/').filter(Boolean);

Expand Down
File renamed without changes.