Created
April 17, 2024 20:48
-
-
Save doublerebel/f1fd83ed52f783867fad45cf39cef585 to your computer and use it in GitHub Desktop.
cloudflare-internal:d1-api D1Database API for Cloudflare Workers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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