@rvoh/psychic 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +6 -4
  2. package/dist/cjs/src/bin/index.js +14 -0
  3. package/dist/cjs/src/cli/index.js +8 -0
  4. package/dist/cjs/src/error/psychic-application/init-missing-package-manager.js +20 -0
  5. package/dist/cjs/src/helpers/path/psychicPath.js +2 -0
  6. package/dist/cjs/src/psychic-application/helpers/PsychicImporter.js +9 -0
  7. package/dist/cjs/src/psychic-application/helpers/globalServiceKeyFromPath.js +11 -0
  8. package/dist/cjs/src/psychic-application/helpers/import/importServices.js +43 -0
  9. package/dist/cjs/src/psychic-application/helpers/lookupClassByGlobalName.js +3 -0
  10. package/dist/cjs/src/psychic-application/index.js +72 -2
  11. package/dist/esm/src/bin/index.js +14 -0
  12. package/dist/esm/src/cli/index.js +8 -0
  13. package/dist/esm/src/error/psychic-application/init-missing-package-manager.js +17 -0
  14. package/dist/esm/src/helpers/path/psychicPath.js +2 -0
  15. package/dist/esm/src/psychic-application/helpers/PsychicImporter.js +9 -0
  16. package/dist/esm/src/psychic-application/helpers/globalServiceKeyFromPath.js +8 -0
  17. package/dist/esm/src/psychic-application/helpers/import/importServices.js +37 -0
  18. package/dist/esm/src/psychic-application/helpers/lookupClassByGlobalName.js +3 -0
  19. package/dist/esm/src/psychic-application/index.js +71 -2
  20. package/dist/types/src/bin/index.d.ts +1 -0
  21. package/dist/types/src/error/psychic-application/init-missing-package-manager.d.ts +4 -0
  22. package/dist/types/src/helpers/path/psychicPath.d.ts +1 -1
  23. package/dist/types/src/psychic-application/helpers/PsychicImporter.d.ts +1 -0
  24. package/dist/types/src/psychic-application/helpers/globalServiceKeyFromPath.d.ts +1 -0
  25. package/dist/types/src/psychic-application/helpers/import/importServices.d.ts +4 -0
  26. package/dist/types/src/psychic-application/index.d.ts +30 -5
  27. package/package.json +6 -7
  28. package/dist/cjs/src/helpers/sspawn.js +0 -26
  29. package/dist/esm/src/helpers/sspawn.js +0 -22
  30. package/dist/types/src/helpers/sspawn.d.ts +0 -2
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
- > ATTENTION: we are currently in the process of releasing this code to the world, as of the afternoon of March 10th, 2025. This notice will be removed, and the version of this repo will be bumped to 1.0.0, once all of the repos have been migrated to the new spaces and we can verify that it is all working. This is anticipated to take 1 day.
1
+ > ATTENTION: we are currently in the process of releasing this code to the world, as of the afternoon of March 10th, 2025. This notice will be removed, and the version of this repo will be bumped to 1.0.0, once all of the repos have been migrated to the new spaces and we can verify that it is all working. This is anticipated to be done in early April, 2025.
2
2
 
3
3
  # Psychic
4
4
 
