database.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. 'use strict';
  2. const { parse: createJsonParseStream } = require('./jsonstream');
  3. const Promise = require('bluebird');
  4. const fs = require('graceful-fs');
  5. const Model = require('./model');
  6. const Schema = require('./schema');
  7. const SchemaType = require('./schematype');
  8. const WarehouseError = require('./error');
  9. const pkg = require('../package.json');
  10. const { open } = fs.promises;
  11. const pipeline = Promise.promisify(require('stream').pipeline);
  12. const log = require('hexo-log')();
  13. let _writev;
  14. if (typeof fs.writev === 'function') {
  15. _writev = (handle, buffers) => handle.writev(buffers);
  16. } else {
  17. _writev = async (handle, buffers) => {
  18. for (const buffer of buffers) await handle.write(buffer);
  19. };
  20. }
  21. async function exportAsync(database, path) {
  22. const handle = await open(path, 'w');
  23. try {
  24. // Start body & Meta & Start models
  25. await handle.write(`{"meta":${JSON.stringify({
  26. version: database.options.version,
  27. warehouse: pkg.version
  28. })},"models":{`);
  29. const models = database._models;
  30. const keys = Object.keys(models);
  31. const { length } = keys;
  32. // models body
  33. for (let i = 0; i < length; i++) {
  34. const key = keys[i];
  35. if (!models[key]) continue;
  36. const buffers = [];
  37. if (i) buffers.push(Buffer.from(',', 'ascii'));
  38. buffers.push(Buffer.from(`"${key}":`));
  39. buffers.push(Buffer.from(models[key]._export()));
  40. await _writev(handle, buffers);
  41. }
  42. // End models
  43. await handle.write('}}');
  44. } catch (e) {
  45. log.error(e);
  46. if (e instanceof RangeError && e.message.includes('Invalid string length')) {
  47. // NOTE: Currently, we can't deal with anything about this issue.
  48. // If do not `catch` the exception after the process will not work (e.g: `after_generate` filter.)
  49. // A side-effect of this workaround is the `db.json` will not generate.
  50. log.warn('see: https://github.com/nodejs/node/issues/35973');
  51. } else {
  52. throw e;
  53. }
  54. } finally {
  55. await handle.close();
  56. }
  57. }
  58. class Database {
  59. /**
  60. * Database constructor.
  61. *
  62. * @param {object} [options]
  63. * @param {number} [options.version=0] Database version
  64. * @param {string} [options.path] Database path
  65. * @param {function} [options.onUpgrade] Triggered when the database is upgraded
  66. * @param {function} [options.onDowngrade] Triggered when the database is downgraded
  67. */
  68. constructor(options) {
  69. this.options = Object.assign({
  70. version: 0,
  71. onUpgrade() {},
  72. onDowngrade() {}
  73. }, options);
  74. this._models = {};
  75. class _Model extends Model {}
  76. this.Model = _Model;
  77. _Model.prototype._database = this;
  78. }
  79. /**
  80. * Creates a new model.
  81. *
  82. * @param {string} name
  83. * @param {Schema|object} [schema]
  84. * @return {Model}
  85. */
  86. model(name, schema) {
  87. if (this._models[name]) {
  88. return this._models[name];
  89. }
  90. this._models[name] = new this.Model(name, schema);
  91. const model = this._models[name];
  92. return model;
  93. }
  94. /**
  95. * Loads database.
  96. *
  97. * @param {function} [callback]
  98. * @return {Promise}
  99. */
  100. load(callback) {
  101. const { path, onUpgrade, onDowngrade, version: newVersion } = this.options;
  102. if (!path) throw new WarehouseError('options.path is required');
  103. let oldVersion = 0;
  104. const getMetaCallBack = data => {
  105. if (data.meta && data.meta.version) {
  106. oldVersion = data.meta.version;
  107. }
  108. };
  109. // data event arg0 wrap key/value pair.
  110. const parseStream = createJsonParseStream('models.$*');
  111. parseStream.once('header', getMetaCallBack);
  112. parseStream.once('footer', getMetaCallBack);
  113. parseStream.on('data', data => {
  114. this.model(data.key)._import(data.value);
  115. });
  116. const rs = fs.createReadStream(path, 'utf8');
  117. return pipeline(rs, parseStream).then(() => {
  118. if (newVersion > oldVersion) {
  119. return onUpgrade(oldVersion, newVersion);
  120. } else if (newVersion < oldVersion) {
  121. return onDowngrade(oldVersion, newVersion);
  122. }
  123. }).asCallback(callback);
  124. }
  125. /**
  126. * Saves database.
  127. *
  128. * @param {function} [callback]
  129. * @return {Promise}
  130. */
  131. save(callback) {
  132. const { path } = this.options;
  133. if (!path) throw new WarehouseError('options.path is required');
  134. return Promise.resolve(exportAsync(this, path)).asCallback(callback);
  135. }
  136. toJSON() {
  137. const models = Object.keys(this._models)
  138. .reduce((obj, key) => {
  139. const value = this._models[key];
  140. if (value != null) obj[key] = value;
  141. return obj;
  142. }, {});
  143. return {
  144. meta: {
  145. version: this.options.version,
  146. warehouse: pkg.version
  147. }, models
  148. };
  149. }
  150. }
  151. Database.prototype.Schema = Schema;
  152. Database.Schema = Database.prototype.Schema;
  153. Database.prototype.SchemaType = SchemaType;
  154. Database.SchemaType = Database.prototype.SchemaType;
  155. Database.version = pkg.version;
  156. module.exports = Database;