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

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/README.md CHANGED
@@ -36,9 +36,9 @@ import {
36
36
  type Json,
37
37
  Controller,
38
38
  Endpoint,
39
- } from "@di-framework/di-framework-http";
40
- import { Component, Container } from "@di-framework/di-framework/decorators";
41
- import { useContainer } from "@di-framework/di-framework/container";
39
+ } from '@di-framework/di-framework-http';
40
+ import { Component, Container } from '@di-framework/di-framework/decorators';
41
+ import { useContainer } from '@di-framework/di-framework/container';
42
42
 
43
43
  const router = TypedRouter();
44
44
 
@@ -66,29 +66,28 @@ export class EchoController {
66
66
  }
67
67
 
68
68
  @Endpoint({
69
- summary: "Echo a message",
70
- description: "Returns the provided message with a server timestamp.",
69
+ summary: 'Echo a message',
70
+ description: 'Returns the provided message with a server timestamp.',
71
71
  responses: {
72
- "200": { description: "Successful echo" },
72
+ '200': { description: 'Successful echo' },
73
73
  },
74
74
  })
75
- static post = router.post<
76
- RequestSpec<Json<EchoPayload>>,
77
- ResponseSpec<EchoResponse>
78
- >("/echo", (req) => {
79
- // Demonstrate auto DI registration: resolve the controller instance from
80
- // the global container without any manual registration.
81
- const controller = useContainer().resolve(EchoController);
82
- return json(controller.echoMessage(req.content.message));
83
- });
75
+ static post = router.post<RequestSpec<Json<EchoPayload>>, ResponseSpec<EchoResponse>>(
76
+ '/echo',
77
+ (req) => {
78
+ // Demonstrate auto DI registration: resolve the controller instance from
79
+ // the global container without any manual registration.
80
+ const controller = useContainer().resolve(EchoController);
81
+ return json(controller.echoMessage(req.content.message));
82
+ },
83
+ );
84
84
  }
85
85
 
86
86
  // Add a simple GET route
87
- router.get("/", () => json({ message: "API is healthy" }));
87
+ router.get('/', () => json({ message: 'API is healthy' }));
88
88
 
89
89
  export default {
90
- fetch: (request: Request, env: any, ctx: any) =>
91
- router.fetch(request, env, ctx),
90
+ fetch: (request: Request, env: any, ctx: any) => router.fetch(request, env, ctx),
92
91
  };
93
92
  ```
94
93
 
@@ -103,7 +102,7 @@ import {
103
102
  type RequestSpec,
104
103
  type ResponseSpec,
105
104
  type Multipart,
106
- } from "@di-framework/di-framework-http";
105
+ } from '@di-framework/di-framework-http';
107
106
 
108
107
  const router = TypedRouter();
109
108
 
@@ -111,10 +110,10 @@ type UploadPayload = { files: File[] };
111
110
  type UploadResult = { filenames: string[] };
112
111
 
113
112
  router.post<RequestSpec<Multipart<UploadPayload>>, ResponseSpec<UploadResult>>(
114
- "/upload",
113
+ '/upload',
115
114
  (req) => {
116
115
  // req.content is typed as FormData
117
- const files = req.content.getAll("files") as File[];
116
+ const files = req.content.getAll('files') as File[];
118
117
  return json({ filenames: files.map((f) => f.name) });
119
118
  },
120
119
  { multipart: true },
@@ -144,13 +143,13 @@ bun x di-framework-http generate --controllers ./src/index.ts
144
143
  You can also generate the spec programmatically using the `generateOpenAPI` function and the default `registry`:
145
144
 
146
145
  ```typescript
147
- import registry, { generateOpenAPI } from "@di-framework/di-framework-http";
148
- import "./controllers/MyController"; // Import to trigger registration
146
+ import registry, { generateOpenAPI } from '@di-framework/di-framework-http';
147
+ import './controllers/MyController'; // Import to trigger registration
149
148
 
150
149
  const spec = generateOpenAPI(
151
150
  {
152
- title: "My API",
153
- version: "1.0.0",
151
+ title: 'My API',
152
+ version: '1.0.0',
154
153
  },
155
154
  registry,
156
155
  );
@@ -161,7 +160,7 @@ console.log(JSON.stringify(spec, null, 2));
161
160
  If you need full control, you can iterate the `registry` manually:
162
161
 
