Skip to content

Commit 7d44bfb

Browse files
Merge pull request #90 from bootgs/max/next
Max/next
2 parents b80dee7 + 17a32f3 commit 7d44bfb

24 files changed

Lines changed: 255 additions & 122 deletions

config/eslint/overrides-tests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const config: Linter.Config = {
1010
* Disallows usage of the `any` type.
1111
* @see {@link https://typescript-eslint.io/rules/no-explicit-any/ no-explicit-any}
1212
*/
13-
"@typescript-eslint/no-explicit-any": "error",
13+
"@typescript-eslint/no-explicit-any": "off",
1414

1515
/**
1616
* Disallows unused variables.

src/controller/BootApplication.ts

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

613
/**
714
* Main application class for bootstrapping and handling Google Apps Script events.
@@ -130,20 +137,24 @@ export class BootApplication {
130137
/**
131138
* Returns a Proxy object that can be used to handle Google Apps Script menu actions.
132139
*
133-
* @returns {any} A Proxy object.
140+
* @returns {AppsScriptMenuProxy} A Proxy object.
134141
*/
135-
public onMenu(): any {
142+
public onMenu(): AppsScriptMenuProxy {
136143
return new Proxy(this, {
137144
get(target, prop, receiver) {
138-
if (prop === "inspect" || isSymbol(prop)) {
145+
if (!isString(prop)) {
146+
return Reflect.get(target, prop, receiver);
147+
}
148+
149+
if (prop === "inspect") {
139150
return Reflect.get(target, prop, receiver);
140151
}
141152

142153
return (event: GoogleAppsScript.Events.AppsScriptEvent) => {
143-
return target._eventDispatcher.dispatchByName(prop as string, event);
154+
return target._eventDispatcher.dispatchByName(prop, event);
144155
};
145156
}
146-
});
157+
}) as unknown as AppsScriptMenuProxy;
147158
}
148159

149160
/**
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* A Proxy object that can be used to handle Google Apps Script menu actions.
3+
*/
4+
export type AppsScriptMenuProxy = {
5+
[key: string]: (event: GoogleAppsScript.Events.AppsScriptEvent) => Promise<void>;
6+
};

src/domain/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./AppsScriptMenuProxy";
12
export * from "./ApplicationConfig";
23
export * from "./ClassProvider";
34
export * from "./ErrorResponse";

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "reflect-metadata";
22
import { BootApplication, BootApplicationFactory } from "./controller";
33

44
export * from "./controller";
5-
export * from "./utils";
5+
export * from "./shared/utils";
66
export * from "./domain/types";
77
export * from "./domain/enums";
88

src/repository/assignInjectMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export function assignInjectMetadata(
1818

1919
return {
2020
...existing,
21-
[ `${type as string}:${index}` ]: { type, token, index }
21+
[ `${type}:${index}` ]: { type, token, index }
2222
};
2323
}

src/repository/assignParamMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export function assignParamMetadata(
1818
): Record<string, ParamDefinition> {
1919
return {
2020
...existing,
21-
[ `${type as string}:${index}` ]: { type, key, index }
21+
[ `${type}:${index}` ]: { type, key, index }
2222
};
2323
}

src/service/EventDispatcher.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ 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";
11+
import { isFunctionLike, isRegExp } from "apps-script-utils";
12+
import { isChangeEvent, isEditEvent, isFormSubmitEvent, isRecord } from "../shared/utils";
1213

1314
/**
1415
* Service for dispatching events to controllers.
@@ -39,7 +40,9 @@ export class EventDispatcher {
3940
const propertyNames = Object.getOwnPropertyNames(prototype);
4041

4142
for (const propertyName of propertyNames) {
42-
if (propertyName === "constructor") continue;
43+
if (propertyName === "constructor") {
44+
continue;
45+
}
4346

4447
const methodHandler = prototype[ propertyName ];
4548

@@ -50,12 +53,15 @@ export class EventDispatcher {
5053
if (eventMetadata === eventType && this.checkFilters(eventType, event, options)) {
5154
const instance = this.resolver.resolve(controller);
5255

53-
const args = this.buildMethodParams(instance as object, propertyName, event);
56+
if (!isRecord(instance)) continue;
57+
58+
const args = this.buildMethodParams(instance, propertyName, event);
5459

55-
const handler = (instance as Record<string | symbol, unknown>)[ propertyName ] as (
56-
...args: unknown[]
57-
) => unknown;
58-
await handler.apply(instance, args);
60+
const handler = instance[ propertyName ];
61+
62+
if (isFunctionLike(handler)) {
63+
await Reflect.apply(handler, instance, args);
64+
}
5965
}
6066
}
6167
}
@@ -70,7 +76,9 @@ export class EventDispatcher {
7076
*/
7177
public async dispatchByName(methodName: string, event: unknown): Promise<void> {
7278
for (const controller of this.controllers.keys()) {
73-
const instance = this.resolver.resolve(controller) as Record<string, any>;
79+
const instance = this.resolver.resolve(controller);
80+
81+
if (!isRecord(instance)) continue;
7482

7583
const prototype = Object.getPrototypeOf(instance);
7684

@@ -91,9 +99,9 @@ export class EventDispatcher {
9199
continue;
92100
}
93101

94-
const handler = instance[ methodName ].bind(instance);
102+
const method = instance[ methodName ];
95103

96-
if (!isFunctionLike(handler)) {
104+
if (!isFunctionLike(method)) {
97105
console.warn(
98106
"Method '%s' in controller '%s' is not a callable function and was skipped during event handling.",
99107
methodName,
@@ -103,10 +111,10 @@ export class EventDispatcher {
103111
continue;
104112
}
105113

106-
const args = this.buildMethodParams(instance as object, methodName, event);
114+
const args = this.buildMethodParams(instance, methodName, event);
107115

108116
try {
109-
await handler(...args);
117+
await Reflect.apply(method, instance, args);
110118
} catch (err: unknown) {
111119
console.error("Error:", err instanceof Error ? err.stack : String(err));
112120
}
@@ -136,9 +144,10 @@ export class EventDispatcher {
136144
propertyKey
137145
);
138146

139-
const metadata: (ParamDefinition | InjectTokenDefinition)[] = (
140-
Object.values(rawMetadata) as (ParamDefinition | InjectTokenDefinition)[]
141-
).concat(Object.values(rawInjectMetadata) as (ParamDefinition | InjectTokenDefinition)[]);
147+
const metadata: (ParamDefinition | InjectTokenDefinition)[] = [
148+
...Object.values(rawMetadata),
149+
...Object.values(rawInjectMetadata)
150+
];
142151

143152
metadata.sort((a, b) => a.index - b.index);
144153

@@ -150,10 +159,7 @@ export class EventDispatcher {
150159
for (const param of metadata) {
151160
switch (param.type) {
152161
case ParamSource.EVENT:
153-
args[ param.index ] =
154-
param.key && typeof event === "object" && event !== null
155-
? (event as Record<string, unknown>)[ param.key ]
156-
: event;
162+
args[ param.index ] = param.key && isRecord(event) ? event[ param.key ] : event;
157163
break;
158164

159165
case ParamSource.INJECT:
@@ -188,36 +194,40 @@ export class EventDispatcher {
188194
event: unknown,
189195
options: Record<string, unknown> | undefined
190196
): boolean {
191-
if (!options) return true;
197+
if (!options) {
198+
return true;
199+
}
192200

193201
switch (eventType) {
194202
case AppsScriptEventType.EDIT:
195203
if (options.range) {
196-
const editEvent = event as GoogleAppsScript.Events.SheetsOnEdit;
197-
const eventRangeA1 =
198-
typeof editEvent.range?.getA1Notation === "function"
199-
? editEvent.range.getA1Notation()
200-
: null;
204+
if (!isEditEvent(event)) {
205+
return false;
206+
}
207+
208+
const eventRangeA1 = isFunctionLike(event.range?.getA1Notation)
209+
? event.range.getA1Notation()
210+
: null;
201211

202212
if (!eventRangeA1) {
203213
return false;
204214
}
205215

206216
const ranges = Array.isArray(options.range) ? options.range : [ options.range ];
207217

208-
// TODO: isRegExp
209218
return ranges.some((r: string | RegExp) =>
210-
r instanceof RegExp ? r.test(eventRangeA1) : eventRangeA1 === r
219+
isRegExp(r) ? r.test(eventRangeA1) : eventRangeA1 === r
211220
);
212221
}
213222
break;
214223

215224
case AppsScriptEventType.FORM_SUBMIT:
216225
if (options.formId) {
217-
const submitEvent = event as GoogleAppsScript.Events.FormsOnFormSubmit;
218-
const eventFormId = (
219-
submitEvent.source as unknown as { getId?: () => string }
220-
)?.getId?.();
226+
if (!isFormSubmitEvent(event)) {
227+
return false;
228+
}
229+
230+
const eventFormId = isFunctionLike(event.source?.getId) ? event.source.getId() : null;
221231

222232
if (!eventFormId) {
223233
return false;
@@ -231,8 +241,11 @@ export class EventDispatcher {
231241

232242
case AppsScriptEventType.CHANGE:
233243
if (options.changeType) {
234-
const changeEvent = event as GoogleAppsScript.Events.SheetsOnChange;
235-
const eventChangeType = changeEvent.changeType;
244+
if (!isChangeEvent(event)) {
245+
return false;
246+
}
247+
248+
const eventChangeType = event.changeType;
236249

237250
if (!eventChangeType) {
238251
return false;

src/service/RequestFactory.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { isString, normalize } from "apps-script-utils";
22
import { HttpHeaders, HttpRequest, ParsedUrl } from "../domain/types";
33
import { RequestMethod } from "../domain/enums";
4+
import { isRecord } from "../shared/utils";
45

56
/**
67
* Factory for creating structured HttpRequest objects.
@@ -24,7 +25,8 @@ export class RequestFactory {
2425
}
2526

2627
try {
27-
return JSON.parse(input.trim()) as HttpHeaders;
28+
const parsed = JSON.parse(input.trim());
29+
return isRecord(parsed) ? (parsed as HttpHeaders) : null;
2830
} catch (err: unknown) {
2931
console.warn("Failed to parse JSON:", err);
3032
}
@@ -34,9 +36,7 @@ export class RequestFactory {
3436

3537
const methodParam = event?.parameter?.method?.toLowerCase();
3638

37-
const method = Object.values(RequestMethod).includes(methodParam as RequestMethod)
38-
? (methodParam as RequestMethod)
39-
: methodRequest;
39+
const method = Object.values(RequestMethod).find((v) => v === methodParam) || methodRequest;
4040

4141
const rawPathname =
4242
event?.pathInfo || event?.parameter?.path || event?.parameter?.pathname || "/";

src/service/Resolver.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,21 @@ export class Resolver {
7878
);
7979
}
8080

81-
const tokenName =
82-
typeof tokenToResolve === "function" ? tokenToResolve.name : String(tokenToResolve);
81+
const tokenName = isFunctionLike(tokenToResolve)
82+
? tokenToResolve.name
83+
: String(tokenToResolve);
8384

8485
throw new Error(
8586
`[Resolve ERROR]: '${tokenName}' is not registered as a provider or controller.`
8687
);
8788
}
8889

8990
deps[ i ] = isFunctionLike(tokenToResolve)
90-
? this.resolve(tokenToResolve as Newable)
91+
? this.resolve(tokenToResolve)
9192
: this._providers.get(tokenToResolve);
9293
}
9394

94-
const TargetClass = target as unknown as new (...args: unknown[]) => T;
95-
const instance = new TargetClass(...deps);
95+
const instance = Reflect.construct(target, deps);
9696

9797
if (isController(target)) {
9898
this._controllers.set(target, instance);

0 commit comments

Comments
 (0)