context_loader.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. 'use strict';
  2. const assert = require('assert');
  3. const is = require('is-type-of');
  4. const FileLoader = require('./file_loader');
  5. const CLASSLOADER = Symbol('classLoader');
  6. const EXPORTS = FileLoader.EXPORTS;
  7. class ClassLoader {
  8. constructor(options) {
  9. assert(options.ctx, 'options.ctx is required');
  10. const properties = options.properties;
  11. this._cache = new Map();
  12. this._ctx = options.ctx;
  13. for (const property in properties) {
  14. this.defineProperty(property, properties[property]);
  15. }
  16. }
  17. defineProperty(property, values) {
  18. Object.defineProperty(this, property, {
  19. get() {
  20. let instance = this._cache.get(property);
  21. if (!instance) {
  22. instance = getInstance(values, this._ctx);
  23. this._cache.set(property, instance);
  24. }
  25. return instance;
  26. },
  27. });
  28. }
  29. }
  30. /**
  31. * Same as {@link FileLoader}, but it will attach file to `inject[fieldClass]`. The exports will be lazy loaded, such as `ctx.group.repository`.
  32. * @extends FileLoader
  33. * @since 1.0.0
  34. */
  35. class ContextLoader extends FileLoader {
  36. /**
  37. * @class
  38. * @param {Object} options - options same as {@link FileLoader}
  39. * @param {String} options.fieldClass - determine the field name of inject object.
  40. */
  41. constructor(options) {
  42. assert(options.property, 'options.property is required');
  43. assert(options.inject, 'options.inject is required');
  44. const target = options.target = {};
  45. if (options.fieldClass) {
  46. options.inject[options.fieldClass] = target;
  47. }
  48. super(options);
  49. const app = this.options.inject;
  50. const property = options.property;
  51. // define ctx.service
  52. Object.defineProperty(app.context, property, {
  53. get() {
  54. // distinguish property cache,
  55. // cache's lifecycle is the same with this context instance
  56. // e.x. ctx.service1 and ctx.service2 have different cache
  57. if (!this[CLASSLOADER]) {
  58. this[CLASSLOADER] = new Map();
  59. }
  60. const classLoader = this[CLASSLOADER];
  61. let instance = classLoader.get(property);
  62. if (!instance) {
  63. instance = getInstance(target, this);
  64. classLoader.set(property, instance);
  65. }
  66. return instance;
  67. },
  68. });
  69. }
  70. }
  71. module.exports = ContextLoader;
  72. function getInstance(values, ctx) {
  73. // it's a directory when it has no exports
  74. // then use ClassLoader
  75. const Class = values[EXPORTS] ? values : null;
  76. let instance;
  77. if (Class) {
  78. if (is.class(Class)) {
  79. instance = new Class(ctx);
  80. } else {
  81. // it's just an object
  82. instance = Class;
  83. }
  84. // Can't set property to primitive, so check again
  85. // e.x. module.exports = 1;
  86. } else if (is.primitive(values)) {
  87. instance = values;
  88. } else {
  89. instance = new ClassLoader({ ctx, properties: values });
  90. }
  91. return instance;
  92. }