@sidekick-coder/db
Version:
Cli Tool to manipulate data from diferent sources
660 lines (650 loc) • 18.6 kB
JavaScript
import * as valibot from 'valibot';
import path, { dirname, resolve } from 'path';
import fs from 'fs';
import 'url';
import { parse as parse$1, stringify as stringify$1 } from 'yaml';
import qs from 'qs';
import * as inquirer from '@inquirer/prompts';
import crypto from 'crypto';
import sift from 'sift';
import { omit, pick, orderBy } from 'lodash-es';
// src/core/validator/valibot.ts
// src/utils/tryCatch.ts
async function tryCatch(tryer) {
try {
const result = await tryer();
return [result, null];
} catch (error) {
return [null, error];
}
}
tryCatch.sync = function(tryer) {
try {
const result = tryer();
return [result, null];
} catch (error) {
return [null, error];
}
};
var parse = parse$1;
var stringify = stringify$1;
var YAML = {
parse,
stringify
};
function readFileSync(path4) {
const [content, error] = tryCatch.sync(() => fs.readFileSync(path4));
if (error) {
return null;
}
return new Uint8Array(content);
}
readFileSync.text = function(filepath, defaultValue = "") {
const content = readFileSync(filepath);
if (!content) {
return defaultValue;
}
return new TextDecoder().decode(content);
};
readFileSync.json = function(path4, options) {
const content = readFileSync.text(path4);
if (!content) {
return (options == null ? void 0 : options.default) || null;
}
const [json, error] = tryCatch.sync(() => JSON.parse(content, options == null ? void 0 : options.reviver));
return error ? (options == null ? void 0 : options.default) || null : json;
};
readFileSync.yaml = function(path4, options) {
const content = readFileSync.text(path4);
if (!content) {
return (options == null ? void 0 : options.default) || null;
}
const [yml, error] = tryCatch.sync(() => YAML.parse(content, options == null ? void 0 : options.parseOptions));
return error ? (options == null ? void 0 : options.default) || null : yml;
};
var filesystem = {
readSync: readFileSync};
function createReviver(folder) {
return (_, value) => {
if (typeof value == "string" && value.startsWith("./")) {
return resolve(dirname(folder), value);
}
return value;
};
}
var schema = valibot.optional(
valibot.pipe(
valibot.any(),
valibot.transform((value) => {
if (typeof value == "object") {
return value;
}
if (/\.yml$/.test(value)) {
const file = value.replace(/^@/, "");
const folder = dirname(file);
return filesystem.readSync.yaml(value.replace(/^@/, ""), {
reviver: createReviver(folder)
});
}
if (typeof value == "string" && value.includes("=")) {
const result = qs.parse(value, { allowEmptyArrays: true });
return result;
}
if (typeof value == "string" && value.startsWith("{")) {
return JSON.parse(value);
}
if (typeof value == "string" && value.startsWith("[")) {
return JSON.parse(value);
}
return value;
}),
valibot.record(valibot.string(), valibot.any())
)
);
function createPathNode() {
return {
resolve: (...args) => path.resolve(...args),
join: (...args) => path.join(...args),
dirname: (args) => path.dirname(args),
basename: (args) => path.basename(args)
};
}
// src/core/validator/valibot.ts
var stringList = valibot.pipe(
valibot.any(),
valibot.transform((value) => {
if (typeof value === "string") {
return value.split(",");
}
if (Array.isArray(value)) {
return value;
}
}),
valibot.array(valibot.string())
);
function array2(s) {
return valibot.pipe(
v2.union([v2.array(s), s]),
valibot.transform((value) => Array.isArray(value) ? value : [value]),
valibot.array(s)
);
}
function path3(dirname2, path4 = createPathNode()) {
return valibot.pipe(
valibot.string(),
valibot.transform((value) => path4.resolve(dirname2, value))
);
}
function uint8() {
return valibot.pipe(
valibot.any(),
valibot.check((value) => value instanceof Uint8Array),
valibot.transform((value) => value)
);
}
var prompts = {
password: (options) => valibot.optionalAsync(valibot.string(), () => {
return inquirer.password({
message: "Enter password",
...options
});
})
};
var extras = {
array: array2,
vars: schema,
stringList,
path: path3,
uint8,
number: valibot.pipe(
valibot.any(),
valibot.transform(Number),
valibot.check((n) => !isNaN(n)),
valibot.number()
)
};
var vWithExtras = {
...valibot,
extras,
prompts
};
var v2 = vWithExtras;
// src/core/validator/validate.ts
function validate(cb, payload) {
const schema4 = typeof cb === "function" ? cb(v2) : cb;
const { output, issues, success } = v2.safeParse(schema4, payload);
if (!success) {
const flatten = v2.flatten(issues);
const messages = [];
if (flatten.root) {
messages.push(...flatten.root);
}
if (flatten.nested) {
Object.entries(flatten.nested).forEach((entry) => {
const [key, value] = entry;
messages.push(...value.map((v3) => `${key}: ${v3}`));
});
}
const message = messages.length ? messages.join(", ") : "Validation failed";
const error = new Error(message);
error.name = "ValidationError";
Object.assign(error, {
messages
});
throw error;
}
return output;
}
validate.async = async function(cb, payload) {
let schema4;
if (typeof cb === "function") {
schema4 = cb(v2);
} else {
schema4 = cb;
}
const { output, issues, success } = await v2.safeParseAsync(schema4, payload);
if (!success) {
const error = new Error("Validation failed");
const flatten = v2.flatten(issues);
const details = {
...flatten.root,
...flatten.nested
};
Object.assign(error, {
details
});
throw error;
}
return output;
};
// src/providers/vault/encryption.ts
var schema2 = v2.object({
value: v2.union([v2.string(), v2.extras.uint8()]),
salt: v2.optional(v2.string(), crypto.randomBytes(16).toString("hex")),
iv: v2.optional(v2.string(), crypto.randomBytes(16).toString("hex")),
password: v2.string()
});
function encrypt(payload) {
const options = validate(schema2, payload);
const salt = Buffer.from(options.salt, "hex");
const iv = Buffer.from(options.iv, "hex");
const key = crypto.scryptSync(options.password, salt, 32);
const cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
if (options.value instanceof Uint8Array) {
const buffer = Buffer.concat([cipher.update(options.value), cipher.final()]);
return new Uint8Array(buffer);
}
if (typeof options.value === "string") {
const encrypted = cipher.update(options.value, "utf8", "hex");
return encrypted + cipher.final("hex");
}
throw new Error("Invalid type");
}
function decrypt(payload) {
const options = validate(schema2, payload);
const salt = Buffer.from(options.salt, "hex");
const iv = Buffer.from(options.iv, "hex");
const key = crypto.scryptSync(options.password, salt, 32);
const decipher = crypto.createDecipheriv("aes-256-ctr", key, iv);
if (typeof options.value === "string") {
let decrypted = decipher.update(options.value, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
if (options.value instanceof Uint8Array) {
const buffer = Buffer.concat([decipher.update(options.value), decipher.final()]);
return new Uint8Array(buffer);
}
throw new Error("Invalid type");
}
function createEncryption(payload) {
const state = {
salt: (payload == null ? void 0 : payload.salt) || crypto.randomBytes(16).toString("hex"),
iv: (payload == null ? void 0 : payload.iv) || crypto.randomBytes(16).toString("hex"),
password: (payload == null ? void 0 : payload.password) || ""
};
const instance = {
state,
setSalt: function(salt) {
state.salt = salt;
return instance;
},
setIv: function(iv) {
state.iv = iv;
return instance;
},
setPassword: function(password2) {
state.password = password2;
return instance;
},
encrypt: function(value) {
return encrypt({ ...state, value });
},
decrypt: function(value) {
return decrypt({ ...state, value });
}
};
return instance;
}
// src/core/database/where.ts
function parseCondition(condition) {
if (condition.value === "$true") {
return {
or: [],
and: [
{
field: condition.field,
operator: condition.operator,
value: true
}
]
};
}
if (condition.value === "$false") {
return {
or: [],
and: [
{
field: condition.field,
operator: condition.operator,
value: false
}
]
};
}
if (condition.value === "$exists") {
return {
or: [],
and: [
{
field: condition.field,
operator: "exists",
value: true
}
]
};
}
if (typeof condition.value == "string" && condition.value.startsWith("$in")) {
const values = condition.value.slice(4, -1).split(",");
return {
or: [],
and: [
{
field: condition.field,
operator: "in",
value: values
}
]
};
}
return {
and: [condition],
or: []
};
}
function transformWhere(where) {
const { and, or, ...rest } = where;
const result = {
and: [],
or: []
};
if ((rest == null ? void 0 : rest.field) && (rest == null ? void 0 : rest.operator)) {
return {
field: rest.field,
operator: rest.operator,
value: rest.value
};
}
for (const [key, value] of Object.entries(rest)) {
const { and: and2, or: or2 } = parseCondition({
field: (value == null ? void 0 : value.field) || key,
operator: (value == null ? void 0 : value.operator) || "eq",
value: (value == null ? void 0 : value.value) || value
});
if (and2) {
result.and.push(...and2);
}
if (or2) {
result.or.push(...or2);
}
}
if (and == null ? void 0 : and.length) {
and.forEach((w) => {
result.and.push(transformWhere(w));
});
}
if (or == null ? void 0 : or.length) {
or.forEach((w) => {
result.or.push(transformWhere(w));
});
}
if (!result.or.length) {
delete result.or;
}
if (!result.and.length) {
delete result.and;
}
return result;
}
v2.pipe(v2.any(), v2.transform(transformWhere));
var operatorMap = {
eq: "$eq",
ne: "$ne",
gt: "$gt",
gte: "$gte",
lt: "$lt",
lte: "$lte",
in: "$in",
exists: "$exists"
};
function parseCondition2(condition) {
const { field, operator, value } = condition;
if (!field || !operator) {
console.error("Invalid condition:", condition);
return {};
}
const siftOperator = operatorMap[operator];
if (!siftOperator) {
throw new Error(`Unsupported operator: ${operator}`);
}
return {
[field]: {
[siftOperator]: value
}
};
}
function parseGroup(group) {
const { and, or, ...rest } = group;
const parsed = {};
if ((rest == null ? void 0 : rest.field) && (rest == null ? void 0 : rest.operator)) {
return parseCondition2(rest);
}
if (and == null ? void 0 : and.length) {
parsed.$and = and.map(parseGroup);
}
if (or == null ? void 0 : or.length) {
parsed.$or = or.map(parseGroup);
}
return parsed;
}
function parseWhere(payload) {
var _a, _b;
if (!payload) return {};
const transformed = transformWhere(payload);
const parsed = parseGroup(transformed);
if (!((_a = parsed.$and) == null ? void 0 : _a.length)) {
delete parsed.$and;
}
if (!((_b = parsed.$or) == null ? void 0 : _b.length)) {
delete parsed.$or;
}
return parsed;
}
function query(data, options) {
var _a;
const { where, include, exclude } = options;
const limit = options.limit;
const offset = options.offset || 0;
const siftQuery = parseWhere(where);
let items = data.filter(sift(siftQuery));
if (include == null ? void 0 : include.length) {
items = items.map((item) => pick(item, options.include));
}
if ((exclude == null ? void 0 : exclude.length) && !(include == null ? void 0 : include.length)) {
items = items.map((item) => omit(item, exclude));
}
if ((_a = options.sortBy) == null ? void 0 : _a.length) {
const sort = options.sortBy.map((f, i) => ({
field: f,
order: options.sortDesc && options.sortDesc[i] ? "desc" : "asc"
}));
items = orderBy(
items,
sort.map((s) => s.field),
sort.map((s) => s.order)
);
}
items = items.slice(offset, limit ? offset + limit : void 0);
return items;
}
function count(data, options) {
const items = query(data, { where: options.where });
return items.length;
}
function findMetadata(options) {
const { filesystem: filesystem2, root, id } = options;
const resolve3 = (...args) => filesystem2.path.resolve(root, ...args);
const filepath = resolve3(id, ".db", "metadata.json");
const json = filesystem2.readSync.json(filepath, {
default: {
salt: crypto.randomBytes(16).toString("hex"),
iv: crypto.randomBytes(16).toString("hex"),
files: []
}
});
const all = filesystem2.readdirSync(resolve3(id));
const files = [];
all.filter((file) => file !== ".db").forEach((file) => {
var _a;
const meta = (_a = json.files) == null ? void 0 : _a.find((f) => f.name === file);
files.push({
name: file,
encrypted: false,
...meta
});
});
json.files = files;
const schema4 = v2.object({
salt: v2.string(),
iv: v2.string(),
files: v2.array(v2.object({ name: v2.string(), encrypted: v2.boolean() }))
});
return validate(schema4, json);
}
// src/providers/vault/findPassword.ts
function findPassword(options) {
const { filesystem: filesystem2, root } = options;
const resolve3 = (...args) => filesystem2.path.resolve(root, ...args);
if (!filesystem2.existsSync(resolve3(".db", "password"))) {
throw new Error("Not authenticated, please run db auth first");
}
const password2 = filesystem2.readSync.text(resolve3(".db", "password"));
const metadata = filesystem2.readSync.json(resolve3(".db", "password.json"), {
schema: (v3) => v3.object({
salt: v3.string(),
iv: v3.string(),
test: v3.string()
})
});
if (!password2 || !metadata) {
throw new Error("Password not found");
}
const encryption = createEncryption({
password: password2,
salt: metadata.salt,
iv: metadata.iv
});
const decrypted = encryption.decrypt(metadata.test);
if (!decrypted.endsWith("success")) {
throw new Error("Password incorrect");
}
return password2;
}
// src/providers/vault/list.ts
async function list(payload) {
var _a;
const { filesystem: filesystem2, root, options, parser } = payload;
const resolve3 = (...args) => filesystem2.path.resolve(root, ...args);
const password2 = findPassword({
filesystem: filesystem2,
root
});
const encryption = createEncryption({
password: password2
});
const where = (options == null ? void 0 : options.where) || {};
const exclude = (options == null ? void 0 : options.exclude) || [];
const include = (options == null ? void 0 : options.include) || [];
const limit = options == null ? void 0 : options.limit;
const page = (options == null ? void 0 : options.page) || 1;
const files = filesystem2.readdirSync(root);
const excludePatterns = [".db"];
const result = [];
for (const folder of files) {
if (excludePatterns.includes(folder)) {
continue;
}
const metadata = findMetadata({
filesystem: filesystem2,
root,
id: folder
});
encryption.setSalt(metadata.salt).setIv(metadata.iv);
const filename = filesystem2.existsSync(resolve3(folder, `index.${parser.ext}`)) ? resolve3(folder, `index.${parser.ext}`) : resolve3(folder, encryption.encrypt(`index.${parser.ext}`));
const basename = filesystem2.path.basename(filename);
if (!filesystem2.existsSync(filename)) {
const error = new Error(`Index file not found at ${filename}`);
Object.assign(error, {
folder: resolve3(folder),
filename: `index.${parser.ext}`,
encrypted_filename: encryption.encrypt(`index.${parser.ext}`)
});
throw error;
}
const fileMeta = (_a = metadata == null ? void 0 : metadata.files) == null ? void 0 : _a.find((f) => f.name === basename);
let raw = filesystem2.readSync(filename);
if (fileMeta == null ? void 0 : fileMeta.encrypted) {
raw = encryption.decrypt(raw);
}
const rawText = new TextDecoder().decode(raw);
const item = {
id: folder.replace(`.${parser.ext}`, ""),
folder: resolve3(folder),
raw: rawText,
lock: (fileMeta == null ? void 0 : fileMeta.encrypted) || false
};
Object.assign(item, parser.parse(rawText));
result.push(item);
}
const items = query(result, {
where,
exclude,
include,
limit,
offset: page > 1 ? (page - 1) * limit : 0
});
const meta = {
total: count(result, { where }),
limit,
total_pages: limit ? Math.ceil(result.length / limit) : 1
};
return {
meta,
data: items
};
}
async function update(payload) {
const { filesystem: filesystem2, root, options, parser } = payload;
const resolve3 = (...args) => filesystem2.path.resolve(root, ...args);
const password2 = findPassword({
filesystem: filesystem2,
root
});
const encryption = createEncryption({
password: password2
});
const data = options.data;
const { data: items } = await list({
filesystem: filesystem2,
root,
parser,
options: {
where: options.where,
limit: options.limit
}
});
const hideKeys = ["id", "folder", "raw", "lock"];
for (const item of items) {
const id = item.id;
const metadata = findMetadata({
id,
filesystem: filesystem2,
root
});
encryption.setSalt(metadata.salt).setIv(metadata.iv);
const baseName = `index.${parser.ext}`;
const baseNameEncrypted = encryption.encrypt(baseName);
const fileMeta = metadata.files.find(
(f) => f.name === baseNameEncrypted || f.name === baseName
);
const filename = resolve3(id, fileMeta.encrypted ? baseNameEncrypted : baseName);
const properties = omit({ ...item, ...data }, hideKeys);
let content = new TextEncoder().encode(parser.stringify(properties));
if (fileMeta.encrypted) {
content = encryption.encrypt(content);
}
filesystem2.writeSync(filename, content);
}
return { count: items.length };
}
export { update };