5
- Psychic is a typescript first Node framework built on top of [kysely](http://kysely.dev) and heavily inspired by Ruby on Rails. It provides a light-weight routing layer around [expressjs](https://expressjs.com) to create a familiar MVC pattern for those coming from a conventional MVC framework, a type-safe ORM with an incredibly powerful autocomplete API, elegant redis and socket.io bindings for distributed websocket applications, the ability to couple with a front-end framework (like react, angular, etc...) and write specs that drive through your front end, while still maintaining a back end-centric context from which to write specs.
5
+ Psychic is a typescript first Node framework built on top of [kysely](http://kysely.dev) and heavily inspired by Ruby on Rails. It provides a light-weight routing layer around [expressjs](https://expressjs.com) to create a familiar MVC pattern for those coming from a conventional MVC framework, and...
6
6
 
7
- For more comprehensive documentation, please see [The official Psychic guides](https://psychicframework.com). We will also be publishing api docs shortly, but the Psychic guides should be comprehensive enough to provide an understanding of the technology and how to use it.
7
+ Introducing [dream](https://psychicframework.com/docs/models/overview)!, a robust, type-safe ORM with an incredibly powerful autocomplete API, through associations, polymorphism, single table inheritence, deep OpenAPI integration, and much more.
8
+
9
+ In addition, psychic also provides elegant redis and socket.io bindings for distributed websocket applications, a powerful background job system integrated with bullmq to enable you to easily send any of your application code into redis for background processing, the ability to couple with a front-end framework (like nextjs, react, angular etc...) and write specs that drive through your front end, while still maintaining a back end-centric context from which to compose, and much more!
8
10
 
9
- NOTE: doing so will create the new app in the psychic folder, so once done testing remember to remove it.
11
+ For more comprehensive documentation, please see [The official Psychic guides](https://psychicframework.com). We will also be publishing api docs shortly, but the Psychic guides should be comprehensive enough to provide an understanding of the technology and how to use it.
10
12
 
11
13
  ## Questions?
12
14
 
@@ -27,6 +27,20 @@ class PsychicBin {
27
27
  if (!bypassDreamSync)
28
28
  await dream_1.DreamBin.sync(() => { });
29
29
  await PsychicBin.syncTypes();
30
+ const psychicApp = index_js_1.default.getOrFail();
31
+ dream_1.DreamCLI.logger.logStartProgress('running post-sync operations...');
32
+ // call post-sync command in a separate process, so that newly-generated
33
+ // types can be reloaded and brought into all classes.
34
+ await dream_1.DreamCLI.spawn(psychicApp.psyCmd('post-sync'), {
35
+ onStdout: message => {
36
+ dream_1.DreamCLI.logger.logContinueProgress(`[post-sync]` + ' ' + message, {
37
+ logPrefixColor: 'cyan',
38
+ });
39
+ },
40
+ });
41
+ dream_1.DreamCLI.logger.logEndProgress();
42
+ }
43
+ static async postSync() {
30
44
  const psychicApp = index_js_1.default.getOrFail();
31
45
  await PsychicBin.syncOpenapiJson();
32
46
  if (psychicApp.openapi?.syncEnumsToClient) {
@@ -50,6 +50,14 @@ class PsychicCLI {
50
50
  await index_js_1.default.sync();
51
51
  process.exit();
52
52
  });
53
+ program
54
+ .command('post-sync')
55
+ .description('an internal command that runs as the second stage of the `sync` command, since after types are rebuit, the application needs to be reloaded before autogenerating certain files, since those files will need to leverage the updated types')
56
+ .action(async () => {
57
+ await initializePsychicApplication();
58
+ await index_js_1.default.postSync();
59
+ process.exit();
60
+ });
53
61
  program
54
62
  .command('sync:routes')
55
63
  .description('reads the routes generated by your app and generates a cache file, which is then used to give autocomplete support to the route helper, amoongst other things.')
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class PsychicApplicationInitMissingPackageManager extends Error {
4
+ constructor() {
5
+ super();
6
+ }
7
+ get message() {
8
+ return `
9
+ must set packageManager when initializing a new PsychicApplication.
10
+
11
+ within conf/app.ts, you must have a call to "#set('packageManager', '<YOUR_CHOSEN_PACKAGE_MANAGER>')", i.e.
12
+
13
+ // conf/app.ts
14
+ export default async (app: PsychicApplication) => {
15
+ await app.set('packageManager', 'yarn')
16
+ }
17
+ `;
18
+ }
19
+ }
20
+ exports.default = PsychicApplicationInitMissingPackageManager;
@@ -12,6 +12,8 @@ function default_1(dreamPathType) {
12
12
  return psychicApp.paths.controllers;
13
13
  case 'controllerSpecs':
14
14
  return psychicApp.paths.controllerSpecs;
15
+ case 'services':
16
+ return psychicApp.paths.services;
15
17
  default:
16
18
  return (0, dream_1.dreamPath)(dreamPathType);
17
19
  }
@@ -8,5 +8,14 @@ class PsychicImporter {
8
8
  const controllerClasses = (await Promise.all(controllerPaths.map(controllerPath => importCb(controllerPath).then(dreamClass => [controllerPath, dreamClass]))));
9
9
  return controllerClasses;
10
10
  }
11
+ static async importServices(pathToServices,
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ importCb) {
14
+ const servicePaths = await dream_1.DreamImporter.ls(pathToServices);
15
+ const serviceClasses = (await Promise.all(servicePaths.map(servicePath =>
16
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
17
+ importCb(servicePath).then(serviceClass => [servicePath, serviceClass]))));
18
+ return serviceClasses;
19
+ }
11
20
  }
12
21
  exports.default = PsychicImporter;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ function default_1(filepath, dirPath) {
5
+ const prefixPath = dirPath;
6
+ return ('services/' +
7
+ filepath
8
+ .replace(prefixPath, '')
9
+ .replace(/\.[jt]s$/, '')
10
+ .replace(/^\//, ''));
11
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = importServices;
4
+ exports.setCachedServices = setCachedServices;
5
+ exports.getServicesOrFail = getServicesOrFail;
6
+ exports.getServicesOrBlank = getServicesOrBlank;
7
+ const globalServiceKeyFromPath_js_1 = require("../globalServiceKeyFromPath.js");
8
+ const PsychicImporter_js_1 = require("../PsychicImporter.js");
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ let _services;
11
+ async function importServices(servicesPath,
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ serviceImportCb) {
14
+ if (_services)
15
+ return _services;
16
+ _services = {};
17
+ const serviceClasses = await PsychicImporter_js_1.default.importServices(servicesPath, serviceImportCb);
18
+ for (const [servicePath, serviceClass] of serviceClasses) {
19
+ const typedServiceClass = serviceClass;
20
+ const serviceKey = (0, globalServiceKeyFromPath_js_1.default)(servicePath, servicesPath);
21
+ if (typeof typedServiceClass['setGlobalName'] === 'function') {
22
+ typedServiceClass['setGlobalName'](serviceKey);
23
+ }
24
+ else {
25
+ typedServiceClass.globalName = serviceKey;
26
+ }
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28
+ _services[serviceKey] = serviceClass;
29
+ }
30
+ return _services;
31
+ }
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ function setCachedServices(services) {
34
+ _services = services;
35
+ }
36
+ function getServicesOrFail() {
37
+ if (!_services)
38
+ throw new Error('Must call loadServices before calling getServicesOrFail');
39
+ return _services;
40
+ }
41
+ function getServicesOrBlank() {
42
+ return _services || {};
43
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = lookupClassByGlobalName;
4
4
  const dream_1 = require("@rvoh/dream");
5
5
  const importControllers_js_1 = require("./import/importControllers.js");
6
+ const importServices_js_1 = require("./import/importServices.js");
6
7
  function lookupClassByGlobalName(name) {
7
8
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
8
9
  const lookup = (0, dream_1.lookupClassByGlobalName)(name);
@@ -11,6 +12,8 @@ function lookupClassByGlobalName(name) {
11
12
  return lookup;
12
13
  const combinedObj = {
13
14
  ...(0, importControllers_js_1.getControllersOrFail)(),
15
+ ...(0, importServices_js_1.getServicesOrFail)(),
14
16
  };
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
15
18
  return combinedObj[name] || null;
16
19
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsychicApplicationAllowedPackageManagersEnumValues = void 0;
3
4
  const dream_1 = require("@rvoh/dream");
4
5
  const init_missing_api_root_js_1 = require("../error/psychic-application/init-missing-api-root.js");
5
6
  const init_missing_call_to_load_controllers_js_1 = require("../error/psychic-application/init-missing-call-to-load-controllers.js");
@@ -9,6 +10,9 @@ const EnvInternal_js_1 = require("../helpers/EnvInternal.js");
9
10
  const cache_js_1 = require("./cache.js");
10
11
  const importControllers_js_1 = require("./helpers/import/importControllers.js");
11
12
  const lookupClassByGlobalName_js_1 = require("./helpers/lookupClassByGlobalName.js");
13
+ const init_missing_package_manager_js_1 = require("../error/psychic-application/init-missing-package-manager.js");
14
+ const importServices_js_1 = require("./helpers/import/importServices.js");
15
+ const pascalizeFileName_js_1 = require("../helpers/pascalizeFileName.js");
12
16
  class PsychicApplication {
13
17
  static async init(cb, dreamCb, opts = {}) {
14
18
  let psychicApp;
@@ -21,11 +25,27 @@ class PsychicApplication {
21
25
  throw new init_missing_api_root_js_1.default();
22
26
  if (!psychicApp.routesCb)
23
27
  throw new init_missing_routes_callback_js_1.default();
28
+ if (!exports.PsychicApplicationAllowedPackageManagersEnumValues.includes(psychicApp.packageManager))
29
+ throw new init_missing_package_manager_js_1.default();
24
30
  if (psychicApp.encryption?.cookies?.current)
25
31
  this.checkKey('cookies', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
26
32
  await psychicApp.inflections?.();
27
33
  dreamApp.set('projectRoot', psychicApp.apiRoot);
28
34
  dreamApp.set('logger', psychicApp.logger);
35
+ dreamApp.on('repl:start', context => {
36
+ const psychicApp = PsychicApplication.getOrFail();
37
+ for (const globalName of Object.keys(psychicApp.services)) {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
39
+ if (!context[globalName]) {
40
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
41
+ ;
42
+ context[(0, pascalizeFileName_js_1.default)(globalName)] = psychicApp.services[globalName];
43
+ }
44
+ }
45
+ });
46
+ for (const plugin of psychicApp.plugins) {
47
+ await plugin(psychicApp);
48
+ }
29
49
  (0, cache_js_1.cachePsychicApplication)(psychicApp);
30
50
  });
31
51
  return psychicApp;
@@ -34,6 +54,29 @@ class PsychicApplication {
34
54
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
35
55
  return (0, lookupClassByGlobalName_js_1.default)(name);
36
56
  }
57
+ /**
58
+ * @internal
59
+ *
60
+ * used to provide the correct package manager syntax for running a script
61
+ * inside of the package.json "scripts" section.
62
+ */
63
+ packageManagerRunCmd(cmd) {
64
+ switch (this.packageManager) {
65
+ case 'npm':
66
+ return `npm run ${cmd}`;
67
+ default:
68
+ return `${this.packageManager} ${cmd}`;
69
+ }
70
+ }
71
+ /**
72
+ * @internal
73
+ *
74
+ * adds the necessary package manager prefix to the psy command provided
75
+ * i.e. `psyCmd('sync')`
76
+ */
77
+ psyCmd(cmd) {
78
+ return this.packageManagerRunCmd(`psy ${cmd}`);
79
+ }
37
80
  static checkKey(encryptionIdentifier, key, algorithm) {
38
81
  if (!dream_1.Encrypt.validateKey(key, algorithm))
39
82
  console.warn(`
@@ -108,14 +151,18 @@ Try setting it to something valid, like:
108
151
  get logger() {
109
152
  return this._logger;
110
153
  }
111
- _sslCredentials;
154
+ _sslCredentials = undefined;
112
155
  get sslCredentials() {
113
156
  return this._sslCredentials;
114
157
  }
115
- _saltRounds;
158
+ _saltRounds = undefined;
116
159
  get saltRounds() {
117
160
  return this._saltRounds;
118
161
  }
162
+ _packageManager;
163
+ get packageManager() {
164
+ return this._packageManager;
165
+ }
119
166
  _routesCb;
120
167
  get routesCb() {
121
168
  return this._routesCb;
@@ -143,6 +190,7 @@ Try setting it to something valid, like:
143
190
  _paths = {
144
191
  apiRoutes: 'src/conf/routes.ts',
145
192
  controllers: 'src/app/controllers',
193
+ services: 'src/app/services',
146
194
  controllerSpecs: 'spec/unit/controllers',
147
195
  };
148
196
  get paths() {
@@ -183,6 +231,10 @@ Try setting it to something valid, like:
183
231
  get loadedControllers() {
184
232
  return this._loadedControllers;
185
233
  }
234
+ _loadedServices = false;
235
+ get loadedServices() {
236
+ return this._loadedServices;
237
+ }
186
238
  _baseDefaultResponseHeaders = {
187
239
  ['cache-control']: 'max-age=0, private, must-revalidate',
188
240
  };
@@ -196,6 +248,13 @@ Try setting it to something valid, like:
196
248
  get controllers() {
197
249
  return (0, importControllers_js_1.getControllersOrFail)();
198
250
  }
251
+ get services() {
252
+ return (0, importServices_js_1.getServicesOrFail)();
253
+ }
254
+ _plugins = [];
255
+ get plugins() {
256
+ return this._plugins;
257
+ }
199
258
  async load(resourceType, resourcePath,
200
259
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
260
  importCb) {
@@ -204,6 +263,10 @@ Try setting it to something valid, like:
204
263
  await (0, importControllers_js_1.default)(this, resourcePath, importCb);
205
264
  this._loadedControllers = true;
206
265
  break;
266
+ case 'services':
267
+ await (0, importServices_js_1.default)(resourcePath, importCb);
268
+ this._loadedServices = true;
269
+ break;
207
270
  }
208
271
  }
209
272
  booted = false;
@@ -226,6 +289,9 @@ Try setting it to something valid, like:
226
289
  await this.inflections?.();
227
290
  this.booted = true;
228
291
  }
292
+ plugin(cb) {
293
+ this._plugins.push(cb);
294
+ }
229
295
  on(hookEventType, cb) {
230
296
  switch (hookEventType) {
231
297
  case 'server:error':
@@ -306,6 +372,9 @@ Try setting it to something valid, like:
306
372
  case 'port':
307
373
  this._port = value;
308
374
  break;
375
+ case 'packageManager':
376
+ this._packageManager = value;
377
+ break;
309
378
  case 'saltRounds':
310
379
  this._saltRounds = value;
311
380
  break;
@@ -344,3 +413,4 @@ Try setting it to something valid, like:
344
413
  }
345
414
  }
346
415
  exports.default = PsychicApplication;
416
+ exports.PsychicApplicationAllowedPackageManagersEnumValues = ['yarn', 'npm', 'pnpm'];
@@ -25,6 +25,20 @@ export default class PsychicBin {
25
25
  if (!bypassDreamSync)
26
26
  await DreamBin.sync(() => { });
27
27
  await PsychicBin.syncTypes();
28
+ const psychicApp = PsychicApplication.getOrFail();
29
+ DreamCLI.logger.logStartProgress('running post-sync operations...');
30
+ // call post-sync command in a separate process, so that newly-generated
31
+ // types can be reloaded and brought into all classes.
32
+ await DreamCLI.spawn(psychicApp.psyCmd('post-sync'), {
33
+ onStdout: message => {
34
+ DreamCLI.logger.logContinueProgress(`[post-sync]` + ' ' + message, {
35
+ logPrefixColor: 'cyan',
36
+ });
37
+ },
38
+ });
39
+ DreamCLI.logger.logEndProgress();
40
+ }
41
+ static async postSync() {
28
42
  const psychicApp = PsychicApplication.getOrFail();
29
43
  await PsychicBin.syncOpenapiJson();
30
44
  if (psychicApp.openapi?.syncEnumsToClient) {
@@ -48,6 +48,14 @@ export default class PsychicCLI {
48
48
  await PsychicBin.sync();
49
49
  process.exit();
50
50
  });
51
+ program
52
+ .command('post-sync')
53
+ .description('an internal command that runs as the second stage of the `sync` command, since after types are rebuit, the application needs to be reloaded before autogenerating certain files, since those files will need to leverage the updated types')
54
+ .action(async () => {
55
+ await initializePsychicApplication();
56
+ await PsychicBin.postSync();
57
+ process.exit();
58
+ });
51
59
  program
52
60
  .command('sync:routes')
53
61
  .description('reads the routes generated by your app and generates a cache file, which is then used to give autocomplete support to the route helper, amoongst other things.')
@@ -0,0 +1,17 @@
1
+ export default class PsychicApplicationInitMissingPackageManager extends Error {
2
+ constructor() {
3
+ super();
4
+ }
5
+ get message() {
6
+ return `
7
+ must set packageManager when initializing a new PsychicApplication.
8
+
9
+ within conf/app.ts, you must have a call to "#set('packageManager', '<YOUR_CHOSEN_PACKAGE_MANAGER>')", i.e.
10
+
11
+ // conf/app.ts
12
+ export default async (app: PsychicApplication) => {
13
+ await app.set('packageManager', 'yarn')
14
+ }
15
+ `;
16
+ }
17
+ }
@@ -9,6 +9,8 @@ export default function (dreamPathType) {
9
9
  return psychicApp.paths.controllers;
10
10
  case 'controllerSpecs':
11
11
  return psychicApp.paths.controllerSpecs;
12
+ case 'services':
13
+ return psychicApp.paths.services;
12
14
  default:
13
15
  return dreamPath(dreamPathType);
14
16
  }
@@ -6,4 +6,13 @@ export default class PsychicImporter {
6
6
  const controllerClasses = (await Promise.all(controllerPaths.map(controllerPath => importCb(controllerPath).then(dreamClass => [controllerPath, dreamClass]))));
7
7
  return controllerClasses;
8
8
  }
9
+ static async importServices(pathToServices,
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ importCb) {
12
+ const servicePaths = await DreamImporter.ls(pathToServices);
13
+ const serviceClasses = (await Promise.all(servicePaths.map(servicePath =>
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
15
+ importCb(servicePath).then(serviceClass => [servicePath, serviceClass]))));
16
+ return serviceClasses;
17
+ }
9
18
  }
@@ -0,0 +1,8 @@
1
+ export default function (filepath, dirPath) {
2
+ const prefixPath = dirPath;
3
+ return ('services/' +
4
+ filepath
5
+ .replace(prefixPath, '')
6
+ .replace(/\.[jt]s$/, '')
7
+ .replace(/^\//, ''));
8
+ }
@@ -0,0 +1,37 @@
1
+ import globalServiceKeyFromPath from '../globalServiceKeyFromPath.js';
2
+ import PsychicImporter from '../PsychicImporter.js';
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ let _services;
5
+ export default async function importServices(servicesPath,
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ serviceImportCb) {
8
+ if (_services)
9
+ return _services;
10
+ _services = {};
11
+ const serviceClasses = await PsychicImporter.importServices(servicesPath, serviceImportCb);
12
+ for (const [servicePath, serviceClass] of serviceClasses) {
13
+ const typedServiceClass = serviceClass;
14
+ const serviceKey = globalServiceKeyFromPath(servicePath, servicesPath);
15
+ if (typeof typedServiceClass['setGlobalName'] === 'function') {
16
+ typedServiceClass['setGlobalName'](serviceKey);
17
+ }
18
+ else {
19
+ typedServiceClass.globalName = serviceKey;
20
+ }
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22
+ _services[serviceKey] = serviceClass;
23
+ }
24
+ return _services;
25
+ }
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ export function setCachedServices(services) {
28
+ _services = services;
29
+ }
30
+ export function getServicesOrFail() {
31
+ if (!_services)
32
+ throw new Error('Must call loadServices before calling getServicesOrFail');
33
+ return _services;
34
+ }
35
+ export function getServicesOrBlank() {
36
+ return _services || {};
37
+ }
@@ -1,5 +1,6 @@
1
1
  import { lookupClassByGlobalName as dreamLookupClassByGlobalName } from '@rvoh/dream';
2
2
  import { getControllersOrFail } from './import/importControllers.js';
3
+ import { getServicesOrFail } from './import/importServices.js';
3
4
  export default function lookupClassByGlobalName(name) {
4
5
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
5
6
  const lookup = dreamLookupClassByGlobalName(name);
@@ -8,6 +9,8 @@ export default function lookupClassByGlobalName(name) {
8
9
  return lookup;
9
10
  const combinedObj = {
10
11
  ...getControllersOrFail(),
12
+ ...getServicesOrFail(),
11
13
  };
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
12
15
  return combinedObj[name] || null;
13
16
  }
@@ -7,6 +7,9 @@ import EnvInternal from '../helpers/EnvInternal.js';
7
7
  import { cachePsychicApplication, getCachedPsychicApplicationOrFail } from './cache.js';
8
8
  import importControllers, { getControllersOrFail } from './helpers/import/importControllers.js';
9
9
  import lookupClassByGlobalName from './helpers/lookupClassByGlobalName.js';
10
+ import PsychicApplicationInitMissingPackageManager from '../error/psychic-application/init-missing-package-manager.js';
11
+ import importServices, { getServicesOrFail } from './helpers/import/importServices.js';
12
+ import pascalizeFileName from '../helpers/pascalizeFileName.js';
10
13
  export default class PsychicApplication {
11
14
  static async init(cb, dreamCb, opts = {}) {
12
15
  let psychicApp;
@@ -19,11 +22,27 @@ export default class PsychicApplication {
19
22
  throw new PsychicApplicationInitMissingApiRoot();
20
23
  if (!psychicApp.routesCb)
21
24
  throw new PsychicApplicationInitMissingRoutesCallback();
25
+ if (!PsychicApplicationAllowedPackageManagersEnumValues.includes(psychicApp.packageManager))
26
+ throw new PsychicApplicationInitMissingPackageManager();
22
27
  if (psychicApp.encryption?.cookies?.current)
23
28
  this.checkKey('cookies', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
24
29
  await psychicApp.inflections?.();
25
30
  dreamApp.set('projectRoot', psychicApp.apiRoot);
26
31
  dreamApp.set('logger', psychicApp.logger);
32
+ dreamApp.on('repl:start', context => {
33
+ const psychicApp = PsychicApplication.getOrFail();
34
+ for (const globalName of Object.keys(psychicApp.services)) {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
36
+ if (!context[globalName]) {
37
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
38
+ ;
39
+ context[pascalizeFileName(globalName)] = psychicApp.services[globalName];
40
+ }
41
+ }
42
+ });
43
+ for (const plugin of psychicApp.plugins) {
44
+ await plugin(psychicApp);
45
+ }
27
46
  cachePsychicApplication(psychicApp);
28
47
  });
29
48
  return psychicApp;
@@ -32,6 +51,29 @@ export default class PsychicApplication {
32
51
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
33
52
  return lookupClassByGlobalName(name);
34
53
  }
54
+ /**
55
+ * @internal
56
+ *
57
+ * used to provide the correct package manager syntax for running a script
58
+ * inside of the package.json "scripts" section.
59
+ */
60
+ packageManagerRunCmd(cmd) {
61
+ switch (this.packageManager) {
62
+ case 'npm':
63
+ return `npm run ${cmd}`;
64
+ default:
65
+ return `${this.packageManager} ${cmd}`;
66
+ }
67
+ }
68
+ /**
69
+ * @internal
70
+ *
71
+ * adds the necessary package manager prefix to the psy command provided
72
+ * i.e. `psyCmd('sync')`
73
+ */
74
+ psyCmd(cmd) {
75
+ return this.packageManagerRunCmd(`psy ${cmd}`);
76
+ }
35
77
  static checkKey(encryptionIdentifier, key, algorithm) {
36
78
  if (!Encrypt.validateKey(key, algorithm))
37
79
  console.warn(`
@@ -106,14 +148,18 @@ Try setting it to something valid, like:
106
148
  get logger() {
107
149
  return this._logger;
108
150
  }
109
- _sslCredentials;
151
+ _sslCredentials = undefined;
110
152
  get sslCredentials() {
111
153
  return this._sslCredentials;
112
154
  }
113
- _saltRounds;
155
+ _saltRounds = undefined;
114
156
  get saltRounds() {
115
157
  return this._saltRounds;
116
158
  }
159
+ _packageManager;
160
+ get packageManager() {
161
+ return this._packageManager;
162
+ }
117
163
  _routesCb;
118
164
  get routesCb() {
119
165
  return this._routesCb;
@@ -141,6 +187,7 @@ Try setting it to something valid, like:
141
187
  _paths = {
142
188
  apiRoutes: 'src/conf/routes.ts',
143
189
  controllers: 'src/app/controllers',
190
+ services: 'src/app/services',
144
191
  controllerSpecs: 'spec/unit/controllers',
145
192
  };
146
193
  get paths() {
@@ -181,6 +228,10 @@ Try setting it to something valid, like:
181
228
  get loadedControllers() {
182
229
  return this._loadedControllers;
183
230
  }
231
+ _loadedServices = false;
232
+ get loadedServices() {
233
+ return this._loadedServices;
234
+ }
184
235
  _baseDefaultResponseHeaders = {
185
236
  ['cache-control']: 'max-age=0, private, must-revalidate',
186
237
  };
@@ -194,6 +245,13 @@ Try setting it to something valid, like:
194
245
  get controllers() {
195
246
  return getControllersOrFail();
196
247
  }
248
+ get services() {
249
+ return getServicesOrFail();
250
+ }
251
+ _plugins = [];
252
+ get plugins() {
253
+ return this._plugins;
254
+ }
197
255
  async load(resourceType, resourcePath,
198
256
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
257
  importCb) {
@@ -202,6 +260,10 @@ Try setting it to something valid, like:
202
260
  await importControllers(this, resourcePath, importCb);
203
261
  this._loadedControllers = true;
204
262
  break;
263
+ case 'services':
264
+ await importServices(resourcePath, importCb);
265
+ this._loadedServices = true;
266
+ break;
205
267
  }
206
268
  }
207
269
  booted = false;
@@ -224,6 +286,9 @@ Try setting it to something valid, like:
224
286
  await this.inflections?.();
225
287
  this.booted = true;
226
288
  }
289
+ plugin(cb) {
290
+ this._plugins.push(cb);
291
+ }
227
292
  on(hookEventType, cb) {
228
293
  switch (hookEventType) {
229
294
  case 'server:error':
@@ -304,6 +369,9 @@ Try setting it to something valid, like:
304
369
  case 'port':
305
370
  this._port = value;
306
371
  break;
372
+ case 'packageManager':
373
+ this._packageManager = value;
374
+ break;
307
375
  case 'saltRounds':
308
376
  this._saltRounds = value;
309
377
  break;
@@ -341,3 +409,4 @@ Try setting it to something valid, like:
341
409
  }
342
410
  }
343
411
  }
412
+ export const PsychicApplicationAllowedPackageManagersEnumValues = ['yarn', 'npm', 'pnpm'];
@@ -5,6 +5,7 @@ export default class PsychicBin {
5
5
  static sync({ bypassDreamSync }?: {
6
6
  bypassDreamSync?: boolean;
7
7
  }): Promise<void>;
8
+ static postSync(): Promise<void>;
8
9
  static syncTypes(customTypes?: any): Promise<void>;
9
10
  static syncOpenapiJson(): Promise<void>;
10
11
  static syncRoutes(): Promise<void>;
@@ -0,0 +1,4 @@
1
+ export default class PsychicApplicationInitMissingPackageManager extends Error {
2
+ constructor();
3
+ get message(): string;
4
+ }
@@ -1,4 +1,4 @@
1
1
  export default function (dreamPathType: PsychicPaths): string;
2
2
  type DreamPaths = 'models' | 'modelSpecs' | 'serializers' | 'db' | 'conf' | 'factories';
3
- export type PsychicPaths = DreamPaths | 'apiRoutes' | 'controllers' | 'controllerSpecs';
3
+ export type PsychicPaths = DreamPaths | 'apiRoutes' | 'controllers' | 'controllerSpecs' | 'services';
4
4
  export {};
@@ -1,4 +1,5 @@
1
1
  import PsychicController from '../../controller/index.js';
2
2
  export default class PsychicImporter {
3
3
  static importControllers(controllersPath: string, importCb: (path: string) => Promise<any>): Promise<[string, typeof PsychicController][]>;
4
+ static importServices(pathToServices: string, importCb: (path: string) => Promise<any>): Promise<[string, any][]>;
4
5
  }
@@ -0,0 +1 @@
1
+ export default function (filepath: string, dirPath: string): string;
@@ -0,0 +1,4 @@
1
+ export default function importServices(servicesPath: string, serviceImportCb: (path: string) => Promise<any>): Promise<Record<string, any>>;
2
+ export declare function setCachedServices(services: Record<string, any>): void;
3
+ export declare function getServicesOrFail(): Record<string, any>;
4
+ export declare function getServicesOrBlank(): Record<string, any>;
@@ -11,6 +11,20 @@ import { PsychicHookEventType, PsychicHookLoadEventTypes } from './types.js';
11
11
  export default class PsychicApplication {
12
12
  static init(cb: (app: PsychicApplication) => void | Promise<void>, dreamCb: (app: DreamApplication) => void | Promise<void>, opts?: PsychicApplicationInitOptions): Promise<PsychicApplication>;
13
13
  static lookupClassByGlobalName(name: string): any;
14
+ /**
15
+ * @internal
16
+ *
17
+ * used to provide the correct package manager syntax for running a script
18
+ * inside of the package.json "scripts" section.
19
+ */
20
+ packageManagerRunCmd(cmd: string): string;
21
+ /**
22
+ * @internal
23
+ *
24
+ * adds the necessary package manager prefix to the psy command provided
25
+ * i.e. `psyCmd('sync')`
26
+ */
27
+ psyCmd(cmd: string): string;
14
28
  private static checkKey;
15
29
  /**
16
30
  * Returns the cached psychic application if it has been set.
@@ -45,10 +59,12 @@ export default class PsychicApplication {
45
59
  };
46
60
  private _logger;
47
61
  get logger(): DreamLogger;
48
- private _sslCredentials?;
62
+ private _sslCredentials;
49
63
  get sslCredentials(): PsychicSslCredentials | undefined;
50
- private _saltRounds?;
64
+ private _saltRounds;
51
65
  get saltRounds(): number | undefined;
66
+ private _packageManager;
67
+ get packageManager(): "yarn" | "npm" | "pnpm";
52
68
  private _routesCb;
53
69
  get routesCb(): (r: PsychicRouter) => void | Promise<void>;
54
70
  private _openapi;
@@ -67,22 +83,30 @@ export default class PsychicApplication {
67
83
  private get overrides();
68
84
  private _loadedControllers;
69
85
  get loadedControllers(): boolean;
86
+ private _loadedServices;
87
+ get loadedServices(): boolean;
70
88
  private _baseDefaultResponseHeaders;
71
89
  private _defaultResponseHeaders;
72
90
  get defaultResponseHeaders(): {
73
91
  [x: string]: string | null;
74
92
  };
75
93
  get controllers(): Record<string, typeof import("../index.js").PsychicController>;
76
- load<RT extends 'controllers'>(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise<any>): Promise<void>;
94
+ get services(): Record<string, any>;
95
+ private _plugins;
96
+ get plugins(): ((app: PsychicApplication) => void | Promise<void>)[];
97
+ load<RT extends 'controllers' | 'services'>(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise<any>): Promise<void>;
77
98
  private booted;
78
99
  boot(force?: boolean): Promise<void>;
100
+ plugin(cb: (app: PsychicApplication) => void | Promise<void>): void;
79
101
  on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'sync' ? () => any : (conf: PsychicApplication) => void | Promise<void>): void;
80
102
  set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
81
- set<Opt extends PsychicApplicationOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicApplicationEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'sessionCookieName' ? string : Opt extends 'clientRoot' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'client' ? PsychicClientOptions : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
103
+ set<Opt extends PsychicApplicationOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicApplicationEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'sessionCookieName' ? string : Opt extends 'clientRoot' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'client' ? PsychicClientOptions : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? PsychicApplicationAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
82
104
  override<Override extends keyof PsychicApplicationOverrides>(override: Override, value: PsychicApplicationOverrides[Override]): void;
83
105
  private runHooksFor;
84
106
  }
85
- export type PsychicApplicationOption = 'appName' | 'apiOnly' | 'apiRoot' | 'encryption' | 'sessionCookieName' | 'client' | 'clientRoot' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
107
+ export type PsychicApplicationOption = 'appName' | 'apiOnly' | 'apiRoot' | 'encryption' | 'sessionCookieName' | 'client' | 'clientRoot' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
108
+ export declare const PsychicApplicationAllowedPackageManagersEnumValues: readonly ["yarn", "npm", "pnpm"];
109
+ export type PsychicApplicationAllowedPackageManagersEnum = (typeof PsychicApplicationAllowedPackageManagersEnumValues)[number];
86
110
  export interface PsychicApplicationSpecialHooks {
87
111
  sync: (() => any)[];
88
112
  serverInit: ((server: PsychicServer) => void | Promise<void>)[];
@@ -143,6 +167,7 @@ interface PsychicOpenapiInfo {
143
167
  }
144
168
  interface PsychicPathOptions {
145
169
  apiRoutes?: string;
170
+ services?: string;
146
171
  controllers?: string;
147
172
  controllerSpecs?: string;
148
173
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "0.27.0",
5
+ "version": "0.28.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -24,11 +24,10 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "client": "yarn --cwd=./client start",
27
- "psy": "NODE_ENV=${NODE_ENV:-test} yarn psyts",
27
+ "psy": "PSYCHIC_CORE_DEVELOPMENT=1 NODE_ENV=${NODE_ENV:-test} yarn psyts",
28
28
  "psyjs": "node ./dist/test-app/src/cli/index.js",
29
29
  "psyts": "NODE_ENV=${NODE_ENV:-test} tsx ./test-app/src/cli/index.ts",
30
- "psycore": "PSYCHIC_CORE_DEVELOPMENT=1 yarn psyts",
31
- "gpsycore": "PSYCHIC_CORE_DEVELOPMENT=1 tsx ./global-cli/main.ts",
30
+ "gpsy": "PSYCHIC_CORE_DEVELOPMENT=1 tsx ./global-cli/main.ts",
32
31
  "build": "echo \"building cjs...\" && rm -rf dist && npx tsc -p ./tsconfig.cjs.build.json && echo \"building esm...\" && npx tsc -p ./tsconfig.esm.build.json",
33
32
  "build:test-app": "rm -rf dist && echo \"building test app to esm...\" && npx tsc -p ./tsconfig.esm.build.test-app.json && echo \"building test app to cjs...\" && npx tsc -p ./tsconfig.cjs.build.test-app.json",
34
33
  "dev": "PSYCHIC_CORE_DEVELOPMENT=1 NODE_ENV=development tsx ./test-app/main.ts",
@@ -63,7 +62,7 @@
63
62
  "devDependencies": {
64
63
  "@eslint/js": "^9.19.0",
65
64
  "@jest-mock/express": "^3.0.0",
66
- "@rvoh/dream": "^0.31.3",
65
+ "@rvoh/dream": "^0.33.0",
67
66
  "@rvoh/dream-spec-helpers": "=0.1.0",
68
67
  "@rvoh/psychic-spec-helpers": "=0.2.0",
69
68
  "@types/express": "^4.17.21",
@@ -89,8 +88,8 @@
89
88
  "typedoc": "^0.26.6",
90
89
  "typescript": "^5.5.4",
91
90
  "typescript-eslint": "=7.18.0",
92
- "vitest": "^3.0.9",
91
+ "vitest": "^3.1.1",
93
92
  "winston": "^3.14.2"
94
93
  },
95
94
  "packageManager": "yarn@4.7.0"
96
- }
95
+ }
@@ -1,26 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = sspawn;
4
- exports.ssspawn = ssspawn;
5
- const child_process_1 = require("child_process");
6
- function sspawn(command,
7
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- opts = {}) {
9
- return new Promise((accept, reject) => {
10
- ssspawn(command, opts).on('close', code => {
11
- if (code !== 0)
12
- reject(new Error(code?.toString()));
13
- accept({});
14
- });
15
- });
16
- }
17
- function ssspawn(command,
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- opts = {}) {
20
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
21
- return (0, child_process_1.spawn)(command, {
22
- stdio: 'inherit',
23
- shell: true,
24
- ...opts,
25
- });
26
- }
@@ -1,22 +0,0 @@
1
- import { spawn } from 'child_process';
2
- export default function sspawn(command,
3
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
- opts = {}) {
5
- return new Promise((accept, reject) => {
6
- ssspawn(command, opts).on('close', code => {
7
- if (code !== 0)
8
- reject(new Error(code?.toString()));
9
- accept({});
10
- });
11
- });
12
- }
13
- export function ssspawn(command,
14
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- opts = {}) {
16
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
17
- return spawn(command, {
18
- stdio: 'inherit',
19
- shell: true,
20
- ...opts,
21
- });
22
- }
@@ -1,2 +0,0 @@
1
- export default function sspawn(command: string, opts?: any): Promise<unknown>;
2
- export declare function ssspawn(command: string, opts?: any): import("child_process").ChildProcessWithoutNullStreams;