163
162
  ```typescript
164
- import registry from "@di-framework/di-framework-http";
163
+ import registry from '@di-framework/di-framework-http';
165
164
 
166
165
  for (const target of registry.getTargets()) {
167
166
  // target is the decorated class
package/dist/index.d.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/dist/index.js CHANGED
@@ -1,9 +1,5 @@
1
1
  // src/typed-router.ts
2
- import {
3
- Router,
4
- withContent,
5
- json as ittyJson
6
- } from "itty-router";
2
+ import { Router, withContent, json as ittyJson } from "itty-router";
7
3
  function json(data, init) {
8
4
  return ittyJson(data, init);
9
5
  }
@@ -21,15 +17,7 @@ function TypedRouter(opts) {
21
17
  req.content = await req.formData();
22
18
  } catch {}
23
19
  }
24
- const methodsToProxy = [
25
- "get",
26
- "post",
27
- "put",
28
- "delete",
29
- "patch",
30
- "head",
31
- "options"
32
- ];
20
+ const methodsToProxy = ["get", "post", "put", "delete", "patch", "head", "options"];
33
21
  const wrapper = new Proxy(r, {
34
22
  get(target, prop, receiver) {
35
23
  if (typeof prop === "string" && methodsToProxy.includes(prop)) {
@@ -90,6 +78,7 @@ var TELEMETRY_METADATA_KEY = "di:telemetry";
90
78
  var TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
91
79
  var PUBLISHER_METADATA_KEY = "di:publisher";
92
80
  var SUBSCRIBER_METADATA_KEY = "di:subscriber";
81
+ var CRON_METADATA_KEY = "di:cron";
93
82
  var metadataStore = new Map;
94
83
  function defineMetadata(key, value, target) {
95
84
  if (!metadataStore.has(target)) {
@@ -103,11 +92,64 @@ function getMetadata(key, target) {
103
92
  function getOwnMetadata(key, target) {
104
93
  return getMetadata(key, target);
105
94
  }
95
+ function parseCronField(field, min, max) {
96
+ if (field === "*") {
97
+ const out = [];
98
+ for (let i = min;i <= max; i++)
99
+ out.push(i);
100
+ return out;
101
+ }
102
+ if (field.startsWith("*/")) {
103
+ const step = parseInt(field.slice(2), 10);
104
+ const out = [];
105
+ for (let i = min;i <= max; i++) {
106
+ if (i % step === 0)
107
+ out.push(i);
108
+ }
109
+ return out;
110
+ }
111
+ if (field.includes(",")) {
112
+ return field.split(",").map((s) => parseInt(s.trim(), 10));
113
+ }
114
+ if (field.includes("-")) {
115
+ const [lo = 0, hi = 0] = field.split("-").map((s) => parseInt(s.trim(), 10));
116
+ const out = [];
117
+ for (let i = lo;i <= hi; i++)
118
+ out.push(i);
119
+ return out;
120
+ }
121
+ return [parseInt(field, 10)];
122
+ }
123
+ function parseCronExpression(expr) {
124
+ const parts = expr.trim().split(/\s+/);
125
+ if (parts.length !== 5)
126
+ throw new Error(`Invalid cron expression "${expr}": expected 5 fields (minute hour dayOfMonth month dayOfWeek)`);
127
+ return {
128
+ minute: parseCronField(parts[0], 0, 59),
129
+ hour: parseCronField(parts[1], 0, 23),
130
+ dayOfMonth: parseCronField(parts[2], 1, 31),
131
+ month: parseCronField(parts[3], 1, 12),
132
+ dayOfWeek: parseCronField(parts[4], 0, 6)
133
+ };
134
+ }
135
+ function getNextCronTime(fields, from) {
136
+ const next = new Date(from);
137
+ next.setSeconds(0, 0);
138
+ next.setMinutes(next.getMinutes() + 1);
139
+ for (let i = 0;i < 1051920; i++) {
140
+ if (fields.minute.includes(next.getMinutes()) && fields.hour.includes(next.getHours()) && fields.dayOfMonth.includes(next.getDate()) && fields.month.includes(next.getMonth() + 1) && fields.dayOfWeek.includes(next.getDay())) {
141
+ return next;
142
+ }
143
+ next.setMinutes(next.getMinutes() + 1);
144
+ }
145
+ throw new Error(`No matching cron time found for expression within 2 years`);
146
+ }
106
147
 
107
148
  class Container {
108
149
  services = new Map;
109
150
  resolutionStack = new Set;
110
151
  listeners = new Map;
152
+ cronJobs = [];
111
153
  register(serviceClass, options = { singleton: true }) {
112
154
  const name = serviceClass.name;
113
155
  this.services.set(name, {
@@ -193,9 +235,16 @@ class Container {
193
235
  }
194
236
  clear() {
195
237
  const count = this.services.size;
238
+ this.stopCronJobs();
196
239
  this.services.clear();
197
240
  this.emit("cleared", { count });
198
241
  }
242
+ stopCronJobs() {
243
+ for (const job of this.cronJobs) {
244
+ job.stop();
245
+ }
246
+ this.cronJobs = [];
247
+ }
199
248
  getServiceNames() {
200
249
  const names = new Set;
201
250
  this.services.forEach((_, key) => {
@@ -307,6 +356,55 @@ class Container {
307
356
  }
308
357
  });
309
358
  }
359
+ applyCron(instance, constructor) {
360
+ const cronMethods = getMetadata(CRON_METADATA_KEY, constructor.prototype) || {};
361
+ Object.entries(cronMethods).forEach(([methodName, schedule]) => {
362
+ const method = instance[methodName];
363
+ if (typeof method !== "function")
364
+ return;
365
+ if (typeof schedule === "number") {
366
+ const timer = setInterval(() => {
367
+ try {
368
+ method.call(instance);
369
+ } catch (err) {
370
+ console.error(`[Cron] ${constructor.name}.${methodName} threw`, err);
371
+ }
372
+ }, schedule);
373
+ this.cronJobs.push({ stop: () => clearInterval(timer) });
374
+ } else {
375
+ const fields = parseCronExpression(schedule);
376
+ let stopped = false;
377
+ const scheduleNext = () => {
378
+ if (stopped)
379
+ return;
380
+ const now = new Date;
381
+ const next = getNextCronTime(fields, now);
382
+ const delay = next.getTime() - now.getTime();
383
+ const timer = setTimeout(() => {
384
+ if (stopped)
385
+ return;
386
+ try {
387
+ method.call(instance);
388
+ } catch (err) {
389
+ console.error(`[Cron] ${constructor.name}.${methodName} threw`, err);
390
+ }
391
+ scheduleNext();
392
+ }, delay);
393
+ job.stop = () => {
394
+ stopped = true;
395
+ clearTimeout(timer);
396
+ };
397
+ };
398
+ const job = {
399
+ stop: () => {
400
+ stopped = true;
401
+ }
402
+ };
403
+ this.cronJobs.push(job);
404
+ scheduleNext();
405
+ }
406
+ });
407
+ }
310
408
  instantiate(type, overrides = {}) {
311
409
  if (typeof type !== "function") {
312
410
  throw new Error("Service type must be a constructor or factory function");
@@ -342,6 +440,7 @@ class Container {
342
440
  const instance = new type(...dependencies);
343
441
  this.applyTelemetry(instance, type);
344
442
  this.applyEvents(instance, type);
443
+ this.applyCron(instance, type);
345
444
  const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
346
445
  const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) || {};
347
446
  const allInjectProperties = {
@@ -462,14 +561,45 @@ function Container2(options = {}) {
462
561
  }
463
562
 
464
563
  // src/decorators.ts
564
+ var INJECT_METADATA_KEY2 = "di:inject";
565
+ var SCHEMAS = Symbol.for("proseva:component-schemas");
566
+ function isRecord(value) {
567
+ return typeof value === "object" && value !== null && !Array.isArray(value);
568
+ }
465
569
  function Controller(options = {}) {
466
- const container2 = Container2(options);
570
+ const containerDecorator = Container2(options);
467
571
  return function(target) {
468
572
  target.isController = true;
469
573
  registry_default.addTarget(target);
470
- container2(target);
574
+ containerDecorator(target);
575
+ const container2 = options.container ?? useContainer();
576
+ const rawMetadata = getOwnMetadata(INJECT_METADATA_KEY2, target);
577
+ const injectMetadata = isRecord(rawMetadata) ? rawMetadata : {};
578
+ for (const [propName, targetType] of Object.entries(injectMetadata)) {
579
+ if (!propName.startsWith("param_") && targetType) {
580
+ target[propName] = container2.resolve(targetType);
581
+ }
582
+ }
471
583
  };
472
584
  }
585
+ function extractSchemaRefs(obj, out) {
586
+ if (typeof obj !== "object" || obj === null)
587
+ return;
588
+ if (Array.isArray(obj)) {
589
+ for (const item of obj)
590
+ extractSchemaRefs(item, out);
591
+ return;
592
+ }
593
+ for (const [key, value] of Object.entries(obj)) {
594
+ if (key === "$ref" && typeof value === "string") {
595
+ const match = /^#\/components\/schemas\/(.+)$/.exec(value);
596
+ if (match?.[1])
597
+ out.add(match[1]);
598
+ } else {
599
+ extractSchemaRefs(value, out);
600
+ }
601
+ }
602
+ }
473
603
  function Endpoint(metadata) {
474
604
  return function(target, propertyKey) {
475
605
  if (propertyKey) {
@@ -479,17 +609,67 @@ function Endpoint(metadata) {
479
609
  property.isEndpoint = true;
480
610
  if (metadata) {
481
611
  property.metadata = metadata;
612
+ const existing = constructor[SCHEMAS] ?? new Set;
613
+ extractSchemaRefs(metadata, existing);
614
+ constructor[SCHEMAS] = existing;
482
615
  }
483
616
  } else {
484
617
  target.isEndpoint = true;
485
618
  if (metadata) {
486
619
  target.metadata = metadata;
620
+ const existing = target[SCHEMAS] ?? new Set;
621
+ extractSchemaRefs(metadata, existing);
622
+ target[SCHEMAS] = existing;
487
623
  }
488
624
  registry_default.addTarget(target);
489
625
  }
490
626
  };
491
627
  }
492
628
  // src/openapi.ts
629
+ function collectRefs(obj, out) {
630
+ if (typeof obj !== "object" || obj === null)
631
+ return;
632
+ if (Array.isArray(obj)) {
633
+ for (const item of obj)
634
+ collectRefs(item, out);
635
+ return;
636
+ }
637
+ for (const [key, value] of Object.entries(obj)) {
638
+ if (key === "$ref" && typeof value === "string") {
639
+ const match = /^#\/components\/schemas\/(.+)$/.exec(value);
640
+ if (match?.[1])
641
+ out.add(match[1]);
642
+ } else {
643
+ collectRefs(value, out);
644
+ }
645
+ }
646
+ }
647
+ function resolveSchema(name, resolved, schemas) {
648
+ if (name in resolved)
649
+ return;
650
+ const schema = schemas[name];
651
+ if (!schema)
652
+ return;
653
+ resolved[name] = schema;
654
+ const transitive = new Set;
655
+ collectRefs(schema, transitive);
656
+ for (const dep of transitive) {
657
+ resolveSchema(dep, resolved, schemas);
658
+ }
659
+ }
660
+ function toOpenAPIPath(path) {
661
+ return path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
662
+ }
663
+ function extractPathParams(path) {
664
+ const names = [];
665
+ const re = /:([a-zA-Z_]\w*)/g;
666
+ let m;
667
+ while ((m = re.exec(path)) !== null) {
668
+ if (m[1])
669
+ names.push(m[1]);
670
+ }
671
+ return names;
672
+ }
493
673
  function generateOpenAPI(options = {}, registryToUse = registry_default) {
494
674
  const spec = {
495
675
  openapi: "3.1.0",
@@ -504,12 +684,16 @@ function generateOpenAPI(options = {}, registryToUse = registry_default) {
504
684
  }
505
685
  };
506
686
  const targets = registryToUse.getTargets();
687
+ const metaParamsMap = new Map;
507
688
  for (const target of targets) {
508
689
  for (const key of Object.getOwnPropertyNames(target)) {
509
690
  const property = target[key];
510
691
  if (property && property.isEndpoint) {
511
692
  const path = property.path || "/unknown";
512
- const method = property.method || "get";
693
+ const method = (property.method || "get").toLowerCase();
694
+ if (property.metadata?.parameters) {
695
+ metaParamsMap.set(`${path}|${method}`, property.metadata.parameters);
696
+ }
513
697
  if (!spec.paths[path]) {
514
698
  spec.paths[path] = {};
515
699
  }
@@ -527,6 +711,36 @@ function generateOpenAPI(options = {}, registryToUse = registry_default) {
527
711
  }
528
712
  }
529
713
  }
714
+ const rewrittenPaths = {};
715
+ for (const [rawPath, methods] of Object.entries(spec.paths)) {
716
+ const openApiPath = toOpenAPIPath(rawPath);
717
+ const pathParamNames = extractPathParams(rawPath);
718
+ const autoParams = pathParamNames.map((name) => ({
719
+ name,
720
+ in: "path",
721
+ required: true,
722
+ schema: { type: "string" }
723
+ }));
724
+ rewrittenPaths[openApiPath] ??= {};
725
+ for (const [method, operation] of Object.entries(methods)) {
726
+ const decoratorParams = metaParamsMap.get(`${rawPath}|${method}`) ?? [];
727
+ if (autoParams.length > 0 || decoratorParams.length > 0) {
728
+ operation.parameters = [...autoParams, ...decoratorParams];
729
+ }
730
+ rewrittenPaths[openApiPath][method] = operation;
731
+ }
732
+ }
733
+ spec.paths = rewrittenPaths;
734
+ const resolved = {};
735
+ for (const target of targets) {
736
+ const refs = target[SCHEMAS];
737
+ if (!refs)
738
+ continue;
739
+ for (const name of refs) {
740
+ resolveSchema(name, resolved, options.schemas || {});
741
+ }
742
+ }
743
+ spec.components.schemas = resolved;
530
744
  return spec;
531
745
  }
532
746
  export {
@@ -534,6 +748,7 @@ export {
534
748
  json,
535
749
  generateOpenAPI,
536
750
  TypedRouter,
751
+ SCHEMAS,
537
752
  Registry,
538
753
  Endpoint,
539
754
  Controller