UNPKG

@knapsack/app

Version:

Build Design Systems on top of knapsack, by Basalt

306 lines (269 loc) • 7.95 kB
/** * Copyright (C) 2018 Basalt This file is part of Knapsack. Knapsack is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Knapsack is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Knapsack; if not, see <https://www.gnu.org/licenses>. */ /* eslint-disable max-classes-per-file */ import low from 'lowdb'; import FileSync from 'lowdb/adapters/FileSync'; import { KnapsackFile } from '@knapsack/core'; import { join } from 'path'; import chokidar from 'chokidar'; import os from 'os'; import fs from 'fs-extra'; import yaml from 'js-yaml'; import { validateDataAgainstSchema } from '@knapsack/schema-utils'; import { formatCode } from '../server-utils'; import { knapsackEvents, EVENTS } from '../events'; import { KnapsackDb } from '../../schemas/misc'; /** * Creates a LoDash powered JSON file database, via `lowdb` that is created using the `_.chain` method. * Each database has all of the power of LoDash for parsing the data. * @link https://www.npmjs.com/package/lowdb * @link https://lodash.com/docs/4.17.11#chain */ export class FileDb { db: low.LowdbSync<any>; constructor({ dbDir, name, defaults = {}, }: { dbDir: string; name: string; defaults?: object; }) { // @todo kebab-case `name` // @todo should this read a pre-existing file first? const dbPath = join(dbDir, `${name}.json`); const adapter = new FileSync(dbPath, { serialize: data => JSON.stringify(data, null, 2) + os.EOL, }); const db = low(adapter); // Set some defaults (required if your JSON file is empty) db.defaults(defaults).write(); this.db = db; // You can use any lodash function like _.get and _.find with shorthand syntax. // // // Use .value() instead of .write() if you're only reading from db // db.get('posts') // .find({ id: 1 }) // .value() // Start watching the file in case user manually changes it so we can re-read it into memory const watcher = chokidar.watch(dbPath, { ignoreInitial: true, }); watcher.on('all', () => { this.db = db.read(); }); knapsackEvents.on(EVENTS.SHUTDOWN, () => { watcher.close(); }); } getDb() { return this.db; } get(key: string): any { // @todo improve types return this.db.get(key).value(); } getAll(): any { // @todo improve types return this.db.value(); } /** * @link https://lodash.com/docs/4.17.11#find */ find(data: any): any { // @todo improve types return this.db.find(data).value(); } values() { return this.db.values().value(); } set(key: string, value: any): any { return this.db.set(key, value).write(); } setAll(data: object): object { // @todo improve types return this.db.setState(data).write(); } update(key: string, func: any): any { // @todo improve types return this.db.update(key, func).write(); } } export class FileDb2<ConfigType> implements KnapsackDb<ConfigType> { /** * Full path to file used for storage */ filePath: string; private type: 'json' | 'yml'; // config: ConfigType; validationSchema: object; constructor({ filePath, // dbDir, // name, type = 'json', defaults, validationSchema, watch = true, writeFileIfAbsent = true, }: { filePath: string; // dbDir: string; type?: 'json' | 'yml'; // name: string; /** * Shallow merge */ defaults?: ConfigType; /** * JSON Schema to validate read & writes with at run time */ validationSchema?: object; watch?: boolean; writeFileIfAbsent?: boolean; }) { this.type = type; this.validationSchema = validationSchema; this.filePath = filePath; if (writeFileIfAbsent && !fs.existsSync(this.filePath)) { const { contents } = this.serialize(defaults); fs.writeFileSync(this.filePath, contents); // this.write(defaults, { sync: true }).then(() => {}); } // if (writeFileIfAbsent) { // // } // this.config = this.read(); if (watch) { // Start watching the file in case user manually changes it so we can re-read it into memory // const watcher = chokidar.watch(this.filePath, { // ignoreInitial: true, // }); // // watcher.on('all', () => { // // @todo if file is changed, trigger client ui to get new changes - we can't have diverged data // this.config = this.read(); // }); // // knapsackEvents.on(EVENTS.SHUTDOWN, () => { // watcher.close(); // }); } } /** * Ensure the data is good during run-time by using provided JSON Schemas to validate * Requires `validationSchema` to be passed in during initial creation * @throws Error if it's not valid */ validateConfig(config: ConfigType): void { if (!this.validationSchema) return; const { ok, message, errors } = validateDataAgainstSchema( this.validationSchema, config, ); if (ok) return; const msg = [ `Data validation error for ${this.filePath}`, 'The data:', JSON.stringify(config, null, ' '), '', 'The error:', message, JSON.stringify(errors, null, ' '), ].join('\n'); throw new Error(msg); } serialize(config: ConfigType): Pick<KnapsackFile, 'contents' | 'encoding'> { this.validateConfig(config); switch (this.type) { case 'json': { const contents = formatCode({ code: JSON.stringify(config) + os.EOL, language: 'json', }); return { contents, encoding: 'utf8', }; } case 'yml': { const contents = formatCode({ code: yaml.safeDump(config), language: 'yml', }); return { contents, encoding: 'utf8', }; } default: throw new Error('Un-supported type used'); } } parse(fileString: string): ConfigType { let config; switch (this.type) { case 'json': config = JSON.parse(fileString); this.validateConfig(config); return config; case 'yml': config = yaml.safeLoad(fileString); this.validateConfig(config); return config; default: throw new Error('Un-supported type used'); } } async read(): Promise<ConfigType> { const dbString: string = await fs.readFile(this.filePath, 'utf8'); return this.parse(dbString); } readSync(): ConfigType { const dbString: string = fs.readFileSync(this.filePath, 'utf8'); return this.parse(dbString); } async savePrep(config: ConfigType): Promise<KnapsackFile[]> { this.validateConfig(config); const { contents, encoding } = this.serialize(config); return [ { contents, encoding, path: this.filePath, }, ]; } // async write(config: ConfigType, { sync = false } = {}): Promise<string> { // const { contents, path, encoding } = this.savePrep(config); // if (sync) { // fs.writeFileSync(path, contents, { encoding }); // } else { // await fs.writeFile(path, contents, { encoding }); // } // return path; // } async getData(): Promise<ConfigType> { const config = await this.read(); this.validateConfig(config); return config; } getDataSync(): ConfigType { const config = this.readSync(); this.validateConfig(config); return config; } }