d1-sql-tag
Version:
A template literal for working with Cloudflare D1 database
220 lines (218 loc) • 6.48 kB
JavaScript
//#region src/sql-tag.ts
let batchId = 0;
const rowTypeSymbol = Symbol("rowType");
function createD1SqlTag(db, options) {
const sqlTag = (strings, ...values) => {
return {
build() {
return buildPreparedStatement(db, options, strings, values);
},
all() {
return buildPreparedStatement(db, options, strings, values).all();
},
run() {
return buildPreparedStatement(db, options, strings, values).run();
},
templateStrings: strings,
templateValues: values
};
};
sqlTag.batch = async (statements) => {
const queries = statements.map((it) => it.query);
const id = makeBatchId();
options?.beforeQuery?.(id, queries);
const start = Date.now();
const result = await db.batch(statements.map((it) => makeNativeStatement(db, it)));
const duration = Date.now() - start;
options?.afterQuery?.(id, queries, result, duration);
for (let i = 0; i < result.length; i++) {
const statement = statements[i];
const statementResult = result[i];
if (statement && "mapper" in statement) statementResult.results = statementResult.results.map(statement.mapper);
}
return result;
};
sqlTag.join = join;
return sqlTag;
}
function createMockSqlTag(handler) {
const sqlTag = ((strings, ...values) => {
return {
build() {
return buildMockPreparedStatement(handler, strings, values);
},
all() {
return buildMockPreparedStatement(handler, strings, values).all();
},
run() {
return buildMockPreparedStatement(handler, strings, values).run();
},
templateStrings: strings,
templateValues: values
};
});
sqlTag.batch = async (statements) => {
const statementsData = statements.map((it) => ({
query: it.query,
values: it.values
}));
const result = await handler.batch(statementsData);
for (let i = 0; i < result.length; i++) {
const statement = statements[i];
const statementResult = result[i];
if (statement && "mapper" in statement) statementResult.results = statementResult.results.map(statement.mapper);
}
return result;
};
sqlTag.join = join;
sqlTag.handler = handler;
return sqlTag;
}
function buildMockPreparedStatement(handler, templateStrings, templateValues) {
const { query, values } = expandTemplate(templateStrings, templateValues);
return {
async all() {
return await handler.all(query, values);
},
run() {
return handler.run(query, values);
},
map(mapper) {
return {
async all() {
const result = await handler.all(query, values);
result.results = result.results.map(mapper);
return result;
},
run() {
return handler.run(query, values);
},
query,
values,
mapper,
[rowTypeSymbol]: null
};
},
query,
values,
[rowTypeSymbol]: null
};
}
function buildPreparedStatement(db, options, templateStrings, templateValues) {
const { query, values } = expandTemplate(templateStrings, templateValues);
const statement = {
all() {
return executeAll(db, options, statement, null);
},
run() {
return executeRun(db, options, statement);
},
map(mapper) {
const mappedStatement = {
all() {
return executeAll(db, options, mappedStatement, mapper);
},
run() {
return executeRun(db, options, mappedStatement);
},
query,
values,
mapper,
[rowTypeSymbol]: null
};
return mappedStatement;
},
query,
values,
[rowTypeSymbol]: null
};
return statement;
}
function expandTemplate(rootTemplateStrings, rootTemplateValues) {
let query = "";
const values = [];
function expand(templateStrings, templateValues) {
for (let i = 0; i < templateStrings.length; i++) {
if (i > 0) {
const value = templateValues[i - 1] ?? null;
if (value && typeof value === "object" && "templateStrings" in value && "templateValues" in value) expand(value.templateStrings, value.templateValues);
else {
let valueIndex = values.indexOf(value);
if (valueIndex === -1) valueIndex = values.push(value) - 1;
query += `?${valueIndex + 1}`;
}
}
query += templateStrings[i];
}
}
expand(rootTemplateStrings, rootTemplateValues);
return {
query,
values
};
}
async function executeAll(db, options, statement, mapper) {
const batchId$1 = makeBatchId();
options?.beforeQuery?.(batchId$1, [statement.query]);
const start = Date.now();
const result = await makeNativeStatement(db, statement).all();
const duration = Date.now() - start;
options?.afterQuery?.(batchId$1, [statement.query], [result], duration);
if (mapper) result.results = result.results.map(mapper);
return result;
}
async function executeRun(db, options, statement) {
const batchId$1 = makeBatchId();
options?.beforeQuery?.(batchId$1, [statement.query]);
const start = Date.now();
const result = await makeNativeStatement(db, statement).run();
const duration = Date.now() - start;
options?.afterQuery?.(batchId$1, [statement.query], [result], duration);
return result;
}
function makeNativeStatement(db, statement) {
let stmt = db.prepare(statement.query);
if (statement.values.length > 0) stmt = stmt.bind(...statement.values);
return stmt;
}
function makeBatchId() {
batchId += 1;
return batchId;
}
function makeTemplateStrings(strings) {
const templateStrings = strings;
Object.defineProperty(templateStrings, "raw", { value: strings });
return templateStrings;
}
function join(values) {
if (values.length === 0) return {
templateStrings: makeTemplateStrings(["NULL"]),
templateValues: []
};
const strings = [""];
for (let i = 1; i < values.length; i++) strings.push(", ");
strings.push("");
return {
templateStrings: makeTemplateStrings(strings),
templateValues: values
};
}
//#endregion
//#region src/logger.ts
function logQueryResults(queries, results, duration) {
console.log(`D1 batch: ${typeof duration === "number" ? `${duration}ms · ` : ""}${queries.length} queries`);
for (let i = 0; i < queries.length; i++) {
const query = queries[i];
const result = results[i];
if (!query || !result) continue;
console.log(`${i + 1}: ${cleanupSqlQuery(query)}`);
const logSuffix = "rows_read" in result.meta ? ` · ${result.meta.rows_read} read · ${result.meta.rows_written} written` : "";
console.log(` ↳ ${result.meta.duration}ms · ${result.meta.changes} changed` + logSuffix);
}
}
function cleanupSqlQuery(query) {
return query.replace(/\n/g, " ").replace(/\s+/g, " ");
}
//#endregion
export { createD1SqlTag, createMockSqlTag, logQueryResults };
//# sourceMappingURL=index.js.map