miniflare
Version:
Fun, full-featured, fully-local simulator for Cloudflare Workers
202 lines (198 loc) • 8.37 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
for (var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target, i = decorators.length - 1, decorator; i >= 0; i--)
(decorator = decorators[i]) && (result = (kind ? decorator(target, key, result) : decorator(result)) || result);
return kind && result && __defProp(target, key, result), result;
};
// src/workers/d1/database.worker.ts
import assert from "node:assert";
import {
get,
HttpError,
MiniflareDurableObject,
POST,
viewToBuffer
} from "miniflare:shared";
import { z } from "miniflare:zod";
// src/workers/d1/dumpSql.ts
function* dumpSql(db, options) {
yield "PRAGMA defer_foreign_keys=TRUE;";
let filterTables = new Set(options?.tables || []), { noData, noSchema } = options || {}, tables_cursor = db.prepare(`
SELECT name, type, sql
FROM sqlite_schema AS o
WHERE (true) AND type=='table'
AND sql NOT NULL
ORDER BY tbl_name='sqlite_sequence', rowid;
`)(), tables = Array.from(tables_cursor);
for (let { name: table, sql } of tables) {
if (filterTables.size > 0 && !filterTables.has(table)) continue;
if (table === "sqlite_sequence")
noSchema || (yield "DELETE FROM sqlite_sequence;");
else if (table.match(/^sqlite_stat./))
noSchema || (yield "ANALYZE sqlite_schema;");
else {
if (sql.startsWith("CREATE VIRTUAL TABLE"))
throw new Error(
"D1 Export error: cannot export databases with Virtual Tables (fts5)"
);
if (table.startsWith("_cf_") || table.startsWith("sqlite_"))
continue;
sql.match(/CREATE TABLE ['"].*/) ? noSchema || (yield `CREATE TABLE IF NOT EXISTS ${sql.substring(13)};`) : noSchema || (yield `${sql};`);
}
if (noData) continue;
let columns_cursor = db.exec(`PRAGMA table_info=${escapeId(table)}`), columns = Array.from(columns_cursor), select = `SELECT ${columns.map((c) => escapeId(c.name)).join(", ")} FROM ${escapeId(table)};`, rows_cursor = db.exec(select);
for (let dataRow of rows_cursor.raw()) {
let formattedCells = dataRow.map((cell, i) => {
let colType = columns[i].type, cellType = typeof cell;
return cell === null ? "NULL" : cellType === "number" ? cell : cellType === "string" ? outputQuotedEscapedString(cell) : cell instanceof ArrayBuffer ? `X'${Array.prototype.map.call(new Uint8Array(cell), (b) => b.toString(16).padStart(2, "0")).join("")}'` : (console.log({ colType, cellType, cell, column: columns[i] }), "ERROR");
});
yield `INSERT INTO ${escapeId(table)} VALUES(${formattedCells.join(",")});`;
}
}
if (!noSchema) {
let rest_of_schema = db.exec(
"SELECT name, sql FROM sqlite_schema AS o WHERE (true) AND sql NOT NULL AND type IN ('index', 'trigger', 'view') ORDER BY type COLLATE NOCASE /* DESC */;"
);
for (let { name, sql } of rest_of_schema)
filterTables.size > 0 && !filterTables.has(name) || (yield `${sql};`);
}
}
function outputQuotedEscapedString(cell) {
let lfs = !1, crs = !1, quotesOrNewlinesRegexp = /'|(\n)|(\r)/g, escapeQuotesDetectingNewlines = (_, lf, cr) => lf ? (lfs = !0, "\\n") : cr ? (crs = !0, "\\r") : "''", output_string = `'${cell.replace(
quotesOrNewlinesRegexp,
escapeQuotesDetectingNewlines
)}'`;
return crs && (output_string = `replace(${output_string},'\\r',char(13))`), lfs && (output_string = `replace(${output_string},'\\n',char(10))`), output_string;
}
function escapeId(id) {
return `"${id.replace(/"/g, '""')}"`;
}
// src/workers/d1/database.worker.ts
var D1ValueSchema = z.union([
z.number(),
z.string(),
z.null(),
z.number().array()
]), D1QuerySchema = z.object({
sql: z.string(),
params: z.array(D1ValueSchema).nullable().optional()
}), D1QueriesSchema = z.union([D1QuerySchema, z.array(D1QuerySchema)]), D1_EXPORT_PRAGMA = "PRAGMA miniflare_d1_export(?,?,?);", D1ResultsFormatSchema = z.enum(["ARRAY_OF_OBJECTS", "ROWS_AND_COLUMNS", "NONE"]).catch("ARRAY_OF_OBJECTS"), served_by = "miniflare.db", D1_SESSION_COMMIT_TOKEN_HTTP_HEADER = "x-cf-d1-session-commit-token", D1Error = class extends HttpError {
constructor(cause) {
super(500);
this.cause = cause;
}
toResponse() {
let response = { success: !1, error: typeof this.cause == "object" && this.cause !== null && "message" in this.cause && typeof this.cause.message == "string" ? this.cause.message : String(this.cause) };
return Response.json(response);
}
};
function convertParams(params) {
return (params ?? []).map(
(param) => (
// If `param` is an array, assume it's a byte array
Array.isArray(param) ? viewToBuffer(new Uint8Array(param)) : param
)
);
}
function convertRows(rows) {
return rows.map(
(row) => row.map((value) => {
let normalised;
return value instanceof ArrayBuffer ? normalised = Array.from(new Uint8Array(value)) : normalised = value, normalised;
})
);
}
function rowsToObjects(columns, rows) {
return rows.map(
(row) => Object.fromEntries(columns.map((name, i) => [name, row[i]]))
);
}
function sqlStmts(db) {
return {
getChanges: db.prepare(
"SELECT total_changes() AS totalChanges, last_insert_rowid() AS lastRowId"
)
};
}
var D1DatabaseObject = class extends MiniflareDurableObject {
#stmts;
constructor(state, env) {
super(state, env), this.#stmts = sqlStmts(this.db);
}
#changes() {
let changes = get(this.#stmts.getChanges());
return assert(changes !== void 0), changes;
}
#query = (format, query) => {
let beforeTime = performance.now(), beforeSize = this.state.storage.sql.databaseSize, beforeChanges = this.#changes(), params = convertParams(query.params ?? []), cursor = this.db.prepare(query.sql)(...params), columns = cursor.columnNames, rows = convertRows(Array.from(cursor.raw())), results;
format === "ROWS_AND_COLUMNS" ? results = { columns, rows } : results = rowsToObjects(columns, rows);
let afterTime = performance.now(), afterSize = this.state.storage.sql.databaseSize, afterChanges = this.#changes(), duration = afterTime - beforeTime, changes = afterChanges.totalChanges - beforeChanges.totalChanges, hasChanges = changes !== 0, lastRowChanged = afterChanges.lastRowId !== beforeChanges.lastRowId, changed = hasChanges || lastRowChanged || afterSize !== beforeSize;
return {
success: !0,
results,
meta: {
served_by,
duration,
changes,
last_row_id: afterChanges.lastRowId,
changed_db: changed,
size_after: afterSize,
rows_read: cursor.rowsRead,
rows_written: cursor.rowsWritten
}
};
};
#txn(queries, format) {
if (queries = queries.filter(
(query) => query.sql.replace(/^\s+--.*/gm, "").trim().length > 0
), queries.length === 0) {
let error = new Error("No SQL statements detected.");
throw new D1Error(error);
}
try {
return this.state.storage.transactionSync(
() => queries.map(this.#query.bind(this, format))
);
} catch (e) {
throw new D1Error(e);
}
}
queryExecute = async (req) => {
let queries = D1QueriesSchema.parse(await req.json());
if (Array.isArray(queries) || (queries = [queries]), this.#isExportPragma(queries))
return this.#doExportData(queries);
let { searchParams } = new URL(req.url), resultsFormat = D1ResultsFormatSchema.parse(
searchParams.get("resultsFormat")
);
return Response.json(this.#txn(queries, resultsFormat), {
headers: {
[D1_SESSION_COMMIT_TOKEN_HTTP_HEADER]: await this.state.storage.getCurrentBookmark()
}
});
};
#isExportPragma(queries) {
return queries.length === 1 && queries[0].sql === D1_EXPORT_PRAGMA && (queries[0].params?.length || 0) >= 2;
}
#doExportData(queries) {
let [noSchema, noData, ...tables] = queries[0].params, options = {
noSchema: !!noSchema,
noData: !!noData,
tables
};
return Response.json({
success: !0,
results: [Array.from(dumpSql(this.state.storage.sql, options))],
meta: {}
});
}
};
__decorateClass([
POST("/query"),
POST("/execute")
], D1DatabaseObject.prototype, "queryExecute", 2);
export {
D1DatabaseObject,
D1Error
};
//# sourceMappingURL=database.worker.js.map