@knapsack/app
Version:
Build Design Systems on top of knapsack, by Basalt
306 lines (269 loc) • 7.95 kB
text/typescript
/**
* 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;
}
}