UNPKG

@sidekick-coder/db

Version:

Cli Tool to manipulate data from diferent sources

1,391 lines (1,368 loc) 40.3 kB
import * as valibot from 'valibot'; import path, { dirname, resolve, join } from 'path'; import fs2 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 cp from 'child_process'; import os from 'os'; import crypto from 'crypto'; import sift from 'sift'; import { omit, pick, orderBy } from 'lodash-es'; import ms from 'ms'; import { format } from 'date-fns'; // src/core/provider/defineProvider.ts function defineProvider(payload) { return payload; } // 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(() => fs2.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 [json2, error] = tryCatch.sync(() => JSON.parse(content, options == null ? void 0 : options.reviver)); return error ? (options == null ? void 0 : options.default) || null : json2; }; 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 schema5 = typeof cb === "function" ? cb(v2) : cb; const { output, issues, success } = v2.safeParse(schema5, 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 schema5; if (typeof cb === "function") { schema5 = cb(v2); } else { schema5 = cb; } const { output, issues, success } = await v2.safeParseAsync(schema5, 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; }; function createFsNode() { const exists = async (path4) => { const [, error] = await tryCatch(() => fs2.promises.access(path4)); return error ? false : true; }; const existsSync = (path4) => { const [, error] = tryCatch.sync(() => fs2.accessSync(path4)); return error ? false : true; }; const read = async (path4) => { const [content, error] = await tryCatch(() => fs2.promises.readFile(path4)); if (error) { return null; } return new Uint8Array(content); }; const readSync = (path4) => { const [content, error] = tryCatch.sync(() => fs2.readFileSync(path4)); if (error) { return null; } return new Uint8Array(content); }; const readdir = async (path4) => { const [files, error] = await tryCatch(() => fs2.promises.readdir(path4)); return error ? [] : files; }; const readdirSync = (path4) => { const [files, error] = tryCatch.sync(() => fs2.readdirSync(path4)); return error ? [] : files; }; const write = async (path4, content) => { const [, error] = await tryCatch(() => fs2.promises.writeFile(path4, content)); if (error) { throw error; } }; const writeSync = (path4, content) => { const [, error] = tryCatch.sync(() => fs2.writeFileSync(path4, content)); if (error) { throw error; } }; const mkdir2 = async (path4) => { const [, error] = await tryCatch(() => fs2.promises.mkdir(path4)); if (error) { throw error; } }; const mkdirSync = (path4) => { const [, error] = tryCatch.sync(() => fs2.mkdirSync(path4)); if (error) { throw error; } }; const remove = async (path4) => { const [, error] = await tryCatch(() => fs2.promises.rm(path4, { recursive: true })); if (error) { throw error; } }; const removeSync = (path4) => { const [, error] = tryCatch.sync(() => fs2.rmSync(path4, { recursive: true })); if (error) { throw error; } }; const removeAt = async (path4, milliseconds) => { if (os.platform() === "win32") { const script = ` Set objShell = CreateObject("WScript.Shell") objShell.Run "cmd /c timeout /t ${milliseconds / 1e3} && del /f /q ${path4}", 0, True `; const key = Math.random().toString(36).substring(7); const tempScriptPath = join(os.tmpdir(), `db-delete-file-${key}.vbs`); fs2.writeFileSync(tempScriptPath, script); const child = cp.spawn("cscript.exe", [tempScriptPath], { detached: true, stdio: "ignore" }); child.unref(); return true; } return false; }; return { exists, existsSync, read, readSync, readdir, readdirSync, write, writeSync, mkdir: mkdir2, mkdirSync, remove, removeSync, removeAt }; } // src/core/filesystem/createFilesystem.ts function createFilesystem(options = {}) { const fs3 = options.fs || createFsNode(); const path4 = options.path || createPathNode(); const locks2 = /* @__PURE__ */ new Set(); function awaitLock2(filepath, timeout = 1e3) { return new Promise((resolve3, reject) => { const interval = setInterval(() => { if (!locks2.has(filepath)) { clearInterval(interval); resolve3(); } }, 100); setTimeout(() => { clearInterval(interval); reject(new Error("Timeout")); }, timeout); }); } async function read(filepath) { return fs3.read(filepath); } read.text = async function(filepath, options2) { const content = await read(filepath); if (!content) { return (options2 == null ? void 0 : options2.default) || ""; } return new TextDecoder().decode(content); }; read.json = async function(filepath, options2) { const content = await read.text(filepath); if (!content) { return (options2 == null ? void 0 : options2.default) || null; } const [json2, error] = await tryCatch(() => JSON.parse(content, options2 == null ? void 0 : options2.reviver)); if (options2 == null ? void 0 : options2.schema) { return validate(options2.schema, json2); } return error ? (options2 == null ? void 0 : options2.default) || null : json2; }; read.yaml = async function(filepath, options2) { const content = await read.text(filepath); if (!content) { return (options2 == null ? void 0 : options2.default) || null; } const [yml, error] = await tryCatch(() => YAML.parse(content, options2 == null ? void 0 : options2.reviver)); return error ? (options2 == null ? void 0 : options2.default) || null : yml; }; function readSync(filepath) { return fs3.readSync(filepath); } readSync.text = function(filepath, defaultValue = "") { const content = readSync(filepath); if (!content) { return defaultValue; } return new TextDecoder().decode(content); }; readSync.json = function(filepath, options2) { const content = readSync.text(filepath); if (!content) { return (options2 == null ? void 0 : options2.default) || null; } const [json2, error] = tryCatch.sync(() => JSON.parse(content, options2 == null ? void 0 : options2.reviver)); if (options2 == null ? void 0 : options2.schema) { return validate(options2.schema, json2); } return error ? (options2 == null ? void 0 : options2.default) || null : json2; }; readSync.yaml = function(filepath, options2) { const content = readSync.text(filepath); if (!content) { return (options2 == null ? void 0 : options2.default) || null; } const [yml, error] = tryCatch.sync(() => YAML.parse(content, options2 == null ? void 0 : options2.parseOptions)); return error ? (options2 == null ? void 0 : options2.default) || null : yml; }; async function readdir(filepath) { return fs3.readdir(filepath); } function readdirSync(filepath) { return fs3.readdirSync(filepath); } async function mkdir2(filepath, options2) { if (await fs3.exists(filepath)) return; if (options2 == null ? void 0 : options2.recursive) { const parent = path4.dirname(filepath); await mkdir2(parent, options2); } await fs3.mkdir(filepath); } function mkdirSync(filepath, options2) { if (fs3.existsSync(filepath)) return; if (options2 == null ? void 0 : options2.recursive) { const parent = path4.dirname(filepath); mkdirSync(parent, options2); } fs3.mkdirSync(filepath); } async function write(filename, content, options2) { if (locks2.has(filename)) { await awaitLock2(filename); } locks2.add(filename); if (options2 == null ? void 0 : options2.recursive) { const parent = path4.dirname(filename); await mkdir2(parent, { recursive: true }); } const [, error] = await tryCatch(() => fs3.write(filename, content)); locks2.delete(filename); if (error) { throw error; } } write.text = async function(filename, content, options2) { await write(filename, new TextEncoder().encode(content), options2); }; write.json = async function(filename, content, options2) { await write.text(filename, JSON.stringify(content, null, 2), options2); }; function writeSync(filename, content, options2) { if (options2 == null ? void 0 : options2.recursive) { const parent = path4.dirname(filename); mkdirSync(parent, { recursive: true }); } fs3.writeSync(filename, content); } writeSync.text = function(filename, content, options2) { writeSync(filename, new TextEncoder().encode(content), options2); }; writeSync.json = function(filename, content, options2) { writeSync.text(filename, JSON.stringify(content, null, 2), options2); }; function remove(filepath) { return fs3.remove(filepath); } function removeSync(filepath) { return fs3.removeSync(filepath); } function removeAt(filepath, miliseconds) { return fs3.removeAt(filepath, miliseconds); } return { path: path4, fs: fs3, exists: fs3.exists, existsSync: fs3.existsSync, read, readSync, readdir, readdirSync, write, writeSync, mkdir: mkdir2, mkdirSync, remove, removeSync, removeAt }; } // src/core/parsers/markdown.ts function parse2(contents) { let body = contents; const result = {}; if (contents.startsWith("---")) { const [, frontmatter, rest] = contents.split("---"); Object.assign(result, YAML.parse(frontmatter)); body = rest.trim(); } result["body"] = body || ""; return result; } function stringify2(data) { const { body, ...properties } = data; let result = ""; if (properties && Object.keys(properties).length) { result += `--- ${YAML.stringify(properties)}--- `; } if (body) { result += body; } return result; } var MD = { parse: parse2, stringify: stringify2 }; // src/core/parsers/all.ts var parsers = []; var json = { name: "json", ext: "json", parse: JSON.parse, stringify: (contents) => JSON.stringify(contents, null, 4) }; var markdown = { name: "markdown", ext: "md", parse: MD.parse, stringify: (contents) => MD.stringify(contents) }; parsers.push(json, markdown); parsers.push({ name: "yaml", ext: "yaml", parse: YAML.parse, stringify: (contents) => YAML.stringify(contents, null, 4) }); parsers.push({ name: "yml", ext: "yml", parse: YAML.parse, stringify: (contents) => YAML.stringify(contents, null, 4) }); // src/providers/vault/config.ts var schema2 = (dirname2, path4 = createPathNode()) => v2.object({ format: v2.optional(v2.string(), "markdown"), path: v2.extras.path(dirname2, path4), id_strategy: v2.optional( v2.object({ name: v2.string(), options: v2.optional(v2.any()) }), { name: "incremental" } ) }); var schema3 = 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(schema3, 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(schema3, 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; } var schema4 = 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 json2 = 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 = json2.files) == null ? void 0 : _a.find((f) => f.name === file); files.push({ name: file, encrypted: false, ...meta }); }); json2.files = files; const schema5 = v2.object({ salt: v2.string(), iv: v2.string(), files: v2.array(v2.object({ name: v2.string(), encrypted: v2.boolean() })) }); return validate(schema5, json2); } // 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 }; } // src/providers/vault/find.ts async function find(payload) { const { filesystem: filesystem2, options, root, parser } = payload; const { data: items } = await list({ filesystem: filesystem2, root, parser, options: { ...options, limit: 1 } }); return items[0] || null; } // src/providers/vault/lockItem.ts async function lockItem(options) { const { filesystem: filesystem2, root } = options; const resolve3 = (...args) => filesystem2.path.resolve(root, ...args); const id = validate((v3) => v3.string(), options.options.id); const filepath = resolve3(id); if (!filesystem2.existsSync(resolve3(id))) { throw new Error(`Item ${id} not found in ${filepath}`); } const password2 = findPassword({ filesystem: filesystem2, root }); const metadata = findMetadata({ id, filesystem: filesystem2, root }); const encryption = createEncryption({ password: password2, salt: metadata.salt, iv: metadata.iv }); for (const file of metadata.files) { if (file.encrypted) { continue; } const source_filename = file.name; const target_filename = encryption.encrypt(source_filename); const contents = filesystem2.readSync(resolve3(id, source_filename)); const encrypted = encryption.encrypt(contents); filesystem2.writeSync(resolve3(id, target_filename), encrypted); filesystem2.removeSync(resolve3(id, source_filename)); file.encrypted = true; file.name = target_filename; } filesystem2.writeSync.json(resolve3(id, ".db", "metadata.json"), metadata, { recursive: true }); return metadata.files; } // src/providers/vault/create.ts async function create(payload) { const { filesystem: filesystem2, root, options, makeId, parser } = payload; const resolve3 = (...args) => filesystem2.path.resolve(root, ...args); const data = options.data; const id = data.id || await makeId(); if (filesystem2.existsSync(resolve3(id))) { throw new Error(`Item with id "${id}" already exists`); } const folder = resolve3(id); const filename = resolve3(id, `index.${parser.ext}`); const raw = parser.stringify(data); filesystem2.mkdirSync(resolve3(id)); filesystem2.writeSync.text(filename, raw); await lockItem({ filesystem: filesystem2, root, options: { id } }); const item = { id, raw, folder, ...data }; return item; } 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 }; } // src/providers/vault/destroy.ts async function destroy(payload) { const { filesystem: filesystem2, root, parser, options } = payload; const { data: items } = await list({ filesystem: filesystem2, root, parser, options: { where: options.where, limit: options.limit } }); for (const item of items) { const filename = filesystem2.path.resolve(root, item.id); filesystem2.removeSync(filename); } return { count: items.length }; } // src/providers/vault/lock.ts async function lock(payload) { const { filesystem: filesystem2, root, parser } = payload; const options = validate( (v3) => v3.object({ where: v3.optional(schema4), limit: v3.optional(v3.number()), verbose: v3.optional(v3.boolean()) }), payload.options ); const { data: items } = await list({ root, filesystem: filesystem2, parser, options: { where: options.where, limit: options.limit } }); let success = 0; let failed = 0; for (const item of items) { const [result, error] = await tryCatch( () => lockItem({ filesystem: filesystem2, root, options: { id: item.id } }) ); if (result) { success++; } if (error) { failed++; } if (options.verbose) { console.log(`Item ${item.id} locked`); } } return { success, failed }; } // src/providers/vault/unlockItem.ts async function unlockItem(options) { const { filesystem: filesystem2, root } = options; const resolve3 = (...args) => filesystem2.path.resolve(root, ...args); const id = validate((v3) => v3.string(), options.options.id); if (!filesystem2.existsSync(resolve3(id))) { throw new Error(`Item ${id} not found in ${resolve3(id)}`); } const password2 = findPassword({ filesystem: filesystem2, root }); const metadata = findMetadata({ id, filesystem: filesystem2, root }); const encryption = createEncryption({ password: password2, salt: metadata.salt, iv: metadata.iv }); encryption.setSalt(metadata.salt).setIv(metadata.iv); for (const file of metadata.files) { if (!file.encrypted) { continue; } const source_filename = file.name; const target_filename = encryption.decrypt(source_filename); const encrypted = filesystem2.readSync(resolve3(id, source_filename)); const contents = encryption.decrypt(encrypted); filesystem2.writeSync(resolve3(id, target_filename), contents); filesystem2.removeSync(resolve3(id, source_filename)); file.encrypted = false; file.name = target_filename; } filesystem2.writeSync.json(resolve3(id, ".db", "metadata.json"), metadata, { recursive: true }); return metadata.files; } // src/providers/vault/unlock.ts async function unlock(payload) { const { filesystem: filesystem2, root, parser } = payload; const options = validate( (v3) => v3.object({ where: v3.optional(schema4), limit: v3.optional(v3.number()), verbose: v3.optional(v3.boolean()) }), payload.options ); const { data: items } = await list({ root, filesystem: filesystem2, parser, options: { where: options.where, limit: options.limit } }); let success = 0; let failed = 0; for (const item of items) { const [result, error] = await tryCatch( () => unlockItem({ filesystem: filesystem2, root, options: { id: item.id } }) ); if (result) { success++; } if (error) { failed++; } if (options.verbose) { console.log(`Item ${item.id} unlocked`); } } return { success, failed }; } async function init(payload) { const { filesystem: filesystem2, root } = payload; const { resolve: resolve3 } = filesystem2.path; const filename = resolve3(root, ".db", "password.json"); const isInitialized = filesystem2.existsSync(filename); if (isInitialized && !payload.options.force) { return { message: "Vault already initialized, if you want to overwrite use force option", filename }; } const options = await validate.async( (v3) => v3.objectAsync({ password: v3.prompts.password(), salt: v3.optional(v3.string(), crypto.randomBytes(16).toString("hex")), iv: v3.optional(v3.string(), crypto.randomBytes(16).toString("hex")), force: v3.optional(v3.boolean(), false) }), payload.options ); const { salt, iv, password: password2 } = options; const encryption = createEncryption({ password: password2, salt, iv }); const test = encryption.encrypt(crypto.randomBytes(16).toString("hex") + "success"); const json2 = { salt, iv, test }; filesystem2.writeSync.json(filename, json2, { recursive: true }); return { message: "Vault database initialized", filename, ...json2 }; } async function auth(payload) { const { filesystem: filesystem2, root } = payload; const resolve3 = filesystem2.path.resolve; const metaFilename = resolve3(root, ".db", "password.json"); if (!await filesystem2.exists(metaFilename)) { throw new Error( `Password metadata file not found in ${metaFilename} Please run "db init" command first` ); } const schema5 = v2.objectAsync({ password: v2.prompts.password(), timeout: v2.optional(v2.string(), "1hs") }); const options = await validate.async(schema5, payload.options); const metadata = await filesystem2.read.json(metaFilename, { schema: (v3) => v3.object({ salt: v3.string(), iv: v3.string(), test: v3.string() }) }); const encryption = createEncryption({ password: options.password, salt: metadata.salt, iv: metadata.iv }); if (!encryption.decrypt(metadata.test).includes("success")) { throw new Error(`Invalid password`); } const filename = resolve3(root, ".db", "password"); filesystem2.writeSync.text(filename, options.password); const milisseconds = ms(options.timeout); const scheduleDeletion = await filesystem2.removeAt(filename, milisseconds); if (!scheduleDeletion) { console.log( `Can't schedule deletion of password file, please remove it manually when you finish` ); } return { message: `Authenticated successfully`, filename, timeout: options.timeout }; } function createDate() { return { name: "date", async create(config) { const pattern = config.pattern || "yyyy-MM-dd"; return format(/* @__PURE__ */ new Date(), pattern); } }; } // src/core/idStrategy/createIncremental.ts function pad(value, size) { return value.toString().padStart(size, "0"); } function createIncremental(filesystem2, filename) { async function create2(payload = {}) { const options = validate( (v3) => v3.object({ padding: v3.optional(v3.number(), 2) }), payload ); if (!filesystem2.existsSync(filename)) { filesystem2.writeSync.json(filename, { last_id: 0 }, { recursive: true }); } const content = filesystem2.readSync.json(filename, { schema: (v3) => v3.object({ last_id: v3.number() }) }); const id = content.last_id + 1; filesystem2.writeSync.json(filename, { last_id: id }); return pad(id, options.padding); } return { name: "incremental", create: create2 }; } function createUuid() { return { name: "uuid", async create() { return crypto.randomUUID(); } }; } // src/core/idStrategy/createStrategies.ts function createStrategies(options) { const { filesystem: filesystem2, root } = options; return [ createIncremental(filesystem2, filesystem2.path.resolve(root, ".db", "incremental.json")), createDate(), createUuid() ]; } // src/providers/vault/provider.ts var provider = defineProvider((payload, { root, fs: fs3, path: path4 }) => { const config = validate(schema2(root, path4), payload); const filesystem2 = createFilesystem({ fs: fs3, path: path4 }); const { format: format2, id_strategy } = config; const strategies = createStrategies({ filesystem: filesystem2, root: config.path }); const parser = parsers.find((p) => p.name === format2); const strategy = strategies.find((s) => s.name === id_strategy.name); if (!parser) { throw new Error(`Parser for format "${format2}" not found`); } if (!strategy) { throw new Error(`Strategy for id "${id_strategy}" not found`); } const makeId = () => strategy.create(id_strategy.options); const common = { filesystem: filesystem2, root: config.path }; return { list: (options = {}) => list({ ...common, parser, options }), find: (options) => find({ ...common, parser, options }), create: (options) => create({ ...common, parser, options, makeId }), update: (options) => update({ ...common, parser, options }), destroy: (options) => destroy({ ...common, parser, options }), init: (options) => init({ ...common, options }), auth: (options) => auth({ ...common, options }), lock: (options) => lock({ ...common, parser, options }), lockItem: (options) => lockItem({ ...common, filesystem: filesystem2, options }), unlock: (options) => unlock({ ...common, parser, options }), unlockItem: (options) => unlockItem({ ...common, options }) }; }); export { provider };