ronin
Version:
Access your RONIN database via TypeScript.
711 lines (702 loc) • 24.1 kB
JavaScript
// src/utils/errors.ts
var ClientError = class extends Error {
message;
code;
constructor(details) {
super(details.message);
this.name = "ClientError";
this.message = details.message;
this.code = details.code;
}
};
var getResponseBody = async (response, options) => {
if (response.ok) return response.json();
const text = await response.text();
let json;
try {
json = JSON.parse(text);
} catch (_err) {
throw new ClientError({
message: `${options?.errorPrefix ? `${options.errorPrefix} ` : ""}${text}`,
code: "JSON_PARSE_ERROR"
});
}
if (json.error) {
json.error.message = `${options?.errorPrefix ? `${options.errorPrefix} ` : ""}${json.error.message}`;
throw new ClientError(json.error);
}
return json;
};
// src/storage.ts
var isStorableObject = (value) => typeof File !== "undefined" && value instanceof File || typeof ReadableStream !== "undefined" && value instanceof ReadableStream || typeof Blob !== "undefined" && value instanceof Blob || typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer || typeof Buffer !== "undefined" && Buffer.isBuffer(value);
var extractStorableObjects = (queries) => queries.reduce(
(references, query, queryIndex) => {
return [
// biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor.
...references,
...Object.entries(query).reduce(
(references2, [queryType, query2]) => {
if (!["set", "add"].includes(queryType)) return references2;
return [
// biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor.
...references2,
...Object.entries(query2).reduce(
(references3, [schema, instructions]) => {
const fields = instructions[queryType === "set" ? "to" : "with"];
return [
// biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor.
...references3,
...Object.entries(fields || {}).reduce(
(references4, [name, value]) => {
if (!isStorableObject(value)) return references4;
const blobValue = value;
const storarableObject = {
query: {
index: queryIndex,
type: queryType
},
schema,
field: name,
value: blobValue
};
if ("type" in blobValue) {
storarableObject.contentType = blobValue.type;
}
if ("name" in blobValue) {
storarableObject.name = blobValue.name;
}
return [...references4, storarableObject];
},
[]
)
];
},
[]
)
];
},
[]
)
];
},
[]
);
var uploadStorableObjects = (storableObjects, options = {}) => {
const fetcher = typeof options?.fetch === "function" ? options.fetch : fetch;
const requests = storableObjects.map(
async ({ name, value, contentType }) => {
const headers = new Headers();
headers.set("Authorization", `Bearer ${options.token}`);
if (contentType) {
headers.set("Content-Type", contentType);
}
if (name) {
headers.set(
"Content-Disposition",
`form-data; filename="${encodeURIComponent(name)}"`
);
}
const request = new Request("https://storage.ronin.co/", {
method: "PUT",
body: value,
headers
});
const response = await fetcher(request);
return getResponseBody(response, {
errorPrefix: "An error occurred while uploading the binary objects included in the provided queries. Error:"
});
}
);
return Promise.all(requests);
};
var processStorableObjects = async (queries, upload) => {
const objects = extractStorableObjects(queries);
if (objects.length > 0) {
const storedObjects = await upload(objects);
for (let index = 0; index < objects.length; index++) {
const { query, schema, field } = objects[index];
const reference = storedObjects[index];
queries[query.index][query.type][schema][query.type === "set" ? "to" : "with"][field] = reference;
}
}
return queries;
};
// src/utils/constants.ts
import { DDL_QUERY_TYPES, DML_QUERY_TYPES_WRITE } from "@ronin/compiler";
var WRITE_QUERY_TYPES = [
...DML_QUERY_TYPES_WRITE,
...DDL_QUERY_TYPES.filter((item) => item !== "list")
];
// src/utils/helpers.ts
import { getProperty, setProperty } from "@ronin/syntax/queries";
var toDashCase = (string) => {
const capitalize = (str) => {
const lower = str.toLowerCase();
return lower.substring(0, 1).toUpperCase() + lower.substring(1, lower.length);
};
const parts = string?.replace(/([A-Z])+/g, capitalize)?.split(/(?=[A-Z])|[.\-\s_]/).map((x) => x.toLowerCase()) ?? [];
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0];
return parts.reduce((acc, part) => `${acc}-${part.toLowerCase()}`);
};
var formatDateFields = (record, dateFields) => {
for (const field of dateFields) {
const value = getProperty(record, field);
if (typeof value === "undefined" || value === null) continue;
setProperty(record, field, new Date(value));
}
};
var mergeOptions = (...options) => {
return options.reduce((acc, opt) => {
const resolvedOpt = typeof opt === "function" ? opt() : opt;
Object.assign(acc, resolvedOpt);
return acc;
}, {});
};
var validateToken = (options = {}) => {
if (!options.token && typeof process !== "undefined") {
const token = typeof process?.env !== "undefined" ? process.env.RONIN_TOKEN : typeof import.meta?.env !== "undefined" ? import.meta.env.RONIN_TOKEN : void 0;
if (!token || token === "undefined") {
const message = "Please specify the `RONIN_TOKEN` environment variable or set the `token` option when invoking RONIN.";
throw new Error(message);
}
options.token = token;
}
if (!options.token) {
let message = "When invoking RONIN from an edge runtime, the";
message += " `token` option must be set.";
throw new Error(message);
}
};
var omit = (obj, keys) => {
if (!obj) return {};
if (!keys || keys.length === 0) return obj;
return keys.reduce(
(acc, key) => {
delete acc[key];
return acc;
},
{ ...obj }
);
};
// src/utils/triggers.ts
import {
DDL_QUERY_TYPES as DDL_QUERY_TYPES2,
QUERY_TYPES,
QUERY_TYPES_READ,
QUERY_TYPES_WRITE
} from "@ronin/compiler";
var EMPTY = Symbol("empty");
var getModel = (instruction) => {
const key = Object.keys(instruction)[0];
let model = String(key);
let multipleRecords = false;
if (model.endsWith("s")) {
model = model.substring(0, model.length - 1);
multipleRecords = true;
}
return {
key,
// Convert camel case (e.g. `subscriptionItems`) into slugs
// (e.g. `subscription-items`).
model: toDashCase(model),
multipleRecords
};
};
var getMethodName = (triggerType, queryType) => {
const capitalizedQueryType = queryType[0].toUpperCase() + queryType.slice(1);
return triggerType === "during" ? queryType : triggerType + capitalizedQueryType;
};
var normalizeResults = (result) => {
const value = Array.isArray(result) ? result : result === EMPTY ? [] : [result];
return structuredClone(value);
};
var invokeTriggers = async (triggerType, definition, options) => {
const { triggers, database, client } = options;
const { query } = definition;
const queryType = Object.keys(query)[0];
let queryModel;
let queryModelDashed;
let multipleRecords;
let oldInstruction;
if (DDL_QUERY_TYPES2.includes(queryType)) {
queryModel = queryModelDashed = "model";
multipleRecords = false;
oldInstruction = query[queryType];
} else {
const queryInstructions = query[queryType];
({
key: queryModel,
model: queryModelDashed,
multipleRecords
} = getModel(queryInstructions));
oldInstruction = queryInstructions[queryModel];
}
const triggerFile = database ? "sink" : queryModelDashed;
const triggersForModel = triggers[triggerFile];
const triggerName = getMethodName(triggerType, queryType);
const queryInstruction = oldInstruction ? structuredClone(oldInstruction) : {};
if (triggersForModel && triggerName in triggersForModel) {
const implicit = definition.implicit ?? false;
const trigger = triggersForModel[triggerName];
const triggerOptions = {
implicit,
client,
...triggerFile === "sink" ? { model: queryModel, database } : {}
};
const triggerResult = await (triggerType === "following" ? trigger(
queryInstruction,
multipleRecords,
normalizeResults(definition.resultBefore),
normalizeResults(definition.resultAfter),
triggerOptions
) : trigger(
queryInstruction,
multipleRecords,
triggerOptions
));
if (triggerType === "before") {
return { queries: triggerResult };
}
if (triggerType === "during") {
const result = triggerResult;
let newQuery = query;
if (result && QUERY_TYPES.some((type) => type in result)) {
newQuery = result;
} else {
newQuery = {
[queryType]: {
[queryModel]: result
}
};
}
return { queries: [newQuery] };
}
if (triggerType === "after") {
return { queries: triggerResult };
}
if (triggerType === "resolving") {
const result = triggerResult;
return { queries: [], result };
}
}
return { queries: [], result: EMPTY };
};
var runQueriesWithTriggers = async (queries, options = {}) => {
const { triggers, waitUntil, requireTriggers } = options;
const triggerErrorType = requireTriggers !== "all" ? ` ${requireTriggers}` : "";
const triggerError = new ClientError({
message: `Please define "during" triggers for the provided${triggerErrorType} queries.`,
code: "TRIGGER_REQUIRED"
});
if (!triggers) {
if (requireTriggers) throw triggerError;
return runQueries(queries, options);
}
const client = createSyntaxFactory(omit(options, ["requireTriggers"]));
if (typeof process === "undefined" && !waitUntil) {
let message = 'In the case that the "ronin" package receives a value for';
message += " its `triggers` option, it must also receive a value for its";
message += " `waitUntil` option. This requirement only applies when using";
message += " an edge runtime and ensures that the edge worker continues to";
message += ' execute until all "following" triggers have been executed.';
throw new Error(message);
}
let queryList = queries.map(({ query, database }) => ({ query, result: EMPTY, database }));
await Promise.all(
queryList.map(async ({ query, database, implicit }, index) => {
const triggerResults = await invokeTriggers(
"before",
{ query, implicit },
{ triggers, database, client }
);
const queriesToInsert = triggerResults.queries.map((query2) => ({
query: query2,
result: EMPTY,
database,
implicit: true
}));
queryList.splice(index, 0, ...queriesToInsert);
})
);
await Promise.all(
queryList.map(async ({ query, database, implicit }, index) => {
const triggerResults = await invokeTriggers(
"during",
{ query, implicit },
{ triggers, database, client }
);
if (triggerResults.queries && triggerResults.queries.length > 0) {
queryList[index].query = triggerResults.queries[0];
return;
}
if (requireTriggers) {
const queryType = Object.keys(query)[0];
const requiredTypes = requireTriggers === "read" ? QUERY_TYPES_READ : requireTriggers === "write" ? QUERY_TYPES_WRITE : QUERY_TYPES;
if (requiredTypes.includes(queryType)) throw triggerError;
}
})
);
await Promise.all(
queryList.map(async ({ query, database, implicit }, index) => {
const triggerResults = await invokeTriggers(
"after",
{ query, implicit },
{ triggers, database, client }
);
const queriesToInsert = triggerResults.queries.map((query2) => ({
query: query2,
result: EMPTY,
database,
implicit: true
}));
queryList.splice(index + 1, 0, ...queriesToInsert);
})
);
queryList = queryList.flatMap((details, index) => {
const { query, database } = details;
if (query.set || query.alter) {
let newQuery;
if (query.set) {
const modelSlug = Object.keys(query.set)[0];
newQuery = {
get: {
[modelSlug]: {
with: query.set[modelSlug].with
}
}
};
} else {
newQuery = {
list: {
model: query.alter.model
}
};
}
const diffQuery = {
query: newQuery,
diffForIndex: index + 1,
result: EMPTY,
database
};
return [diffQuery, details];
}
return [details];
});
await Promise.all(
queryList.map(async ({ query, database, implicit }, index) => {
const triggerResults = await invokeTriggers(
"resolving",
{ query, implicit },
{ triggers, database, client }
);
queryList[index].result = triggerResults.result;
})
);
const queriesWithoutResults = queryList.map((query, index) => ({ ...query, index })).filter((query) => query.result === EMPTY);
if (queriesWithoutResults.length > 0) {
const resultsFromDatabase = await runQueries(queriesWithoutResults, options);
for (let index = 0; index < resultsFromDatabase.length; index++) {
const query = queriesWithoutResults[index];
const result = resultsFromDatabase[index].result;
queryList[query.index].result = result;
}
}
for (let index = 0; index < queryList.length; index++) {
const { query, result, database, implicit } = queryList[index];
const queryType = Object.keys(query)[0];
if (!WRITE_QUERY_TYPES.includes(queryType)) continue;
const diffMatch = queryList.find((item) => item.diffForIndex === index);
let resultBefore = diffMatch ? diffMatch.result : EMPTY;
let resultAfter = result;
if (queryType === "remove" || queryType === "drop") {
resultBefore = result;
resultAfter = EMPTY;
}
const promise = invokeTriggers(
"following",
{ query, resultBefore, resultAfter, implicit },
{ triggers, database, client }
);
const clearPromise = promise.then(
() => {
},
(error) => Promise.reject(error)
);
if (waitUntil) waitUntil(clearPromise);
}
return queryList.filter(
(query) => typeof query.diffForIndex === "undefined" && typeof query.implicit === "undefined"
).map(({ result, database }) => ({
result,
database
}));
};
// src/queries.ts
var runQueries = async (queries, options = {}) => {
validateToken(options);
let hasWriteQuery = null;
let hasSingleQuery = true;
const operations = queries.reduce(
(acc, details) => {
const { database = "default" } = details;
if (!acc[database]) acc[database] = {};
if (database !== "default") hasSingleQuery = false;
if ("query" in details) {
const { query } = details;
if (!acc[database].queries) acc[database].queries = [];
acc[database].queries.push(query);
const queryType = Object.keys(query)[0];
hasWriteQuery = hasWriteQuery || WRITE_QUERY_TYPES.includes(queryType);
return acc;
}
const { statement } = details;
if (!acc[database].nativeQueries) acc[database].nativeQueries = [];
acc[database].nativeQueries.push({
query: statement.statement,
values: statement.params
});
return acc;
},
{}
);
const requestBody = hasSingleQuery ? operations.default : operations;
const hasCachingSupport = "cache" in new Request("https://ronin.co");
const request = new Request("https://data.ronin.co", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${options.token}`
},
body: JSON.stringify(requestBody),
// Disable cache if write queries are performed, as those must be
// guaranteed to reach RONIN.
...hasWriteQuery && hasCachingSupport ? { cache: "no-store" } : {},
// Allow for passing custom `fetch` options (e.g. in Next.js).
...typeof options?.fetch === "object" ? options.fetch : {}
});
const fetcher = typeof options?.fetch === "function" ? options.fetch : fetch;
const response = await fetcher(request);
const responseResults = await getResponseBody(response);
const startFormatting = performance.now();
const formattedResults = [];
if ("results" in responseResults) {
const usableResults = responseResults.results;
const finalResults = formatResults(usableResults);
formattedResults.push(...finalResults.map((result) => ({ result })));
} else {
for (const [database, { results }] of Object.entries(responseResults)) {
const finalResults = formatResults(results);
formattedResults.push(...finalResults.map((result) => ({ result, database })));
}
}
const endFormatting = performance.now();
const VERBOSE_LOGGING = typeof process !== "undefined" && process?.env && process.env.__RENDER_DEBUG_LEVEL === "verbose" || typeof import.meta?.env !== "undefined" && import.meta.env.__RENDER_DEBUG_LEVEL === "verbose";
if (VERBOSE_LOGGING) {
console.log(`Formatting took ${endFormatting - startFormatting}ms`);
}
return formattedResults;
};
async function runQueriesWithStorageAndTriggers(queries, options = {}) {
const singleDatabase = Array.isArray(queries);
const normalizedQueries = singleDatabase ? { default: queries } : queries;
const queriesWithReferences = (await Promise.all(
Object.entries(normalizedQueries).map(async ([database, queries2]) => {
const populatedQueries = await processStorableObjects(queries2, (objects) => {
return uploadStorableObjects(objects, options);
});
return populatedQueries.map((query) => ({
query,
database: database === "default" ? void 0 : database
}));
})
)).flat();
const results = await runQueriesWithTriggers(queriesWithReferences, options);
if (singleDatabase)
return results.filter(({ database }) => !database).map(({ result }) => result);
return results.reduce(
(acc, { result, database = "default" }) => {
if (!acc[database]) acc[database] = [];
acc[database].push(result);
return acc;
},
{}
);
}
var formatIndividualResult = (result) => {
if ("amount" in result && typeof result.amount !== "undefined" && result.amount !== null) {
return Number(result.amount);
}
const dateFields = "modelFields" in result ? Object.entries(result.modelFields).filter(([, type]) => type === "date").map(([slug]) => slug) : [];
if ("record" in result) {
if (result.record === null) return null;
formatDateFields(result.record, dateFields);
return result.record;
}
if ("records" in result) {
for (const record of result.records) {
formatDateFields(record, dateFields);
}
const formattedRecords = result.records;
if (typeof result.moreBefore !== "undefined")
formattedRecords.moreBefore = result.moreBefore;
if (typeof result.moreAfter !== "undefined")
formattedRecords.moreAfter = result.moreAfter;
return formattedRecords;
}
return result;
};
var formatResults = (results) => {
const formattedResults = [];
for (const result of results) {
if ("models" in result) {
formattedResults.push(
Object.fromEntries(
Object.entries(result.models).map(([model, result2]) => {
return [model, formatIndividualResult(result2)];
})
)
);
continue;
}
formattedResults.push(formatIndividualResult(result));
}
return formattedResults;
};
// src/utils/handlers.ts
var queriesHandler = async (queries, options = {}) => {
if ("statements" in queries) {
const results = await runQueries(
queries.statements.map((statement) => ({ statement })),
options
);
return results.map(({ result }) => result);
}
if (options.database) {
const queryList = { [options.database]: queries };
const result = await runQueriesWithStorageAndTriggers(queryList, options);
return result[options.database];
}
return runQueriesWithStorageAndTriggers(queries, options);
};
var queryHandler = async (query, options) => {
const input = "statement" in query ? { statements: [query.statement] } : [query];
const results = await queriesHandler(input, options);
return results[0];
};
// src/index.ts
import {
QUERY_SYMBOLS
} from "@ronin/compiler";
import {
getBatchProxy,
getBatchProxySQL,
getSyntaxProxy,
getSyntaxProxySQL
} from "@ronin/syntax/queries";
var createSyntaxFactory = (options) => {
const callback = (defaultQuery, queryOptions) => {
const query = defaultQuery;
return queryHandler(query[QUERY_SYMBOLS.QUERY], mergeOptions(options, queryOptions));
};
const replacer = (value) => isStorableObject(value) ? value : void 0;
return {
// Query types for interacting with records.
get: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.get`,
callback,
replacer
}),
set: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.set`,
callback,
replacer
}),
add: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.add`,
callback,
replacer
}),
remove: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.remove`,
callback,
replacer
}),
count: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.count`,
callback,
replacer
}),
// Query types for interacting with the database schema.
list: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.list`,
callback,
replacer
}),
create: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.create`,
callback,
replacer
}),
alter: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.alter`,
callback,
replacer
}),
drop: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.drop`,
callback,
replacer
}),
// Function for executing a transaction containing multiple queries.
batch: (operations, queryOptions) => {
const batchOperations = operations;
const queries = getBatchProxy(batchOperations).map(({ structure }) => structure);
const finalOptions = mergeOptions(options, queryOptions);
return queriesHandler(queries, finalOptions);
},
sql: getSyntaxProxySQL({
callback: (statement) => queryHandler({ statement }, mergeOptions(options, {}))
}),
sqlBatch: (operations, queryOptions) => {
const batchOperations = operations;
const statements = getBatchProxySQL(batchOperations);
const finalOptions = mergeOptions(options, queryOptions);
return queriesHandler({ statements }, finalOptions);
}
};
};
var factory = createSyntaxFactory({});
var get = factory.get;
var set = factory.set;
var add = factory.add;
var remove = factory.remove;
var count = factory.count;
var list = factory.list;
var create = factory.create;
var alter = factory.alter;
var drop = factory.drop;
var batch = factory.batch;
var sql = factory.sql;
var sqlBatch = factory.sqlBatch;
var index_default = createSyntaxFactory;
export {
ClientError,
isStorableObject,
processStorableObjects,
runQueriesWithStorageAndTriggers,
createSyntaxFactory,
get,
set,
add,
remove,
count,
list,
create,
alter,
drop,
batch,
sql,
sqlBatch,
index_default
};