proteum 2.2.7 → 2.2.9

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 (43) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -1
  3. package/agents/project/AGENTS.md +4 -5
  4. package/agents/project/CODING_STYLE.md +5 -1
  5. package/agents/project/client/AGENTS.md +2 -0
  6. package/agents/project/diagnostics.md +2 -3
  7. package/agents/project/root/AGENTS.md +4 -5
  8. package/agents/project/tests/AGENTS.md +0 -1
  9. package/cli/commands/check.ts +21 -3
  10. package/cli/commands/configure.ts +1 -0
  11. package/cli/compiler/artifacts/controllers.ts +30 -4
  12. package/cli/compiler/artifacts/services.ts +67 -29
  13. package/cli/presentation/commands.ts +2 -2
  14. package/cli/scaffold/templates.ts +2 -3
  15. package/cli/utils/agents.ts +114 -4
  16. package/client/dev/profiler/ApexChart.tsx +4 -3
  17. package/common/dev/inspection.ts +16 -3
  18. package/common/dev/serverHotReload.ts +26 -25
  19. package/eslint.js +220 -0
  20. package/package.json +1 -1
  21. package/server/app/commands.ts +11 -16
  22. package/server/app/commandsManager.ts +5 -1
  23. package/server/app/controller/index.ts +68 -16
  24. package/server/app/devCommands.ts +3 -3
  25. package/server/app/devDiagnostics.ts +2 -2
  26. package/server/app/index.ts +19 -8
  27. package/server/app/service/container.ts +22 -19
  28. package/server/app/service/index.ts +33 -13
  29. package/server/app.tsconfig.json +0 -1
  30. package/server/services/auth/index.ts +12 -6
  31. package/server/services/auth/router/index.ts +12 -14
  32. package/server/services/auth/router/request.ts +34 -13
  33. package/server/services/disks/driver.ts +1 -1
  34. package/server/services/disks/index.ts +11 -8
  35. package/server/services/email/index.ts +1 -1
  36. package/server/services/prisma/Facet.ts +6 -5
  37. package/server/services/router/index.ts +8 -7
  38. package/server/services/router/request/validation/zod.ts +2 -0
  39. package/server/services/router/response/index.ts +9 -9
  40. package/server/services/router/service.ts +12 -8
  41. package/tests/agents-utils.test.cjs +55 -0
  42. package/tests/eslint-rules.test.cjs +110 -0
  43. package/types/global/vendors.d.ts +70 -0
