UNPKG

d1-sql-tag

Version:

A template literal for working with Cloudflare D1 database

220 lines (218 loc) 6.48 kB
//#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