Skip to content

Commit 344fb4e

Browse files
Merge pull request #88 from bootgs/max/next
Max/next
2 parents 50cb113 + 1dc1a25 commit 344fb4e

16 files changed

Lines changed: 309 additions & 8 deletions

File tree

src/controller/BootApplication.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { ApplicationConfig, InjectionToken, Newable } from "../domain/types";
22
import { AppsScriptEventType, RequestMethod } from "../domain/enums";
3-
import {
4-
EventDispatcher,
5-
RequestFactory,
6-
Resolver,
7-
ResponseBuilder,
8-
Router,
9-
RouterExplorer
10-
} from "../service";
3+
import { EventDispatcher, RequestFactory, Resolver, ResponseBuilder, Router, RouterExplorer } from "../service";
4+
import { isSymbol } from "apps-script-utils";
115

6+
/**
7+
* Main application class for bootstrapping and handling Google Apps Script events.
8+
*/
129
export class BootApplication {
1310
private readonly _controllers = new Map<Newable, unknown>();
1411
private readonly _providers = new Map<InjectionToken, unknown>();
@@ -18,6 +15,11 @@ export class BootApplication {
1815
private readonly _responseBuilder = new ResponseBuilder();
1916
private readonly _eventDispatcher: EventDispatcher;
2017

18+
/**
19+
* Creates a new instance of BootApplication.
20+
*
21+
* @param {ApplicationConfig} config The application configuration.
22+
*/
2123
constructor(config: ApplicationConfig) {
2224
(config.controllers || []).forEach((c) => this._controllers.set(c, null));
2325

@@ -50,26 +52,62 @@ export class BootApplication {
5052
this._eventDispatcher = new EventDispatcher(this._resolver, this._controllers);
5153
}
5254

55+
/**
56+
* Handles Google Apps Script doGet events.
57+
*
58+
* @param {GoogleAppsScript.Events.DoGet} event The doGet event object.
59+
* @returns {Promise<GoogleAppsScript.HTML.HtmlOutput | GoogleAppsScript.Content.TextOutput>} The response object.
60+
*/
5361
public async doGet(event: GoogleAppsScript.Events.DoGet) {
5462
return this.handleHttpRequest(RequestMethod.GET, event);
5563
}
5664

65+
/**
66+
* Handles Google Apps Script doPost events.
67+
*
68+
* @param {GoogleAppsScript.Events.DoPost} event The doPost event object.
69+
* @returns {Promise<GoogleAppsScript.HTML.HtmlOutput | GoogleAppsScript.Content.TextOutput>} The response object.
70+
*/
5771
public async doPost(event: GoogleAppsScript.Events.DoPost) {
5872
return this.handleHttpRequest(RequestMethod.POST, event);
5973
}
6074

75+
/**
76+
* Handles Google Apps Script onInstall events.
77+
*
78+
* @param {GoogleAppsScript.Events.AddonOnInstall} event The onInstall event object.
79+
* @returns {Promise<void>}
80+
*/
6181
public async onInstall(event: GoogleAppsScript.Events.AddonOnInstall) {
6282
await this._eventDispatcher.dispatch(AppsScriptEventType.INSTALL, event);
6383
}
6484

85+
/**
86+
* Handles Google Apps Script onOpen events.
87+
*
88+
* @param {GoogleAppsScript.Events.AppsScriptEvent} event The onOpen event object.
89+
* @returns {Promise<void>}
90+
*/
6591
public async onOpen(event: GoogleAppsScript.Events.AppsScriptEvent) {
6692
await this._eventDispatcher.dispatch(AppsScriptEventType.OPEN, event);
6793
}
6894

95+
/**
96+
* Handles Google Apps Script onEdit events.
97+
*
98+
* @param {GoogleAppsScript.Events.SheetsOnEdit} event The onEdit event object.
99+
* @returns {Promise<void>}
100+
*/
69101
public async onEdit(event: GoogleAppsScript.Events.SheetsOnEdit) {
70102
await this._eventDispatcher.dispatch(AppsScriptEventType.EDIT, event);
71103
}
72104

105+
/**
106+
* Handles Google Apps Script onChange events.
107+
*
108+
* @param {GoogleAppsScript.Events.SheetsOnChange} event The onChange event object.
109+
* @returns {Promise<void>}
110+
*/
73111
public async onChange(event: GoogleAppsScript.Events.SheetsOnChange) {
74112
await this._eventDispatcher.dispatch(AppsScriptEventType.CHANGE, event);
75113
}
@@ -79,10 +117,42 @@ export class BootApplication {
79117
// await this.eventDispatcher.dispatch(AppsScriptEventType.SELECTION_CHANGE, event);
80118
// }
81119

120+
/**
121+
* Handles Google Apps Script onFormSubmit events.
122+
*
123+
* @param {GoogleAppsScript.Events.FormsOnFormSubmit} event The onFormSubmit event object.
124+
* @returns {Promise<void>}
125+
*/
82126
public async onFormSubmit(event: GoogleAppsScript.Events.FormsOnFormSubmit) {
83127
await this._eventDispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, event);
84128
}
85129

130+
/**
131+
* Returns a Proxy object that can be used to handle Google Apps Script menu actions.
132+
*
133+
* @returns {any} A Proxy object.
134+
*/
135+
public onMenu(): any {
136+
return new Proxy(this, {
137+
get(target, prop, receiver) {
138+
if (prop === "inspect" || isSymbol(prop)) {
139+
return Reflect.get(target, prop, receiver);
140+
}
141+
142+
return (event: GoogleAppsScript.Events.AppsScriptEvent) => {
143+
return target._eventDispatcher.dispatchByName(prop as string, event);
144+
};
145+
}
146+
});
147+
}
148+
149+
/**
150+
* Handles incoming HTTP requests and routes them to the appropriate controller.
151+
*
152+
* @param {RequestMethod} method The HTTP method (GET or POST).
153+
* @param {GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost} event The Apps Script event object.
154+
* @returns {Promise<GoogleAppsScript.HTML.HtmlOutput | GoogleAppsScript.Content.TextOutput>} The response object.
155+
*/
86156
private async handleHttpRequest(
87157
method: RequestMethod,
88158
event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost

src/controller/BootApplicationFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { ApplicationConfig } from "../domain/types";
22
import { BootApplication } from "../controller";
33

4+
/**
5+
* Factory for creating BootApplication instances.
6+
*/
47
export class BootApplicationFactory {
58
/**
69
* Creates an instance of BootApplication.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { AppsScriptEventType } from "../../../domain/enums";
22
import { createAppsScriptDecorator } from "../../../repository";
33

4+
/**
5+
* Decorator for handling Google Apps Script onChange events.
6+
*/
47
export const OnChange = createAppsScriptDecorator(AppsScriptEventType.CHANGE);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { AppsScriptEventType } from "../../../domain/enums";
22
import { createAppsScriptDecorator } from "../../../repository";
33

4+
/**
5+
* Decorator for handling Google Apps Script onEdit events.
6+
*/
47
export const OnEdit = createAppsScriptDecorator(AppsScriptEventType.EDIT);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { AppsScriptEventType } from "../../../domain/enums";
22
import { createAppsScriptDecorator } from "../../../repository";
33

4+
/**
5+
* Decorator for handling Google Apps Script onFormSubmit events.
6+
*/
47
export const OnFormSubmit = createAppsScriptDecorator(AppsScriptEventType.FORM_SUBMIT);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { AppsScriptEventType } from "../../../domain/enums";
22
import { createAppsScriptDecorator } from "../../../repository";
33

4+
/**
5+
* Decorator for handling Google Apps Script onInstall events.
6+
*/
47
export const OnInstall = createAppsScriptDecorator(AppsScriptEventType.INSTALL);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { AppsScriptEventType } from "../../../domain/enums";
22
import { createAppsScriptDecorator } from "../../../repository";
33

4+
/**
5+
* Decorator for handling Google Apps Script onOpen events.
6+
*/
47
export const OnOpen = createAppsScriptDecorator(AppsScriptEventType.OPEN);

src/service/EventDispatcher.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,30 @@ import { AppsScriptEventType, ParamSource } from "../domain/enums";
88
import { InjectTokenDefinition, Newable, ParamDefinition } from "../domain/types";
99
import { getInjectionTokens } from "../repository";
1010
import { Resolver } from "../service";
11+
import { isFunctionLike } from "apps-script-utils";
1112

13+
/**
14+
* Service for dispatching events to controllers.
15+
*/
1216
export class EventDispatcher {
17+
/**
18+
* Creates a new instance of EventDispatcher.
19+
*
20+
* @param {Resolver} resolver The dependency resolver.
21+
* @param {Map<Newable, unknown>} controllers The registered controllers.
22+
*/
1323
constructor(
1424
private readonly resolver: Resolver,
1525
private readonly controllers: Map<Newable, unknown>
1626
) {}
1727

28+
/**
29+
* Dispatches an event to the registered controllers.
30+
*
31+
* @param {AppsScriptEventType} eventType The type of the event to dispatch.
32+
* @param {unknown} event The event object.
33+
* @returns {Promise<void>} A promise that resolves when all handlers have finished.
34+
*/
1835
public async dispatch(eventType: AppsScriptEventType, event: unknown): Promise<void> {
1936
for (const controller of this.controllers.keys()) {
2037
const prototype = controller.prototype;
@@ -44,6 +61,66 @@ export class EventDispatcher {
4461
}
4562
}
4663

64+
/**
65+
* Dispatches an event to the registered controllers by its name.
66+
*
67+
* @param {string} methodName The name of the method to dispatch the event to.
68+
* @param {unknown} event The event object.
69+
* @returns {Promise<void>} A promise that resolves when all handlers have finished.
70+
*/
71+
public async dispatchByName(methodName: string, event: unknown): Promise<void> {
72+
for (const controller of this.controllers.keys()) {
73+
const instance = this.resolver.resolve(controller) as Record<string, any>;
74+
75+
const prototype = Object.getPrototypeOf(instance);
76+
77+
const methodNames: string[] = [];
78+
79+
let currentProto = prototype;
80+
81+
while (currentProto && currentProto !== Object.prototype) {
82+
Object.getOwnPropertyNames(currentProto).forEach((name) => {
83+
if (name !== "constructor" && isFunctionLike(currentProto[ name ])) {
84+
methodNames.push(name);
85+
}
86+
});
87+
currentProto = Object.getPrototypeOf(currentProto);
88+
}
89+
90+
if (!methodNames.includes(methodName)) {
91+
continue;
92+
}
93+
94+
const handler = instance[ methodName ].bind(instance);
95+
96+
if (!isFunctionLike(handler)) {
97+
console.warn(
98+
"Method '%s' in controller '%s' is not a callable function and was skipped during event handling.",
99+
methodName,
100+
controller.name
101+
);
102+
103+
continue;
104+
}
105+
106+
const args = this.buildMethodParams(instance as object, methodName, event);
107+
108+
try {
109+
await handler(...args);
110+
} catch (err: unknown) {
111+
console.error("Error:", err instanceof Error ? err.stack : String(err));
112+
}
113+
}
114+
}
115+
116+
/**
117+
* Builds the parameters for a controller method.
118+
*
119+
* @param {object} target The target object.
120+
* @param {string | symbol} propertyKey The name of the property.
121+
* @param {unknown} event The event object.
122+
* @returns {unknown[]} An array of parameters for the method.
123+
*/
47124
public buildMethodParams(
48125
target: object,
49126
propertyKey: string | symbol,
@@ -98,6 +175,14 @@ export class EventDispatcher {
98175
return args;
99176
}
100177

178+
/**
179+
* Checks if the event should be dispatched based on the provided options.
180+
*
181+
* @param {AppsScriptEventType} eventType The type of the event.
182+
* @param {unknown} event The event object.
183+
* @param {Record<string, unknown> | undefined} options The options to check against.
184+
* @returns {boolean} True if the event should be dispatched, false otherwise.
185+
*/
101186
private checkFilters(
102187
eventType: AppsScriptEventType,
103188
event: unknown,

src/service/RequestFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { isString, normalize } from "apps-script-utils";
22
import { HttpHeaders, HttpRequest, ParsedUrl } from "../domain/types";
33
import { RequestMethod } from "../domain/enums";
44

5+
/**
6+
* Factory for creating structured HttpRequest objects.
7+
*/
58
export class RequestFactory {
69
/**
710
* Creates a structured HttpRequest object from a raw Apps Script DoGet or DoPost event.

src/service/Resolver.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@ import { PARAMTYPES_METADATA } from "../domain/constants";
55
import { getInjectionTokens } from "../repository";
66
import { isController, isInjectable } from "../shared/utils";
77

8+
/**
9+
* Dependency injection resolver.
10+
*/
811
export class Resolver {
12+
/**
13+
* Creates a new instance of Resolver.
14+
*
15+
* @param {Map<InjectionToken, unknown>} _controllers Registered controllers.
16+
* @param {Map<InjectionToken, unknown>} _providers Registered providers.
17+
*/
918
constructor(
1019
private readonly _controllers: Map<InjectionToken, unknown>,
1120
private readonly _providers: Map<InjectionToken, unknown>
1221
) {}
1322

23+
/**
24+
* Resolves a dependency by its injection token.
25+
*
26+
* @param {InjectionToken<T>} token The injection token.
27+
* @returns {T} The resolved instance.
28+
* @throws {Error} If the dependency cannot be resolved.
29+
*/
1430
public resolve<T>(token: InjectionToken<T>): T {
1531
if (this._controllers.has(token)) {
1632
const instance = this._controllers.get(token);

0 commit comments

Comments
 (0)