Skip to content

Instantly share code, notes, and snippets.

@doublerebel
Created April 17, 2024 20:48
Show Gist options
  • Save doublerebel/f1fd83ed52f783867fad45cf39cef585 to your computer and use it in GitHub Desktop.
Save doublerebel/f1fd83ed52f783867fad45cf39cef585 to your computer and use it in GitHub Desktop.
cloudflare-internal:d1-api D1Database API for Cloudflare Workers
// Copyright (c) 2023 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0
class D1Database {
fetcher;
constructor(fetcher) {
this.fetcher = fetcher;
}
prepare(query) {
return new D1PreparedStatement(this, query);
}
// DEPRECATED, TO BE REMOVED WITH NEXT BREAKING CHANGE
async dump() {
const response = await this.fetcher.fetch('http://d1/dump', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
});
if (response.status !== 200) {
try {
const err = (await response.json());
throw new Error(`D1_DUMP_ERROR: ${err.error}`, {
cause: new Error(err.error),
});
}
catch (e) {
throw new Error(`D1_DUMP_ERROR: Status + ${response.status}`, {
cause: new Error(`Status ${response.status}`),
});
}
}
return await response.arrayBuffer();
}
async batch(statements) {
const exec = (await this._sendOrThrow('/query', statements.map((s) => s.statement), statements.map((s) => s.params), 'ROWS_AND_COLUMNS'));
return exec.map(toArrayOfObjects);
}
async exec(query) {
const lines = query.trim().split('\n');
const _exec = await this._send('/execute', lines, [], 'NONE');
const exec = Array.isArray(_exec) ? _exec : [_exec];
const error = exec
.map((r) => {
return r.error ? 1 : 0;
})
.indexOf(1);
if (error !== -1) {
throw new Error(`D1_EXEC_ERROR: Error in line ${error + 1}: ${lines[error]}: ${exec[error]?.error}`, {
cause: new Error(`Error in line ${error + 1}: ${lines[error]}: ${exec[error]?.error}`),
});
}
else {
return {
count: exec.length,
duration: exec.reduce((p, c) => {
return p + c.meta['duration'];
}, 0),
};
}
}
async _sendOrThrow(endpoint, query, params, resultsFormat) {
const results = await this._send(endpoint, query, params, resultsFormat);
const firstResult = firstIfArray(results);
if (!firstResult.success) {
throw new Error(`D1_ERROR: ${firstResult.error}`, {
cause: new Error(firstResult.error),
});
}
else {
return results;
}
}
async _send(endpoint, query, params, resultsFormat) {
/* this needs work - we currently only support ordered ?n params */
const body = JSON.stringify(Array.isArray(query)
? query.map((s, index) => {
return { sql: s, params: params[index] };
})
: {
sql: query,
params: params,
});
const url = new URL(endpoint, 'http://d1');
url.searchParams.set('resultsFormat', resultsFormat);
const response = await this.fetcher.fetch(url.href, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body,
});
try {
const answer = await toJson(response);
if (Array.isArray(answer)) {
return answer.map((r) => mapD1Result(r));
}
else {
return mapD1Result(answer);
}
}
catch (_e) {
const e = _e;
const message = e.cause?.message ||
e.message ||
'Something went wrong';
throw new Error(`D1_ERROR: ${message}`, {
cause: new Error(message),
});
}
}
}
class D1PreparedStatement {
database;
statement;
params;
constructor(database, statement, values) {
this.database = database;
this.statement = statement;
this.params = values || [];
}
bind(...values) {
// Validate value types
const transformedValues = values.map((r) => {
const rType = typeof r;
if (rType === 'number' || rType === 'string') {
return r;
}
else if (rType === 'boolean') {
return r ? 1 : 0;
}
else if (rType === 'object') {
// nulls are objects in javascript
if (r == null)
return r;
// arrays with uint8's are good
if (Array.isArray(r) &&
r.every((b) => {
return typeof b == 'number' && b >= 0 && b < 256;
}))
return r;
// convert ArrayBuffer to array
if (r instanceof ArrayBuffer) {
return Array.from(new Uint8Array(r));
}
// convert view to array
if (ArrayBuffer.isView(r)) {
// For some reason TS doesn't think this is valid, but it is!
return Array.from(r);
}
}
throw new Error(`D1_TYPE_ERROR: Type '${rType}' not supported for value '${r}'`, {
cause: new Error(`Type '${rType}' not supported for value '${r}'`),
});
});
return new D1PreparedStatement(this.database, this.statement, transformedValues);
}
async first(colName) {
const info = firstIfArray(await this.database._sendOrThrow('/query', this.statement, this.params, 'ROWS_AND_COLUMNS'));
const results = toArrayOfObjects(info).results;
const hasResults = results.length > 0;
if (!hasResults)
return null;
const firstResult = results[0];
if (colName !== undefined) {
if (hasResults && firstResult[colName] === undefined) {
throw new Error(`D1_COLUMN_NOTFOUND: Column not found (${colName})`, {
cause: new Error('Column not found'),
});
}
return firstResult[colName];
}
else {
return firstResult;
}
}
async run() {
return firstIfArray(await this.database._sendOrThrow('/execute', this.statement, this.params, 'NONE'));
}
async all() {
return toArrayOfObjects(firstIfArray(await this.database._sendOrThrow('/query', this.statement, this.params, 'ROWS_AND_COLUMNS')));
}
async raw(options) {
const s = firstIfArray(await this.database._sendOrThrow('/query', this.statement, this.params, 'ROWS_AND_COLUMNS'));
// If no results returned, return empty array
if (!('results' in s))
return [];
// If ARRAY_OF_OBJECTS returned, extract cells
if (Array.isArray(s.results)) {
const raw = [];
for (const row of s.results) {
if (options?.columnNames && raw.length === 0) {
raw.push(Array.from(Object.keys(row)));
}
const entry = Object.keys(row).map((k) => {
return row[k];
});
raw.push(entry);
}
return raw;
}
else {
// Otherwise, data is already in the correct format
return [
...(options?.columnNames ? [s.results.columns] : []),
...s.results.rows,
];
}
}
}
function firstIfArray(results) {
return Array.isArray(results) ? results[0] : results;
}
// This shim may be used against an older version of D1 that doesn't support
// the ROWS_AND_COLUMNS/NONE interchange format, so be permissive here
function toArrayOfObjects(response) {
// If 'results' is missing from upstream, add an empty array
if (!('results' in response))
return {
...response,
results: [],
};
const results = response.results;
if (Array.isArray(results)) {
return { ...response, results };
}
else {
const { rows, columns } = results;
return {
...response,
results: rows.map((row) => Object.fromEntries(row.map((cell, i) => [columns[i], cell]))),
};
}
}
function mapD1Result(result) {
// The rest of the app can guarantee that success is true/false, but from the API
// we only guarantee that error is present/absent.
return result.error
? {
success: false,
meta: result.meta || {},
error: result.error,
}
: {
success: true,
meta: result.meta || {},
...('results' in result ? { results: result.results } : {}),
};
}
async function toJson(response) {
const body = await response.text();
try {
return JSON.parse(body);
}
catch (e) {
throw new Error(`Failed to parse body as JSON, got: ${body}`);
}
}
export default function makeBinding(env) {
return new D1Database(env.fetcher);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment