Skip to content

Commit 940774a

Browse files
Merge pull request #107 from bootgs/fix/route-inheritance-bug
Fix/route inheritance bug
2 parents fd5c5c7 + 77b5209 commit 940774a

7 files changed

Lines changed: 253 additions & 83 deletions

File tree

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ Use this when building Sidebars, Modals, or Add-ons.
236236
**Example (Client-side JS):**
237237

238238
```javascript
239-
const path = '/api/users/123';
240-
const method = 'GET';
239+
const path = "/api/users/123";
240+
const method = "GET";
241241
const headers = JSON.stringify({
242242
"X-Request-Source": "internal"
243243
});
@@ -259,14 +259,14 @@ const event = {
259259
};
260260

261261
google.script.run
262-
.withSuccessHandler(response => {
262+
.withSuccessHandler((response) => {
263263
// Parse the optimized string response
264-
const result = typeof response === 'string' ? JSON.parse(response) : response;
265-
264+
const result = typeof response === "string" ? JSON.parse(response) : response;
265+
266266
console.log("Status:", result.status);
267267
console.log("Data:", result.body);
268268
})
269-
.doGet(event);
269+
.doGet(event);
270270
```
271271

272272
#### 2. External Usage (Web App URL)
@@ -281,7 +281,9 @@ Use this when accessing the script via a direct link, a webhook, or a third-part
281281
The framework automatically handles your controller's return value based on whether the `@ResponseBody` decorator is used (note that `@RestController` applies this by default):
282282

283283
#### A. Default Wrapper (No `@ResponseBody`)
284+
284285
If the controller method is not marked with `@ResponseBody`, the framework returns a full HTTP-like payload:
286+
285287
```json
286288
{
287289
"status": 200,
@@ -293,6 +295,7 @@ If the controller method is not marked with `@ResponseBody`, the framework retur
293295
```
294296

295297
#### B. Direct Context (`@ResponseBody`)
298+
296299
If the method is marked with `@ResponseBody`, the framework bypasses the payload wrapper and returns only the data directly.
297300

298301
> [!TIP]

config/eslint/overrides-tests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const config: Linter.Config = {
1616
* Disallows unused variables.
1717
* @see {@link https://typescript-eslint.io/rules/no-unused-vars/ no-unused-vars}
1818
*/
19-
"@typescript-eslint/no-unused-vars": "error"
19+
"@typescript-eslint/no-unused-vars": "off"
2020
}
2121
};
2222

