Shared UI kit
database.js 120 lines
Files · 11
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
export function validate(schema, data) {
if (!data || typeof data !== "object" || Array.isArray(data))
throw Error("Expected an object.");
for (const key of schema.required || [])
if (data[key] === undefined) throw Error(`${key} is required.`);
for (const [key, value] of Object.entries(data)) {
const field = schema.properties[key];
if (!field) {
if (schema.additionalProperties === false)
throw Error(`Unknown field: ${key}.`);
continue;
}
const types = [].concat(field.type);
if (
!types.some((type) =>
type === "null"
? value === null
: type === "integer"
? Number.isInteger(value)
: type === "number"
? typeof value === "number" && Number.isFinite(value)
: typeof value === type,
)
)
throw Error(`${key} must be ${types.join(" or ")}.`);
if (field.enum && !field.enum.includes(value))
throw Error(`Invalid ${key}.`);
if ("const" in field && value !== field.const)
throw Error(`Invalid ${key}.`);
if (field.minimum !== undefined && value < field.minimum)
throw Error(`${key} must be at least ${field.minimum}.`);
if (field.exclusiveMinimum !== undefined && value <= field.exclusiveMinimum)
throw Error(`${key} must be greater than ${field.exclusiveMinimum}.`);
if (
value !== null &&
field.pattern &&
!new RegExp(field.pattern).test(value)
)
throw Error(`Invalid ${key} format.`);
if (
typeof value === "string" &&
field.pattern === "^\\d{4}-\\d{2}-\\d{2}$" &&
(Number.isNaN(Date.parse(value)) ||
new Date(value).toISOString().slice(0, 10) !== value)
)
throw Error(`Invalid ${key} date.`);
}
return data;
}
export function database(L, ddl, manifest) {
let tail = Promise.resolve();
const ready = (async () => {
for (const sql of ddl
.split(";")
.map((s) => s.trim())
.filter(Boolean)) {
const table = /CREATE TABLE (\w+)/.exec(sql)[1];
if (
!(
await L.db.query(
"SELECT name FROM sqlite_master WHERE type = ? AND name = ?",
["table", table],
)
).length
)
await L.db.exec(sql);
}
})();
const run = (operation) => {
const result = tail.then(() => ready).then(operation);
tail = result.catch(() => {});
return result;
};
const get = async (table, id) =>
(await L.db.query(`SELECT * FROM ${table} WHERE id = ?`, [id]))[0];
const insert = async (table, data) => {
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
await L.db.exec(
`INSERT INTO ${table} (${entries.map(([k]) => k).join(",")}) VALUES (${entries.map(() => "?").join(",")})`,
entries.map(([, v]) => v),
);
return (
await L.db.query(
`SELECT * FROM ${table} WHERE rowid = last_insert_rowid()`,
)
)[0];
};
const update = async (table, id, data) => {
const entries = Object.entries(data).filter(
([k, v]) => k !== "id" && v !== undefined,
);
if (entries.length)
await L.db.exec(
`UPDATE ${table} SET ${entries.map(([k]) => `${k} = ?`).join(",")} WHERE id = ?`,
[...entries.map(([, v]) => v), id],
);
return get(table, id);
};
const emit = async (name, row) => {
const schema = manifest.requested_permissions.events[name].schema;
const payload = Object.fromEntries(
Object.entries(row).filter(([k]) => k in schema.properties),
);
validate(schema, payload);
await L.events.emit(name, payload);
};
return {
ready,
run,
get,
insert,
update,
emit,
query: (...args) => L.db.query(...args),
exec: (...args) => L.db.exec(...args),
check: (name, data) =>
validate(manifest.requested_permissions.listeners[name].schema, data),
};
}