@di-framework/di-framework-http 0.0.0-prerelease.308 → 0.0.0-prerelease.310

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.
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from "./src/typed-router.ts";
2
- export * from "./src/decorators.ts";
3
- export * from "./src/openapi.ts";
4
- export * from "./src/registry.ts";
5
- export { default as registry } from "./src/registry.ts";
1
+ export * from './src/typed-router.ts';
2
+ export * from './src/decorators.ts';
3
+ export * from './src/openapi.ts';
4
+ export * from './src/registry.ts';
5
+ export { default as registry } from './src/registry.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@di-framework/di-framework-http",
3
- "version": "0.0.0-prerelease.308",
3
+ "version": "0.0.0-prerelease.310",
4
4
  "description": "Extends di-framework with HTTP features",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "typescript": "^5"
41
41
  },
42
42
  "peerDependencies": {
43
- "@di-framework/di-framework": "^0.0.0-prerelease.308",
43
+ "@di-framework/di-framework": "^0.0.0-prerelease.310",
44
44
  "typescript": "^5"
45
45
  }
46
46
  }
package/src/cli.test.ts CHANGED
@@ -1,85 +1,76 @@
1
- import { describe, it, expect, afterEach } from "bun:test";
2
- import { spawnSync } from "child_process";
3
- import fs from "fs";
4
- import path from "path";
1
+ import { describe, it, expect, afterEach } from 'bun:test';
2
+ import { spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
5
 
6
- const CLI_PATH = path.resolve(import.meta.dir, "cli.ts");
7
- const TEST_OUTPUT = "test.openapi.json";
6
+ const CLI_PATH = path.resolve(import.meta.dir, 'cli.ts');
7
+ const TEST_OUTPUT = 'test.openapi.json';
8
8
 
9
- describe("CLI", () => {
9
+ describe('CLI', () => {
10
10
  afterEach(() => {
11
11
  if (fs.existsSync(TEST_OUTPUT)) {
12
12
  fs.unlinkSync(TEST_OUTPUT);
13
13
  }
14
14
  });
15
15
 
16
- it("should show help when no command is provided", () => {
17
- const { stdout } = spawnSync("bun", [CLI_PATH]);
18
- expect(stdout.toString()).toContain("Usage: di-framework-http generate");
16
+ it('should show help when no command is provided', () => {
17
+ const { stdout } = spawnSync('bun', [CLI_PATH]);
18
+ expect(stdout.toString()).toContain('Usage: di-framework-http generate');
19
19
  });
20
20
 
21
- it("should error when --controllers is missing", () => {
22
- const { stderr, status } = spawnSync("bun", [CLI_PATH, "generate"]);
21
+ it('should error when --controllers is missing', () => {
22
+ const { stderr, status } = spawnSync('bun', [CLI_PATH, 'generate']);
23
23
  expect(status).toBe(1);
24
- expect(stderr.toString()).toContain(
25
- "Error: --controllers path is required",
26
- );
24
+ expect(stderr.toString()).toContain('Error: --controllers path is required');
27
25
  });
28
26
 
29
- it("should generate OpenAPI spec with --controllers and --output", () => {
27
+ it('should generate OpenAPI spec with --controllers and --output', () => {
30
28
  const controllersPath = path.resolve(
31
29
  import.meta.dir,
32
- "../../examples/packages/http-router/index.ts",
30
+ '../../examples/packages/http-router/index.ts',
33
31
  );
34
- const { stdout, status } = spawnSync("bun", [
32
+ const { stdout, status } = spawnSync('bun', [
35
33
  CLI_PATH,
36
- "generate",
37
- "--controllers",
34
+ 'generate',
35
+ '--controllers',
38
36
  controllersPath,
39
- "--output",
37
+ '--output',
40
38
  TEST_OUTPUT,
41
39
  ]);
42
40
 
43
41
  expect(status).toBe(0);
44
- expect(stdout.toString()).toContain(
45
- "Successfully generated OpenAPI spec at test.openapi.json",
46
- );
42
+ expect(stdout.toString()).toContain('Successfully generated OpenAPI spec at test.openapi.json');
47
43
  expect(fs.existsSync(TEST_OUTPUT)).toBe(true);
48
44
 
49
- const spec = JSON.parse(fs.readFileSync(TEST_OUTPUT, "utf8"));
50
- expect(spec.openapi).toBe("3.1.0");
51
- expect(spec.paths["/echo"]).toBeDefined();
52
- expect(spec.paths["/echo"].post).toBeDefined();
45
+ const spec = JSON.parse(fs.readFileSync(TEST_OUTPUT, 'utf8'));
46
+ expect(spec.openapi).toBe('3.1.0');
47
+ expect(spec.paths['/echo']).toBeDefined();
48
+ expect(spec.paths['/echo'].post).toBeDefined();
53
49
  });
54
50
 
55
- it("should use default output path (openapi.json) if --output is missing", () => {
56
- const DEFAULT_OUTPUT = "openapi.json";
51
+ it('should use default output path (openapi.json) if --output is missing', () => {
52
+ const DEFAULT_OUTPUT = 'openapi.json';
57
53
  if (fs.existsSync(DEFAULT_OUTPUT)) fs.unlinkSync(DEFAULT_OUTPUT);
58
54
 
59
55
  const controllersPath = path.resolve(
60
56
  import.meta.dir,
61
- "../../examples/packages/http-router/index.ts",
57
+ '../../examples/packages/http-router/index.ts',
62
58
  );
63
- const { status } = spawnSync("bun", [
64
- CLI_PATH,
65
- "generate",
66
- "--controllers",
67
- controllersPath,
68
- ]);
59
+ const { status } = spawnSync('bun', [CLI_PATH, 'generate', '--controllers', controllersPath]);
69
60
 
70
61
  expect(status).toBe(0);
71
62
  expect(fs.existsSync(DEFAULT_OUTPUT)).toBe(true);
72
63
  fs.unlinkSync(DEFAULT_OUTPUT);
73
64
  });
74
65
 
75
- it("should error with invalid controllers path", () => {
76
- const { stderr, status } = spawnSync("bun", [
66
+ it('should error with invalid controllers path', () => {
67
+ const { stderr, status } = spawnSync('bun', [
77
68
  CLI_PATH,
78
- "generate",
79
- "--controllers",
80
- "./non-existent-file.ts",
69
+ 'generate',
70
+ '--controllers',
71
+ './non-existent-file.ts',
81
72
  ]);
82
73
  expect(status).toBe(1);
83
- expect(stderr.toString()).toContain("Error generating OpenAPI spec");
74
+ expect(stderr.toString()).toContain('Error generating OpenAPI spec');
84
75
  });
85
76
  });
package/src/cli.ts CHANGED
@@ -1,34 +1,31 @@
1
1
  #!/usr/bin/env bun
2
- import fs from "fs";
3
- import path from "path";
4
- import { generateOpenAPI } from "./openapi.ts";
5
- import registry from "./registry.ts";
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { generateOpenAPI } from './openapi.ts';
5
+ import registry from './registry.ts';
6
6
 
7
7
  const args = process.argv.slice(2);
8
8
  const command = args[0];
9
9
 
10
- if (command === "generate") {
11
- const outputArg = args.indexOf("--output");
12
- const outputPath = outputArg !== -1 ? args[outputArg + 1] : "openapi.json";
10
+ if (command === 'generate') {
11
+ const outputArg = args.indexOf('--output');
12
+ const outputPath = outputArg !== -1 ? args[outputArg + 1] : 'openapi.json';
13
13
 
14
- const controllersArg = args.indexOf("--controllers");
14
+ const controllersArg = args.indexOf('--controllers');
15
15
  if (controllersArg === -1) {
16
- console.error("Error: --controllers path is required");
16
+ console.error('Error: --controllers path is required');
17
17
  process.exit(1);
18
18
  }
19
19
 
20
20
  async function run() {
21
21
  try {
22
22
  // Import the user's controllers to trigger decorator registration
23
- const controllersPathResolved = path.resolve(
24
- process.cwd(),
25
- args[controllersArg + 1]!,
26
- );
23
+ const controllersPathResolved = path.resolve(process.cwd(), args[controllersArg + 1]!);
27
24
  await import(controllersPathResolved);
28
25
 
29
26
  const spec = generateOpenAPI(
30
27
  {
31
- title: "API Documentation",
28
+ title: 'API Documentation',
32
29
  },
33
30
  registry,
34
31
  );
@@ -1,15 +1,17 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { Controller, Endpoint } from "./decorators.ts";
3
- import registry from "./registry.ts";
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { Controller, Endpoint, SCHEMAS } from './decorators.ts';
3
+ import registry from './registry.ts';
4
+ import { useContainer } from '@di-framework/di-framework/container';
5
+ import { Component } from '@di-framework/di-framework/decorators';
4
6
 
5
- describe("Decorators", () => {
6
- it("should register a controller and its endpoints", () => {
7
+ describe('Decorators', () => {
8
+ it('should register a controller and its endpoints', () => {
7
9
  const initialSize = registry.getTargets().size;
8
10
 
9
11
  @Controller()
10
12
  class TestController {
11
- @Endpoint({ summary: "Test endpoint" })
12
- static test = { isEndpoint: true, path: "/test", method: "get" };
13
+ @Endpoint({ summary: 'Test endpoint' })
14
+ static test = { isEndpoint: true, path: '/test', method: 'get' };
13
15
  }
14
16
 
15
17
  expect(registry.getTargets().has(TestController)).toBe(true);
@@ -18,25 +20,25 @@ describe("Decorators", () => {
18
20
  // @ts-ignore
19
21
  expect(TestController.test.isEndpoint).toBe(true);
20
22
  // @ts-ignore
21
- expect(TestController.test.metadata.summary).toBe("Test endpoint");
23
+ expect(TestController.test.metadata.summary).toBe('Test endpoint');
22
24
 
23
25
  expect(registry.getTargets().size).toBeGreaterThan(initialSize);
24
26
  });
25
27
 
26
- it("should work as a class decorator for Endpoint", () => {
27
- @Endpoint({ summary: "Class level" })
28
+ it('should work as a class decorator for Endpoint', () => {
29
+ @Endpoint({ summary: 'Class level' })
28
30
  class ClassEndpoint {}
29
31
 
30
32
  expect(registry.getTargets().has(ClassEndpoint)).toBe(true);
31
33
  // @ts-ignore
32
34
  expect(ClassEndpoint.isEndpoint).toBe(true);
33
35
  // @ts-ignore
34
- expect(ClassEndpoint.metadata.summary).toBe("Class level");
36
+ expect(ClassEndpoint.metadata.summary).toBe('Class level');
35
37
  });
36
38
 
37
- it("should work on instance methods (via prototype)", () => {
39
+ it('should work on instance methods (via prototype)', () => {
38
40
  class InstanceController {
39
- @Endpoint({ summary: "Instance method" })
41
+ @Endpoint({ summary: 'Instance method' })
40
42
  method() {}
41
43
  }
42
44
 
@@ -44,4 +46,51 @@ describe("Decorators", () => {
44
46
  // @ts-ignore
45
47
  expect(InstanceController.prototype.method.isEndpoint).toBe(true);
46
48
  });
49
+
50
+ it('should inject static properties on Controller classes', () => {
51
+ class InjectedService {
52
+ value = 42;
53
+ }
54
+ useContainer().register(InjectedService);
55
+
56
+ @Controller()
57
+ class InjectedController {
58
+ @Component(InjectedService)
59
+ static service: InjectedService;
60
+ }
61
+
62
+ expect(InjectedController.service).toBeDefined();
63
+ expect(InjectedController.service.value).toBe(42);
64
+ });
65
+
66
+ it('should extract schemas from Endpoint metadata and store them via SCHEMAS symbol', () => {
67
+ class SchemaEndpoint {
68
+ @Endpoint({
69
+ responses: {
70
+ '200': {
71
+ description: 'A successful response',
72
+ content: {
73
+ 'application/json': {
74
+ schema: { $ref: '#/components/schemas/MyModel' },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ requestBody: {
80
+ content: {
81
+ 'application/json': {
82
+ schema: { $ref: '#/components/schemas/MyRequest' },
83
+ },
84
+ },
85
+ },
86
+ })
87
+ static myEndpoint() {}
88
+ }
89
+
90
+ const schemas = (SchemaEndpoint as any)[SCHEMAS];
91
+ expect(schemas).toBeDefined();
92
+ expect(schemas.has('MyModel')).toBe(true);
93
+ expect(schemas.has('MyRequest')).toBe(true);
94
+ expect(schemas.size).toBe(2);
95
+ });
47
96
  });
package/src/decorators.ts CHANGED
@@ -1,24 +1,67 @@
1
- import registry from "./registry.ts";
2
- import { Container as ContainerDecorator } from "@di-framework/di-framework/decorators";
1
+ import registry from './registry.ts';
2
+ import { Container as ContainerDecorator } from '@di-framework/di-framework/decorators';
3
+ import { getOwnMetadata, useContainer } from '@di-framework/di-framework/container';
3
4
 
4
- export function Controller(
5
- options: { singleton?: boolean; container?: any } = {},
6
- ) {
5
+ const INJECT_METADATA_KEY = 'di:inject';
6
+ export const SCHEMAS = Symbol.for('proseva:component-schemas');
7
+
8
+ type UnknownRecord = Record<string, unknown>;
9
+
10
+ function isRecord(value: unknown): value is UnknownRecord {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+
14
+ interface ContainerLike {
15
+ resolve(token: unknown): unknown;
16
+ }
17
+
18
+ export function Controller(options: { singleton?: boolean; container?: any } = {}) {
7
19
  // Compose DI registration with OpenAPI registry marking
8
- const container = ContainerDecorator(options);
20
+ const containerDecorator = ContainerDecorator(options);
9
21
  return function (target: any) {
10
22
  // Mark for HTTP/OpenAPI purposes
11
23
  target.isController = true;
12
24
  registry.addTarget(target);
13
25
 
14
26
  // Also register with the DI container (same instance as core framework)
15
- container(target);
27
+ containerDecorator(target);
28
+
29
+ const container: ContainerLike = (options.container ?? useContainer()) as ContainerLike;
30
+
31
+ const rawMetadata: unknown = getOwnMetadata(INJECT_METADATA_KEY, target) as unknown;
32
+ const injectMetadata: UnknownRecord = isRecord(rawMetadata) ? rawMetadata : {};
33
+
34
+ for (const [propName, targetType] of Object.entries(injectMetadata)) {
35
+ if (!propName.startsWith('param_') && targetType) {
36
+ (target as UnknownRecord)[propName] = container.resolve(targetType);
37
+ }
38
+ }
16
39
  };
17
40
  }
18
41
 
42
+ /** Extract all `#/components/schemas/<Name>` references from a metadata tree. */
43
+ function extractSchemaRefs(obj: unknown, out: Set<string>): void {
44
+ if (typeof obj !== 'object' || obj === null) return;
45
+
46
+ if (Array.isArray(obj)) {
47
+ for (const item of obj) extractSchemaRefs(item, out);
48
+ return;
49
+ }
50
+
51
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
52
+ if (key === '$ref' && typeof value === 'string') {
53
+ const match = /^#\/components\/schemas\/(.+)$/.exec(value);
54
+ if (match?.[1]) out.add(match[1]);
55
+ } else {
56
+ extractSchemaRefs(value, out);
57
+ }
58
+ }
59
+ }
60
+
19
61
  export function Endpoint(metadata?: {
20
62
  summary?: string;
21
63
  description?: string;
64
+ parameters?: unknown[];
22
65
  requestBody?: any;
23
66
  responses?: Record<string, any>;
24
67
  }) {
@@ -28,22 +71,35 @@ export function Endpoint(metadata?: {
28
71
 
29
72
  // For static methods on a class, target is the constructor.
30
73
  // If it's a static method, target itself is the constructor.
31
- const constructor =
32
- typeof target === "function" ? target : target.constructor;
74
+ const constructor = typeof target === 'function' ? target : target.constructor;
33
75
  registry.addTarget(constructor);
34
76
 
35
77
  // We'll let the generator discover the details from the property for now,
36
78
  // or we could register it explicitly here if we had path/method info.
37
79
  // Since TypedRouter adds path/method to the handler, we keep it as is
38
80
  // but we can ensure the metadata is attached.
39
- property.isEndpoint = true;
81
+ if (property) {
82
+ property.isEndpoint = true;
83
+ if (metadata) {
84
+ property.metadata = metadata;
85
+ }
86
+ }
87
+
88
+ // Also attach schemas if metadata is provided
40
89
  if (metadata) {
41
- property.metadata = metadata;
90
+ const existing: Set<string> =
91
+ (constructor as Record<symbol, Set<string>>)[SCHEMAS] ?? new Set<string>();
92
+ extractSchemaRefs(metadata, existing);
93
+ (constructor as Record<symbol, Set<string>>)[SCHEMAS] = existing;
42
94
  }
43
95
  } else {
44
96
  target.isEndpoint = true;
45
97
  if (metadata) {
46
98
  target.metadata = metadata;
99
+ const existing: Set<string> =
100
+ (target as Record<symbol, Set<string>>)[SCHEMAS] ?? new Set<string>();
101
+ extractSchemaRefs(metadata, existing);
102
+ (target as Record<symbol, Set<string>>)[SCHEMAS] = existing;
47
103
  }
48
104
  registry.addTarget(target);
49
105
  }