src/service/EventDispatcher.ts

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,46 +37,54 @@ export class EventDispatcher {
3737
const promises: Promise<void>[] = [];
3838

3939
for (const controller of this.controllers.keys()) {
40-
const prototype: Record<string, unknown> = (controller as any).prototype;
40+
const processedMethods: Set<string> = new Set<string>();
4141

42-
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
42+
let prototype: object = (controller as any).prototype;
4343

44-
for (const propertyName of propertyNames) {
45-
if (propertyName === "constructor") {
46-
continue;
47-
}
44+
while (prototype && prototype !== Object.prototype) {
45+
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
4846

49-
const methodHandler: unknown = prototype[propertyName];
47+
for (const propertyName of propertyNames) {
48+
if (propertyName === "constructor" || processedMethods.has(propertyName)) {
49+
continue;
50+
}
5051

51-
const eventMetadata: AppsScriptEventType | undefined = Reflect.getMetadata(
52-
APPSSCRIPT_EVENT_METADATA,
53-
methodHandler as object
54-
);
52+
processedMethods.add(propertyName);
5553

56-
const options: Record<string, unknown> | undefined = Reflect.getMetadata(
57-
APPSSCRIPT_OPTIONS_METADATA,
58-
methodHandler as object
59-
);
54+
const methodHandler: unknown = (prototype as any)[propertyName];
6055

61-
if (eventMetadata === eventType && this.checkFilters(eventType, event, options)) {
62-
const instance: unknown = this.resolver.resolve(controller);
56+
const eventMetadata: AppsScriptEventType | undefined = Reflect.getMetadata(
57+
APPSSCRIPT_EVENT_METADATA,
58+
methodHandler as object
59+
);
6360

64-
if (!isRecord(instance)) {
65-
continue;
66-
}
61+
const options: Record<string, unknown> | undefined = Reflect.getMetadata(
62+
APPSSCRIPT_OPTIONS_METADATA,
63+
methodHandler as object
64+
);
65+
66+
if (eventMetadata === eventType && this.checkFilters(eventType, event, options)) {
67+
const instance: unknown = this.resolver.resolve(controller);
6768

68-
const args: unknown[] = this.buildMethodParams(instance, propertyName, event);
69+
if (!isRecord(instance)) {
70+
continue;
71+
}
72+
73+
const args: unknown[] = this.buildMethodParams(instance, propertyName, event);
6974

70-
const handler: unknown = instance[propertyName];
75+
const handler: unknown = instance[propertyName];
7176

72-
if (isFunctionLike(handler)) {
73-
const result: unknown = Reflect.apply(handler, instance, args);
77+
if (isFunctionLike(handler)) {
78+
const result: unknown = Reflect.apply(handler, instance, args);
7479

75-
if (result instanceof Promise) {
76-
promises.push(result);
80+
if (result instanceof Promise) {
81+
promises.push(result);
82+
}
7783
}
7884
}
7985
}
86+
87+
prototype = Object.getPrototypeOf(prototype);
8088
}
8189
}
8290

src/service/Router.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -382,27 +382,38 @@ export class Router {
382382
return null;
383383
}
384384

385-
const prototype: any = Object.getPrototypeOf(instance);
385+
let prototype: any = Object.getPrototypeOf(instance);
386+
const visitedMethods: Set<string> = new Set<string>();
386387

387-
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
388+
while (prototype && prototype !== Object.prototype) {
389+
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
388390

389-
for (const propertyName of propertyNames) {
390-
const method: any = instance[propertyName];
391+
for (const propertyName of propertyNames) {
392+
if (propertyName === "constructor" || visitedMethods.has(propertyName)) {
393+
continue;
394+
}
391395

392-
if (!isFunctionLike(method)) continue;
396+
visitedMethods.add(propertyName);
393397

394-
const exceptions: Newable<Error>[] | undefined = Reflect.getMetadata(
395-
EXCEPTION_HANDLER_METADATA,
396-
method
397-
);
398+
const method: any = instance[propertyName];
398399

399-
if (exceptions && Array.isArray(exceptions)) {
400-
for (const exceptionClass of exceptions) {
401-
if (err instanceof exceptionClass) {
402-
return propertyName;
400+
if (!isFunctionLike(method)) continue;
401+
402+
const exceptions: Newable<Error>[] | undefined = Reflect.getMetadata(
403+
EXCEPTION_HANDLER_METADATA,
404+
method
405+
);
406+
407+
if (exceptions && Array.isArray(exceptions)) {
408+
for (const exceptionClass of exceptions) {
409+
if (err instanceof exceptionClass) {
410+
return propertyName;
411+
}
403412
}
404413
}
405414
}
415+
416+
prototype = Object.getPrototypeOf(prototype);
406417
}
407418

408419
return null;

src/service/RouterExplorer.ts

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,54 @@ export class RouterExplorer {
3737

3838
const basePath: string = (controllerOptions.basePath as string) || "/";
3939

40-
const prototype: Record<string, unknown> = (controller as any).prototype;
40+
let prototype: object = (controller as any).prototype;
41+
const visitedMethods: Set<string> = new Set<string>();
4142

42-
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
43+
while (prototype && prototype !== Object.prototype) {
44+
const propertyNames: string[] = Object.getOwnPropertyNames(prototype);
4345

44-
for (const propertyName of propertyNames) {
45-
if (propertyName === "constructor") {
46-
continue;
47-
}
46+
for (const propertyName of propertyNames) {
47+
if (propertyName === "constructor" || visitedMethods.has(propertyName)) {
48+
continue;
49+
}
4850

49-
const methodHandler: unknown = prototype[propertyName];
50-
51-
const routePath: string | undefined = Reflect.getMetadata(
52-
PATH_METADATA,
53-
methodHandler as object
54-
);
55-
56-
const requestMethods: RequestMethod | RequestMethod[] | undefined = Reflect.getMetadata(
57-
METHOD_METADATA,
58-
methodHandler as object
59-
);
60-
61-
const produce: ContentMimeType | undefined = Reflect.getMetadata(
62-
PRODUCE_METADATA,
63-
methodHandler as object
64-
);
65-
66-
if (routePath && requestMethods) {
67-
const methods: RequestMethod[] = Array.isArray(requestMethods)
68-
? requestMethods
69-
: [requestMethods];
70-
71-
for (const method of methods) {
72-
routes.push({
73-
controller,
74-
handler: propertyName,
75-
method,
76-
path: decodeURI(normalize(`/${basePath}/${routePath}`)),
77-
produce
78-
});
51+
visitedMethods.add(propertyName);
52+
53+
const methodHandler: unknown = (prototype as any)[propertyName];
54+
55+
const routePath: string | undefined = Reflect.getMetadata(
56+
PATH_METADATA,
57+
methodHandler as object
58+
);
59+
60+
const requestMethods: RequestMethod | RequestMethod[] | undefined = Reflect.getMetadata(
61+
METHOD_METADATA,
62+
methodHandler as object
63+
);
64+
65+
const produce: ContentMimeType | undefined = Reflect.getMetadata(
66+
PRODUCE_METADATA,
67+
methodHandler as object
68+
);
69+
70+
if (routePath && requestMethods) {
71+
const methods: RequestMethod[] = Array.isArray(requestMethods)
72+
? requestMethods
73+
: [requestMethods];
74+
75+
for (const method of methods) {
76+
routes.push({
77+
controller,
78+
handler: propertyName,
79+
method,
80+
path: decodeURI(normalize(`/${basePath}/${routePath}`)),
81+
produce
82+
});
83+
}
7984
}
8085
}
86+
87+
prototype = Object.getPrototypeOf(prototype);
8188
}
8289
}
8390

0 commit comments

Comments
 (0)