@@ -18,7 +18,7 @@ export default function ApexChart({
18
18
  if (!target || !options) return;
19
19
 
20
20
  let disposed = false;
21
- let chart: { destroy: () => void } | undefined;
21
+ let chart: { destroy: () => void; render: () => Promise<unknown> | void } | undefined;
22
22
 
23
23
  target.innerHTML = '';
24
24
  setErrorMessage(undefined);
@@ -29,8 +29,9 @@ export default function ApexChart({
29
29
  if (disposed || !mountRef.current) return;
30
30
 
31
31
  const ApexCharts = module.default;
32
- chart = new ApexCharts(mountRef.current, options);
33
- await chart.render();
32
+ const nextChart = new ApexCharts(mountRef.current, options);
33
+ chart = nextChart;
34
+ await nextChart.render();
34
35
  } catch (error) {
35
36
  if (!disposed) setErrorMessage(readErrorMessage(error));
36
37
  }
@@ -713,12 +713,25 @@ const resolveGuidanceFile = ({
713
713
  fallbackFilepath: string;
714
714
  relativePath: string;
715
715
  }) => {
716
- const localFilepath = joinPath(appRoot, relativePath);
717
- if (fileExists(localFilepath)) return { filepath: localFilepath, warning: undefined as string | undefined };
716
+ const resolvedAppRoot = resolvePath(appRoot);
717
+ const repoRoot = findRepoRoot(resolvedAppRoot);
718
+ let currentRoot = resolvedAppRoot;
719
+
720
+ while (true) {
721
+ const localFilepath = joinPath(currentRoot, relativePath);
722
+ if (fileExists(localFilepath)) return { filepath: localFilepath, warning: undefined as string | undefined };
723
+
724
+ if (currentRoot === repoRoot) break;
725
+
726
+ const parentRoot = dirnamePath(currentRoot);
727
+ if (parentRoot === currentRoot) break;
728
+
729
+ currentRoot = parentRoot;
730
+ }
718
731
 
719
732
  return {
720
733
  filepath: fallbackFilepath,
721
- warning: `Missing ${relativePath} in ${appRoot}; using ${fallbackFilepath}.`,
734
+ warning: `Missing ${relativePath} in ${appRoot} and its repository ancestors; using ${fallbackFilepath}.`,
722
735
  };
723
736
  };
724
737
 
@@ -27,33 +27,34 @@ export type TServerReadyMessage = {
27
27
  connectedProjects?: TServerReadyConnectedProject[];
28
28
  };
29
29
 
30
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
31
+
30
32
  export const isServerHotReloadRequest = (value: unknown): value is TServerHotReloadRequest =>
31
- typeof value === 'object' &&
32
- value !== null &&
33
- (value as TServerHotReloadRequest).type === serverHotReloadMessageType.request &&
34
- Array.isArray((value as TServerHotReloadRequest).changedFiles);
33
+ isRecord(value) &&
34
+ value.type === serverHotReloadMessageType.request &&
35
+ Array.isArray(value.changedFiles);
35
36
 
36
37
  export const isServerHotReloadResult = (value: unknown): value is TServerHotReloadResult =>
37
- typeof value === 'object' &&
38
- value !== null &&
39
- ((value as TServerHotReloadResult).type === serverHotReloadMessageType.succeeded ||
40
- (value as TServerHotReloadResult).type === serverHotReloadMessageType.failed) &&
41
- Array.isArray((value as TServerHotReloadResult).changedFiles);
38
+ isRecord(value) &&
39
+ (value.type === serverHotReloadMessageType.succeeded || value.type === serverHotReloadMessageType.failed) &&
40
+ Array.isArray(value.changedFiles);
42
41
 
43
42
  const isServerReadyConnectedProject = (value: unknown): value is TServerReadyConnectedProject =>
44
- typeof value === 'object' &&
45
- value !== null &&
46
- typeof (value as TServerReadyConnectedProject).namespace === 'string' &&
47
- typeof (value as TServerReadyConnectedProject).identifier === 'string' &&
48
- typeof (value as TServerReadyConnectedProject).name === 'string' &&
49
- typeof (value as TServerReadyConnectedProject).urlInternal === 'string' &&
50
- typeof (value as TServerReadyConnectedProject).healthUrl === 'string';
51
-
52
- export const isServerReadyMessage = (value: unknown): value is TServerReadyMessage =>
53
- typeof value === 'object' &&
54
- value !== null &&
55
- (value as TServerReadyMessage).type === serverHotReloadMessageType.ready &&
56
- typeof (value as TServerReadyMessage).publicUrl === 'string' &&
57
- ((value as TServerReadyMessage).connectedProjects === undefined ||
58
- (Array.isArray((value as TServerReadyMessage).connectedProjects) &&
59
- (value as TServerReadyMessage).connectedProjects.every(isServerReadyConnectedProject)));
43
+ isRecord(value) &&
44
+ typeof value.namespace === 'string' &&
45
+ typeof value.identifier === 'string' &&
46
+ typeof value.name === 'string' &&
47
+ typeof value.urlInternal === 'string' &&
48
+ typeof value.healthUrl === 'string';
49
+
50
+ export const isServerReadyMessage = (value: unknown): value is TServerReadyMessage => {
51
+ if (!isRecord(value)) return false;
52
+
53
+ const connectedProjects = value.connectedProjects;
54
+ return (
55
+ value.type === serverHotReloadMessageType.ready &&
56
+ typeof value.publicUrl === 'string' &&
57
+ (connectedProjects === undefined ||
58
+ (Array.isArray(connectedProjects) && connectedProjects.every(isServerReadyConnectedProject)))
59
+ );
60
+ };
package/eslint.js CHANGED
@@ -14,6 +14,220 @@ const defaultIgnores = [
14
14
  const createZodTypeFactorySelector = (factoryName) =>
15
15
  `CallExpression[callee.type='MemberExpression'][callee.computed=false][callee.object.type='Identifier'][callee.object.name=/^(schema|z|zod)$/][callee.property.name='${factoryName}']`;
16
16
 
17
+ const skippedTraversalKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
18
+
19
+ const traverseNode = (node, visit, parent = null, parentKey = null) => {
20
+ if (!node || typeof node !== 'object') return;
21
+
22
+ visit(node, parent, parentKey);
23
+
24
+ for (const key of Object.keys(node)) {
25
+ if (skippedTraversalKeys.has(key)) continue;
26
+
27
+ const value = node[key];
28
+ if (Array.isArray(value)) {
29
+ value.forEach((child) => {
30
+ if (child && typeof child.type === 'string') traverseNode(child, visit, node, key);
31
+ });
32
+ continue;
33
+ }
34
+
35
+ if (value && typeof value.type === 'string') traverseNode(value, visit, node, key);
36
+ }
37
+ };
38
+
39
+ const collectPatternNames = (node, names = []) => {
40
+ if (!node) return names;
41
+
42
+ if (node.type === 'Identifier') {
43
+ names.push(node.name);
44
+ return names;
45
+ }
46
+
47
+ if (node.type === 'RestElement') return collectPatternNames(node.argument, names);
48
+ if (node.type === 'AssignmentPattern') return collectPatternNames(node.left, names);
49
+ if (node.type === 'TSParameterProperty') return collectPatternNames(node.parameter, names);
50
+
51
+ if (node.type === 'ArrayPattern') {
52
+ node.elements.forEach((element) => collectPatternNames(element, names));
53
+ return names;
54
+ }
55
+
56
+ if (node.type === 'ObjectPattern') {
57
+ node.properties.forEach((property) => {
58
+ if (property.type === 'Property') collectPatternNames(property.value, names);
59
+ if (property.type === 'RestElement') collectPatternNames(property.argument, names);
60
+ });
61
+ }
62
+
63
+ return names;
64
+ };
65
+
66
+ const nodeReferencesName = (node, names) => {
67
+ let references = false;
68
+
69
+ traverseNode(node, (child, parent, parentKey) => {
70
+ if (child.type !== 'Identifier' || !names.includes(child.name)) return;
71
+ if (parent?.type === 'MemberExpression' && parentKey === 'property' && parent.computed === false) return;
72
+ if (parent?.type === 'Property' && parentKey === 'key' && parent.computed === false) return;
73
+ if (parent?.type === 'MethodDefinition' && parentKey === 'key' && parent.computed === false) return;
74
+ if (parent?.type === 'PropertyDefinition' && parentKey === 'key' && parent.computed === false) return;
75
+
76
+ references = true;
77
+ });
78
+
79
+ return references;
80
+ };
81
+
82
+ const collectDerivedErrorNames = (node, baseNames) => {
83
+ const names = [...baseNames];
84
+ let changed = true;
85
+
86
+ while (changed) {
87
+ changed = false;
88
+
89
+ traverseNode(node, (child) => {
90
+ if (child.type === 'VariableDeclarator' && child.id?.type === 'Identifier' && nodeReferencesName(child.init, names)) {
91
+ if (!names.includes(child.id.name)) {
92
+ names.push(child.id.name);
93
+ changed = true;
94
+ }
95
+ }
96
+
97
+ if (
98
+ child.type === 'AssignmentExpression' &&
99
+ child.left?.type === 'Identifier' &&
100
+ nodeReferencesName(child.right, names)
101
+ ) {
102
+ if (!names.includes(child.left.name)) {
103
+ names.push(child.left.name);
104
+ changed = true;
105
+ }
106
+ }
107
+ });
108
+ }
109
+
110
+ return names;
111
+ };
112
+
113
+ const getCalleePropertyName = (callee) => {
114
+ if (!callee) return null;
115
+ if (callee.type === 'Identifier') return callee.name;
116
+ if (callee.type === 'MemberExpression') {
117
+ if (callee.property.type === 'Identifier') return callee.property.name;
118
+ if (callee.property.type === 'Literal') return String(callee.property.value);
119
+ }
120
+
121
+ return null;
122
+ };
123
+
124
+ const preservingCallNames = new Set([
125
+ 'captureError',
126
+ 'captureException',
127
+ 'consoleError',
128
+ 'handleError',
129
+ 'logError',
130
+ 'onError',
131
+ 'reject',
132
+ 'reportError',
133
+ 'setError',
134
+ 'setErrorMessage',
135
+ ]);
136
+
137
+ const preservingMemberNames = new Set(['captureException', 'error', 'handleError', 'reject', 'warn']);
138
+
139
+ const isPreservingCall = (callExpression, names) => {
140
+ const propertyName = getCalleePropertyName(callExpression.callee);
141
+ if (!propertyName) return false;
142
+
143
+ const isKnownPreserver =
144
+ preservingCallNames.has(propertyName) ||
145
+ (callExpression.callee.type === 'MemberExpression' && preservingMemberNames.has(propertyName));
146
+
147
+ return isKnownPreserver && nodeReferencesName(callExpression, names);
148
+ };
149
+
150
+ const handlerPreservesCaughtError = (node, names) => {
151
+ let preserves = false;
152
+
153
+ traverseNode(node, (child) => {
154
+ if (child.type === 'ThrowStatement' && nodeReferencesName(child.argument, names)) preserves = true;
155
+ if (child.type === 'CallExpression' && isPreservingCall(child, names)) preserves = true;
156
+ });
157
+
158
+ return preserves;
159
+ };
160
+
161
+ const directPromiseCatchHandlers = new Set([
162
+ 'captureError',
163
+ 'captureException',
164
+ 'consoleError',
165
+ 'handleError',
166
+ 'logError',
167
+ 'reportError',
168
+ ]);
169
+
170
+ const isDirectPromiseCatchHandler = (node) => {
171
+ const name = getCalleePropertyName(node);
172
+ if (name && directPromiseCatchHandlers.has(name)) return true;
173
+ return node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
174
+ };
175
+
176
+ const createSwallowedErrorRule = () => ({
177
+ meta: {
178
+ type: 'problem',
179
+ docs: {
180
+ description: 'Require caught errors to be preserved, reported, rethrown, or surfaced with original detail.',
181
+ },
182
+ messages: {
183
+ missingParam:
184
+ 'Caught errors must be bound and preserved. Use `catch (error)` and rethrow, report, route, or surface original details.',
185
+ unusedParam:
186
+ 'Caught error `{{name}}` is discarded. Rethrow it, report it, route it to app error handling, or surface its original details.',
187
+ unpreserved:
188
+ 'Caught error `{{name}}` is used but not preserved. Rethrow it, report it, route it, or surface original error details.',
189
+ },
190
+ schema: [],
191
+ },
192
+ create(context) {
193
+ const reportHandler = (node, params, body) => {
194
+ const names = params.flatMap((param) => collectPatternNames(param));
195
+ if (names.length === 0) {
196
+ context.report({ node, messageId: 'missingParam' });
197
+ return;
198
+ }
199
+
200
+ const referencedName = names.find((name) => nodeReferencesName(body, [name]));
201
+ if (!referencedName) {
202
+ context.report({ node, messageId: 'unusedParam', data: { name: names[0] } });
203
+ return;
204
+ }
205
+
206
+ if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names))) {
207
+ context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
208
+ }
209
+ };
210
+
211
+ return {
212
+ CatchClause(node) {
213
+ reportHandler(node, node.param ? [node.param] : [], node.body);
214
+ },
215
+ "CallExpression[callee.type='MemberExpression'][callee.property.name='catch']"(node) {
216
+ const [handler] = node.arguments;
217
+ if (!handler) return;
218
+ if (isDirectPromiseCatchHandler(handler)) return;
219
+
220
+ if (handler.type !== 'ArrowFunctionExpression' && handler.type !== 'FunctionExpression') {
221
+ context.report({ node: handler, messageId: 'missingParam' });
222
+ return;
223
+ }
224
+
225
+ reportHandler(handler, handler.params, handler.body);
226
+ },
227
+ };
228
+ },
229
+ });
230
+
17
231
  const createProteumEslintConfig = ({ ignores = [] } = {}) => [
18
232
  {
19
233
  ignores: [...defaultIgnores, ...ignores],
@@ -37,12 +251,18 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
37
251
  },
38
252
  plugins: {
39
253
  '@typescript-eslint': tseslint.plugin,
254
+ proteum: {
255
+ rules: {
256
+ 'no-swallowed-caught-error': createSwallowedErrorRule(),
257
+ },
258
+ },
40
259
  react: reactPlugin,
41
260
  'react-hooks': reactHooksPlugin,
42
261
  'jsx-a11y': jsxA11yPlugin,
43
262
  },
44
263
  rules: {
45
264
  '@typescript-eslint/no-explicit-any': 'error',
265
+ 'proteum/no-swallowed-caught-error': 'error',
46
266
  'no-restricted-syntax': [
47
267
  'error',
48
268
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.2.7",
4
+ "version": "2.2.9",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -3,26 +3,25 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  import { Command as ClipanionCommand, Option, UsageError } from 'clipanion';
6
- import type CurrentCommandApplication from '@/server/index';
7
6
 
8
7
  /*----------------------------------
9
8
  - TYPES
10
9
  ----------------------------------*/
11
10
 
12
11
  export type TCommandApplication = {
13
- env: {
12
+ env?: {
14
13
  profile?: string;
15
14
  name?: string;
16
15
  [key: string]: unknown;
17
16
  };
18
- identity: {
17
+ identity?: {
19
18
  identifier?: string;
20
19
  [key: string]: unknown;
21
20
  };
22
- getRootServices: () => Record<string, unknown>;
21
+ getRootServices?: () => Record<string, unknown>;
23
22
  findService?: (serviceId: string) => unknown;
24
- models?: { client?: any };
25
- Models?: { client?: any };
23
+ models?: { client?: object };
24
+ Models?: { client?: object };
26
25
  };
27
26
 
28
27
  export type TCommandService = {
@@ -33,23 +32,19 @@ export type TCommandService = {
33
32
  - COMMAND CLASSES
34
33
  ----------------------------------*/
35
34
 
36
- export abstract class Commands<TApplication extends TCommandApplication = CurrentCommandApplication> {
37
- public app: CurrentCommandApplication;
35
+ export abstract class Commands<TApplication extends TCommandApplication = TCommandApplication> {
36
+ public app: TApplication;
38
37
 
39
- public constructor(app: CurrentCommandApplication) {
38
+ public constructor(app: TApplication) {
40
39
  this.app = app;
41
40
  }
42
41
 
43
- public get services(): CurrentCommandApplication {
42
+ public get services(): TApplication {
44
43
  return this.app;
45
44
  }
46
45
 
47
- public get models(): any {
48
- const app = this.app as {
49
- models?: { client?: any };
50
- Models?: { client?: any };
51
- };
52
- const models = app.models?.client ?? app.Models?.client;
46
+ public get models(): object {
47
+ const models = this.app.models?.client ?? this.app.Models?.client;
53
48
 
54
49
  if (!models)
55
50
  throw new Error(`${this.constructor.name} tried to access models but no Models service is registered.`);
@@ -197,13 +197,17 @@ const parseCommandOptionTokens = (tokens: string[]) => {
197
197
  - SERVICE
198
198
  ----------------------------------*/
199
199
 
200
- export default class CommandsManager extends Service<Config, Hooks, Application> {
200
+ export default class CommandsManager extends Service<Config, Hooks, Application, object> {
201
201
  public priority = 2 as 2;
202
202
 
203
203
  public commandsIndex: CommandsList = {};
204
204
 
205
205
  private runtimeCli?: Cli;
206
206
 
207
+ public constructor(parent: object | 'self', config: Config | null | undefined, app: Application | 'self') {
208
+ super(parent, config, app);
209
+ }
210
+
207
211
  public command<TArgs extends any[]>(
208
212
  ...args:
209
213
  | [name: string, description: string, childrens: RuntimeCommand[]]
@@ -7,39 +7,88 @@ import zod from 'zod';
7
7
 
8
8
  // Core
9
9
  import context from '@server/context';
10
- import type { Application } from '../index';
11
- import type { TServiceModelsClient } from '../service';
12
- import type { TRouterContext, TAnyRouter } from '@server/services/router';
13
10
  import {
14
11
  toValidationSchema,
15
12
  type TValidationSchema,
16
13
  type TValidationShape,
17
14
  } from '@server/services/router/request/validation/zod';
15
+ import type {
16
+ Request as ServerRequest,
17
+ Response as ServerResponse,
18
+ TAnyRouter,
19
+ TRouterContextServices,
20
+ } from '@server/services/router';
18
21
 
19
22
  export { schema } from '@server/services/router/request/validation/zod';
20
- export type { z } from '@server/services/router/request/validation/zod';
23
+ export type {
24
+ z,
25
+ TInferValidationSchema,
26
+ TTypedValidationSchema,
27
+ TValidationSchema,
28
+ TValidationShape,
29
+ } from '@server/services/router/request/validation/zod';
21
30
 
22
31
  /*----------------------------------
23
32
  - TYPES
24
33
  ----------------------------------*/
25
34
 
26
- type TControllerRouter<TApplication extends Application = Application> = TApplication extends { Router: infer TRouter }
27
- ? TRouter extends TAnyRouter
28
- ? TRouter
29
- : TAnyRouter
35
+ type TControllerModelsClient<TApplication extends object = object> = TApplication extends {
36
+ Models: { client: infer TModels };
37
+ }
38
+ ? TModels
39
+ : TApplication extends {
40
+ models: { client: infer TModels };
41
+ }
42
+ ? TModels
43
+ : object;
44
+
45
+ type TControllerRouter<TRouter> = TRouter extends TAnyRouter ? TRouter : TAnyRouter;
46
+ type TControllerApplicationRouter<TApplication extends object> = TApplication extends { Router: infer TRouter }
47
+ ? TControllerRouter<TRouter>
30
48
  : TAnyRouter;
31
49
 
32
- export type TControllerRequestContext<TApplication extends Application = Application> = TRouterContext<
33
- TControllerRouter<TApplication>
34
- >;
50
+ export type TControllerRequestContext<
51
+ TApplication extends object = object,
52
+ TRouter extends object = object,
53
+ TRequestServices extends object = {},
54
+ > = {
55
+ app: TApplication;
56
+ context: object;
57
+ request: ServerRequest<TControllerRouter<TRouter>>;
58
+ api: ServerRequest<TControllerRouter<TRouter>>['api'];
59
+ response: ServerResponse<TControllerRouter<TRouter>>;
60
+ route: object;
61
+ page?: object;
62
+ Router: TControllerRouter<TRouter>;
63
+ } & (TRouter extends TAnyRouter ? TRouterContextServices<TControllerRouter<TRouter>> : {}) &
64
+ TRequestServices;
65
+
66
+ type TControllerBaseContext<TApplication extends object> = {
67
+ app: TApplication;
68
+ request: { data: TObjetDonnees };
69
+ };
70
+
71
+ type TControllerDefaultContext<TApplication extends object, TRequestServices extends object> = {
72
+ app: TApplication;
73
+ context: object;
74
+ request: ServerRequest<TControllerApplicationRouter<TApplication>>;
75
+ api: ServerRequest<TControllerApplicationRouter<TApplication>>['api'];
76
+ response: ServerResponse<TControllerApplicationRouter<TApplication>>;
77
+ route: object;
78
+ page?: object;
79
+ Router: TControllerApplicationRouter<TApplication>;
80
+ } & TRouterContextServices<TControllerApplicationRouter<TApplication>> &
81
+ TRequestServices;
35
82
 
36
83
  /*----------------------------------
37
84
  - CLASS
38
85
  ----------------------------------*/
39
86
 
40
87
  export default abstract class Controller<
41
- TApplication extends Application = Application,
42
- TContext extends TControllerRequestContext<TApplication> = TControllerRequestContext<TApplication>,
88
+ TApplication extends object = object,
89
+ TRouter extends object = object,
90
+ TRequestServices extends object = {},
91
+ TContext extends TControllerBaseContext<TApplication> = TControllerDefaultContext<TApplication, TRequestServices>,
43
92
  > {
44
93
  public constructor(public request: TContext) {}
45
94
 
@@ -51,9 +100,12 @@ export default abstract class Controller<
51
100
  return this.app;
52
101
  }
53
102
 
54
- public get models(): TServiceModelsClient<TApplication> {
55
- const app = this.app as { models?: { client?: unknown }; Models?: { client?: unknown } };
56
- return (app.models?.client ?? app.Models?.client) as TServiceModelsClient<TApplication>;
103
+ public get models(): TControllerModelsClient<TApplication> {
104
+ const app = this.app as {
105
+ models?: { client?: TControllerModelsClient<TApplication> };
106
+ Models?: { client?: TControllerModelsClient<TApplication> };
107
+ };
108
+ return (app.models?.client ?? app.Models?.client) as TControllerModelsClient<TApplication>;
57
109
  }
58
110
 
59
111
  public input<TSchema extends TValidationSchema>(schema: TSchema): zod.output<TSchema>;
@@ -112,10 +112,10 @@ const loadGeneratedCommandDefinitions = () =>
112
112
  (a, b) => a.path.localeCompare(b.path),
113
113
  );
114
114
 
115
- export default class DevCommandsRegistry<TApplication extends Application = Application> {
115
+ export default class DevCommandsRegistry {
116
116
  private definitions = loadGeneratedCommandDefinitions();
117
117
 
118
- public constructor(private app: TApplication) {}
118
+ public constructor(private app: Application) {}
119
119
 
120
120
  public list() {
121
121
  return this.definitions.map((definition) => ({
@@ -150,7 +150,7 @@ export default class DevCommandsRegistry<TApplication extends Application = Appl
150
150
 
151
151
  try {
152
152
  const instance = new definition.Command(this.app);
153
- const method = (instance as Record<string, unknown>)[definition.methodName];
153
+ const method = Reflect.get(instance, definition.methodName);
154
154
 
155
155
  if (typeof method !== 'function') {
156
156
  throw new Error(
@@ -36,8 +36,8 @@ const isExplainSectionName = (value: string): value is TExplainSectionName =>
36
36
  const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
37
37
  ['silly', 'log', 'info', 'warn', 'error'].includes(value);
38
38
 
39
- export default class DevDiagnosticsRegistry<TApplication extends Application = Application> {
40
- public constructor(private app: TApplication) {}
39
+ export default class DevDiagnosticsRegistry {
40
+ public constructor(private app: Application) {}
41
41
 
42
42
  private getManifestFilepath() {
43
43
  return path.join(this.app.container.path.root, '.proteum', 'manifest.json');
@@ -44,8 +44,13 @@ export const Service = ServicesContainer;
44
44
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
45
45
 
46
46
  export type ApplicationProperties = Prettify<keyof Application>;
47
- export type RootServicesOf<TApplication extends Application = Application> = Prettify<{
48
- [TKey in Exclude<keyof TApplication, ApplicationProperties> as TApplication[TKey] extends AnyService ? TKey : never]: TApplication[TKey];
47
+ type RootServiceCandidate = {
48
+ runHook: object;
49
+ getServiceInstance: object;
50
+ status: string | undefined;
51
+ };
52
+ export type RootServicesOf<TApplication extends object = object> = Prettify<{
53
+ [TKey in Exclude<keyof TApplication, ApplicationProperties> as TApplication[TKey] extends RootServiceCandidate ? TKey : never]: TApplication[TKey];
49
54
  }>;
50
55
 
51
56
  const isServiceInstance = (value: unknown): value is AnyService => {
@@ -56,19 +61,24 @@ const isServiceInstance = (value: unknown): value is AnyService => {
56
61
  return typeof service.runHook === 'function' && typeof service.getServiceInstance === 'function' && service.status !== undefined;
57
62
  };
58
63
 
64
+ const createCommandsManager = (app: Application) => new CommandsManager(app, { debug: true }, app);
65
+ const createDevCommandsRegistry = (app: Application) => new DevCommandsRegistry(app);
66
+ const createDevDiagnosticsRegistry = (app: Application) => new DevDiagnosticsRegistry(app);
67
+
59
68
  /*----------------------------------
60
69
  - FUNCTIONS
61
70
  ----------------------------------*/
62
71
  export abstract class Application<
63
72
  TServicesContainer extends ServicesContainerClass = ServicesContainerClass,
64
73
  TUser extends TBasicUser = TBasicUser,
65
- > extends ApplicationService<Config, Hooks, Application, Application> {
74
+ > extends ApplicationService<Config, Hooks, object, object> {
66
75
  public static identity = ConfigApplication.identity;
67
76
  public static setup = ConfigApplication.setup;
68
77
 
69
78
  public app!: this;
70
79
  public servicesContainer!: TServicesContainer;
71
80
  public userType!: TUser;
81
+ public declare Router: object;
72
82
 
73
83
  /*----------------------------------
74
84
  - PROPERTIES
@@ -122,21 +132,22 @@ export abstract class Application<
122
132
  - COMMANDS
123
133
  ----------------------------------*/
124
134
 
125
- private commandsManager = new CommandsManager(this, { debug: true }, this);
126
- private devCommandsRegistry?: DevCommandsRegistry<this>;
127
- private devDiagnosticsRegistry?: DevDiagnosticsRegistry<this>;
135
+ private commandsManager?: CommandsManager;
136
+ private devCommandsRegistry?: DevCommandsRegistry;
137
+ private devDiagnosticsRegistry?: DevDiagnosticsRegistry;
128
138
 
129
139
  public command(...args: Parameters<CommandsManager['command']>) {
140
+ this.commandsManager ??= createCommandsManager(this as Application);
130
141
  return this.commandsManager.command(...args);
131
142
  }
132
143
 
133
144
  public getDevCommands() {
134
- this.devCommandsRegistry ??= new DevCommandsRegistry(this);
145
+ this.devCommandsRegistry ??= createDevCommandsRegistry(this as Application);
135
146
  return this.devCommandsRegistry;
136
147
  }
137
148
 
138
149
  public getDevDiagnostics() {
139
- this.devDiagnosticsRegistry ??= new DevDiagnosticsRegistry(this);
150
+ this.devDiagnosticsRegistry ??= createDevDiagnosticsRegistry(this as Application);
140
151
  return this.devDiagnosticsRegistry;
141
152
  }
142
153