You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
4.8 KiB
193 lines
4.8 KiB
'use strict'; |
|
|
|
const { parse: createJsonParseStream } = require('./jsonstream'); |
|
const Promise = require('bluebird'); |
|
const fs = require('graceful-fs'); |
|
const Model = require('./model'); |
|
const Schema = require('./schema'); |
|
const SchemaType = require('./schematype'); |
|
const WarehouseError = require('./error'); |
|
const pkg = require('../package.json'); |
|
const { open } = fs.promises; |
|
const pipeline = Promise.promisify(require('stream').pipeline); |
|
const log = require('hexo-log')(); |
|
|
|
let _writev; |
|
|
|
if (typeof fs.writev === 'function') { |
|
_writev = (handle, buffers) => handle.writev(buffers); |
|
} else { |
|
_writev = async (handle, buffers) => { |
|
for (const buffer of buffers) await handle.write(buffer); |
|
}; |
|
} |
|
|
|
async function exportAsync(database, path) { |
|
const handle = await open(path, 'w'); |
|
|
|
try { |
|
// Start body & Meta & Start models |
|
await handle.write(`{"meta":${JSON.stringify({ |
|
version: database.options.version, |
|
warehouse: pkg.version |
|
})},"models":{`); |
|
|
|
const models = database._models; |
|
const keys = Object.keys(models); |
|
const { length } = keys; |
|
|
|
// models body |
|
for (let i = 0; i < length; i++) { |
|
const key = keys[i]; |
|
|
|
if (!models[key]) continue; |
|
|
|
const buffers = []; |
|
|
|
if (i) buffers.push(Buffer.from(',', 'ascii')); |
|
|
|
buffers.push(Buffer.from(`"${key}":`)); |
|
|
|
buffers.push(Buffer.from(models[key]._export())); |
|
await _writev(handle, buffers); |
|
} |
|
|
|
// End models |
|
await handle.write('}}'); |
|
} catch (e) { |
|
log.error(e); |
|
if (e instanceof RangeError && e.message.includes('Invalid string length')) { |
|
// NOTE: Currently, we can't deal with anything about this issue. |
|
// If do not `catch` the exception after the process will not work (e.g: `after_generate` filter.) |
|
// A side-effect of this workaround is the `db.json` will not generate. |
|
log.warn('see: https://github.com/nodejs/node/issues/35973'); |
|
} else { |
|
throw e; |
|
} |
|
} finally { |
|
await handle.close(); |
|
} |
|
} |
|
|
|
class Database { |
|
|
|
/** |
|
* Database constructor. |
|
* |
|
* @param {object} [options] |
|
* @param {number} [options.version=0] Database version |
|
* @param {string} [options.path] Database path |
|
* @param {function} [options.onUpgrade] Triggered when the database is upgraded |
|
* @param {function} [options.onDowngrade] Triggered when the database is downgraded |
|
*/ |
|
constructor(options) { |
|
this.options = Object.assign({ |
|
version: 0, |
|
onUpgrade() {}, |
|
|
|
onDowngrade() {} |
|
}, options); |
|
|
|
this._models = {}; |
|
|
|
class _Model extends Model {} |
|
|
|
this.Model = _Model; |
|
|
|
_Model.prototype._database = this; |
|
} |
|
|
|
/** |
|
* Creates a new model. |
|
* |
|
* @param {string} name |
|
* @param {Schema|object} [schema] |
|
* @return {Model} |
|
*/ |
|
model(name, schema) { |
|
if (this._models[name]) { |
|
return this._models[name]; |
|
} |
|
|
|
this._models[name] = new this.Model(name, schema); |
|
const model = this._models[name]; |
|
return model; |
|
} |
|
|
|
/** |
|
* Loads database. |
|
* |
|
* @param {function} [callback] |
|
* @return {Promise} |
|
*/ |
|
load(callback) { |
|
const { path, onUpgrade, onDowngrade, version: newVersion } = this.options; |
|
|
|
if (!path) throw new WarehouseError('options.path is required'); |
|
|
|
let oldVersion = 0; |
|
|
|
const getMetaCallBack = data => { |
|
if (data.meta && data.meta.version) { |
|
oldVersion = data.meta.version; |
|
} |
|
}; |
|
|
|
// data event arg0 wrap key/value pair. |
|
const parseStream = createJsonParseStream('models.$*'); |
|
|
|
parseStream.once('header', getMetaCallBack); |
|
parseStream.once('footer', getMetaCallBack); |
|
|
|
parseStream.on('data', data => { |
|
this.model(data.key)._import(data.value); |
|
}); |
|
|
|
const rs = fs.createReadStream(path, 'utf8'); |
|
|
|
return pipeline(rs, parseStream).then(() => { |
|
if (newVersion > oldVersion) { |
|
return onUpgrade(oldVersion, newVersion); |
|
} else if (newVersion < oldVersion) { |
|
return onDowngrade(oldVersion, newVersion); |
|
} |
|
}).asCallback(callback); |
|
} |
|
|
|
/** |
|
* Saves database. |
|
* |
|
* @param {function} [callback] |
|
* @return {Promise} |
|
*/ |
|
save(callback) { |
|
const { path } = this.options; |
|
|
|
if (!path) throw new WarehouseError('options.path is required'); |
|
return Promise.resolve(exportAsync(this, path)).asCallback(callback); |
|
} |
|
|
|
toJSON() { |
|
const models = Object.keys(this._models) |
|
.reduce((obj, key) => { |
|
const value = this._models[key]; |
|
if (value != null) obj[key] = value; |
|
return obj; |
|
}, {}); |
|
|
|
return { |
|
meta: { |
|
version: this.options.version, |
|
warehouse: pkg.version |
|
}, models |
|
}; |
|
} |
|
} |
|
|
|
Database.prototype.Schema = Schema; |
|
Database.Schema = Database.prototype.Schema; |
|
Database.prototype.SchemaType = SchemaType; |
|
Database.SchemaType = Database.prototype.SchemaType; |
|
Database.version = pkg.version; |
|
|
|
module.exports = Database;
|
|
|