Skip to content

Commit 8f9c5e4

Browse files
Merge pull request #95 from bootgs/max/next
Max/next
2 parents e6a00b2 + 040c471 commit 8f9c5e4

194 files changed

Lines changed: 4622 additions & 777 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,5 @@ nodelinter.config.json
107107
CHANGELOG-*.md
108108
*.mdx
109109
trivy_report*
110+
.output.txt
110111
!/.gitignore

.junie/guidelines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ setup file.
5252

5353
#### Test Example
5454

55-
```typescript
55+
```TypeScript
5656
import "reflect-metadata";
5757
import { describe, expect, it } from "vitest";
5858
import { HttpController } from "../src/decorators";

README.md

Lines changed: 239 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export class SheetController {
6565
Bootstrap your application by creating an `App` instance and delegating the standard Apps Script entry points (`doGet`,
6666
`doPost`) to it.
6767

68+
#### Synchronous Application
69+
70+
Use `App` for synchronous execution:
71+
6872
```TypeScript
6973
import {App} from "bootgs";
7074
import {SheetController} from "./SheetController";
@@ -90,10 +94,43 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
9094
}
9195
```
9296

97+
#### Asynchronous Application
98+
99+
Use `AsyncApp` when you need to handle asynchronous operations (e.g., `UrlFetchApp` promises or other async tasks) in your controllers:
100+
101+
```TypeScript
102+
import {AsyncApp} from "bootgs";
103+
import {SheetController} from "./SheetController";
104+
105+
/**
106+
* Global entry point for GET requests.
107+
*/
108+
export async function doGet(event: GoogleAppsScript.Events.DoGet) {
109+
const app = AsyncApp.create({
110+
controllers: [SheetController]
111+
});
112+
return await app.doGet(event);
113+
}
114+
115+
/**
116+
* Global entry point for POST requests.
117+
*/
118+
export async function doPost(event: GoogleAppsScript.Events.DoPost) {
119+
const app = AsyncApp.create({
120+
controllers: [SheetController]
121+
});
122+
return await app.doPost(event);
123+
}
124+
```
125+
93126
## Features
94127

95-
- **Decorator-based Routing**: Intuitive mapping of HTTP and Apps Script events.
96-
- **Dependency Injection**: Decouple your components for better testability.
128+
- **Decorator-based Routing**: Intuitive mapping of HTTP and Apps Script events (GET, POST, etc.).
129+
- **Spring Boot & NestJS Patterns**: Familiar decorators like `@RequestMapping`, `@Autowired`, `@Value`.
130+
- **Validation**: Declarative parameter validation using Spring Boot-style decorators like `@Min`, `@Max`, `@Email`, etc.
131+
- **Pipes & Validation**: Transform and validate incoming data with `@UsePipes` and built-in pipes (e.g., `ParseNumberPipe`).
132+
- **Global Error Handling**: Centralized exception management using `@ControllerAdvice` and `@ExceptionHandler`.
133+
- **Dependency Injection**: Fully-featured DI for better decoupling and testability.
97134
- **Type Safety**: Built with TypeScript for a robust development experience.
98135
- **Modern Architecture**: Inspired by frameworks like NestJS and Spring Boot.
99136

@@ -117,11 +154,21 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
117154
<td><code>ClassDecorator</code></td>
118155
<td>Marks a class as a general-purpose controller.</td>
119156
</tr>
157+
<tr>
158+
<td><code>@RequestMapping(path?: string, method?: RequestMethod | RequestMethod[])</code></td>
159+
<td><code>ClassDecorator & MethodDecorator</code></td>
160+
<td>Maps a specific request path onto a controller or a handler method.</td>
161+
</tr>
120162
<tr>
121163
<td><code>@HttpController(basePath?: string)</code></td>
122164
<td><code>ClassDecorator</code></td>
123165
<td>Marks a class as an HTTP request controller. Default base path is <code>/</code>.</td>
124166
</tr>
167+
<tr>
168+
<td><code>@ControllerAdvice()</code></td>
169+
<td><code>ClassDecorator</code></td>
170+
<td>Marks a class as a global exception handler and data binder.</td>
171+
</tr>
125172
<tr>
126173
<td><code>@SheetController(sheetName?: string | string[] | RegExp)</code></td>
127174
<td><code>ClassDecorator</code></td>
@@ -165,6 +212,11 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
165212
<td><code>ClassDecorator</code></td>
166213
<td>Alias for <code>@HttpController()</code>.</td>
167214
</tr>
215+
<tr>
216+
<td><code>@RestControllerAdvice()</code></td>
217+
<td><code>ClassDecorator</code></td>
218+
<td>Alias for <code>@ControllerAdvice()</code>.</td>
219+
</tr>
168220
<tr>
169221
<td><code>@SheetsController(sheetName?: string | string[] | RegExp)</code></td>
170222
<td><code>ClassDecorator</code></td>
@@ -236,6 +288,11 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
236288
<tr>
237289
<td colspan="3" align="center"><b>HTTP Methods</b></td>
238290
</tr>
291+
<tr>
292+
<td><code>@RequestMapping(path?: string, method?: RequestMethod | RequestMethod[])</code></td>
293+
<td><code>ClassDecorator & MethodDecorator</code></td>
294+
<td>Maps a specific request path onto a controller or a handler method.</td>
295+
</tr>
239296
<tr>
240297
<td><code>@Get(path?: string)</code></td>
241298
<td><code>MethodDecorator</code></td>
@@ -271,6 +328,24 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
271328
<td><code>MethodDecorator</code></td>
272329
<td>Maps a method to handle HTTP OPTIONS requests.</td>
273330
</tr>
331+
<tr>
332+
<td colspan="3" align="center"><b>Error Handling & Security</b></td>
333+
</tr>
334+
<tr>
335+
<td><code>@ExceptionHandler(value?: Newable | Newable[])</code></td>
336+
<td><code>MethodDecorator</code></td>
337+
<td>Annotation for handling exceptions in specific handler classes and/or handler methods.</td>
338+
</tr>
339+
<tr>
340+
<td><code>@ResponseStatus(value: number)</code></td>
341+
<td><code>MethodDecorator & ClassDecorator</code></td>
342+
<td>Marks a method or exception class with the status code that should be returned.</td>
343+
</tr>
344+
<tr>
345+
<td><code>@UsePipes(...pipes: any[])</code></td>
346+
<td><code>MethodDecorator & ClassDecorator</code></td>
347+
<td>Specifies the pipes to be used for a controller or method.</td>
348+
</tr>
274349
<tr>
275350
<td colspan="3" align="center"><b>Aliases</b></td>
276351
</tr>
@@ -343,48 +418,201 @@ export function doPost(event: GoogleAppsScript.Events.DoPost) {
343418
<td>Injects request headers or a specific header value.</td>
344419
</tr>
345420
<tr>
346-
<td><code>@Body(key?: string)</code></td>
421+
<td><code>@Body(key?: string, ...pipes: any[])</code></td>
347422
<td><code>ParameterDecorator</code></td>
348-
<td>Injects the full request body or a specific key.</td>
423+
<td>Injects the full request body or a specific key. Supports transformation pipes.</td>
349424
</tr>
350425
<tr>
351-
<td><code>@Param(key?: string)</code></td>
426+
<td><code>@Param(key?: string, ...pipes: any[])</code></td>
352427
<td><code>ParameterDecorator</code></td>
353-
<td>Injects values from URL path parameters.</td>
428+
<td>Injects values from URL path parameters. Supports transformation pipes.</td>
354429
</tr>
355430
<tr>
356-
<td><code>@Query(key?: string)</code></td>
431+
<td><code>@Query(key?: string, ...pipes: any[])</code></td>
357432
<td><code>ParameterDecorator</code></td>
358-
<td>Injects values from URL query parameters.</td>
433+
<td>Injects values from URL query parameters. Supports transformation pipes.</td>
359434
</tr>
360435
<tr>
361436
<td><code>@Inject(token: any)</code></td>
362437
<td><code>ParameterDecorator</code></td>
363438
<td>Explicitly specifies an injection token for a dependency.</td>
364439
</tr>
440+
<tr>
441+
<td><code>@Value(key: string)</code></td>
442+
<td><code>ParameterDecorator & PropertyDecorator</code></td>
443+
<td>Injects a value from the application configuration.</td>
444+
</tr>
365445
<tr>
366446
<td colspan="3" align="center"><b>Aliases</b></td>
367447
</tr>
368448
<tr>
369-
<td><code>@RequestBody(key?: string)</code></td>
449+
<td><code>@Autowired(token?: any)</code></td>
450+
<td><code>ParameterDecorator & PropertyDecorator</code></td>
451+
<td>Alias for <code>@Inject()</code>.</td>
452+
</tr>
453+
<tr>
454+
<td><code>@RequestBody(key?: string, ...pipes: any[])</code></td>
370455
<td><code>ParameterDecorator</code></td>
371456
<td>Alias for <code>@Body()</code>.</td>
372457
</tr>
373458
<tr>
374-
<td><code>@PathVariable(key?: string)</code></td>
459+
<td><code>@PathVariable(key?: string, ...pipes: any[])</code></td>
375460
<td><code>ParameterDecorator</code></td>
376461
<td>Alias for <code>@Param()</code>.</td>
377462
</tr>
378463
<tr>
379-
<td><code>@RequestParam(key?: string)</code></td>
464+
<td><code>@RequestParam(key?: string, ...pipes: any[])</code></td>
380465
<td><code>ParameterDecorator</code></td>
381466
<td>Alias for <code>@Query()</code>.</td>
382467
</tr>
468+
<tr>
469+
<td colspan="3" align="center"><b>Validation Decorators (Spring Boot style)</b></td>
470+
</tr>
471+
<tr>
472+
<td><code>@AssertFalse()</code></td>
473+
<td><code>ParameterDecorator</code></td>
474+
<td>Validates that the value is <code>false</code>.</td>
475+
</tr>
476+
<tr>
477+
<td><code>@AssertTrue()</code></td>
478+
<td><code>ParameterDecorator</code></td>
479+
<td>Validates that the value is <code>true</code>.</td>
480+
</tr>
481+
<tr>
482+
<td><code>@Email()</code></td>
483+
<td><code>ParameterDecorator</code></td>
484+
<td>Validates that the value is a valid email address.</td>
485+
</tr>
486+
<tr>
487+
<td><code>@Max(value: number)</code></td>
488+
<td><code>ParameterDecorator</code></td>
489+
<td>Validates that the value is less than or equal to the specified maximum.</td>
490+
</tr>
491+
<tr>
492+
<td><code>@Min(value: number)</code></td>
493+
<td><code>ParameterDecorator</code></td>
494+
<td>Validates that the value is greater than or equal to the specified minimum.</td>
495+
</tr>
496+
<tr>
497+
<td><code>@Negative()</code></td>
498+
<td><code>ParameterDecorator</code></td>
499+
<td>Validates that the value is strictly negative.</td>
500+
</tr>
501+
<tr>
502+
<td><code>@NegativeOrZero()</code></td>
503+
<td><code>ParameterDecorator</code></td>
504+
<td>Validates that the value is negative or zero.</td>
505+
</tr>
506+
<tr>
507+
<td><code>@NotBlank()</code></td>
508+
<td><code>ParameterDecorator</code></td>
509+
<td>Validates that the value is not null and contains at least one non-whitespace character.</td>
510+
</tr>
511+
<tr>
512+
<td><code>@NotEmpty()</code></td>
513+
<td><code>ParameterDecorator</code></td>
514+
<td>Validates that the value is not null and not empty (works for strings, arrays, and objects).</td>
515+
</tr>
516+
<tr>
517+
<td><code>@Pattern(regexp: string | RegExp)</code></td>
518+
<td><code>ParameterDecorator</code></td>
519+
<td>Validates that the value matches the specified regular expression.</td>
520+
</tr>
521+
<tr>
522+
<td><code>@Positive()</code></td>
523+
<td><code>ParameterDecorator</code></td>
524+
<td>Validates that the value is strictly positive.</td>
525+
</tr>
526+
<tr>
527+
<td><code>@PositiveOrZero()</code></td>
528+
<td><code>ParameterDecorator</code></td>
529+
<td>Validates that the value is positive or zero.</td>
530+
</tr>
531+
<tr>
532+
<td><code>@Size(options: { min?: number, max?: number })</code></td>
533+
<td><code>ParameterDecorator</code></td>
534+
<td>Validates that the size of the value is between the specified minimum and maximum.</td>
535+
</tr>
383536
</tbody>
384537
</table>
385538

386539
</details>
387540

541+
### Built-in Pipes
542+
543+
Pipes can be used to transform data before it reaches your handler:
544+
545+
| Pipe | Description |
546+
| :------------------- | :-------------------------------------------------------------------------- |
547+
| `ParseNumberPipe` | Transforms a string to a number. |
548+
| `ParseFloatPipe` | Transforms a string to a float. |
549+
| `ParseBooleanPipe` | Transforms a string to a boolean. |
550+
| `AssertFalsePipe` | Validates that the value is `false`. |
551+
| `AssertTruePipe` | Validates that the value is `true`. |
552+
| `EmailPipe` | Validates that the value is a valid email address. |
553+
| `MaxPipe` | Validates that the value is less than or equal to the specified maximum. |
554+
| `MinPipe` | Validates that the value is greater than or equal to the specified minimum. |
555+
| `NegativePipe` | Validates that the value is strictly negative. |
556+
| `NegativeOrZeroPipe` | Validates that the value is negative or zero. |
557+
| `NotBlankPipe` | Validates that the value is not blank. |
558+
| `NotEmptyPipe` | Validates that the value is not empty. |
559+
| `PatternPipe` | Validates that the value matches the specified regular expression. |
560+
| `PositivePipe` | Validates that the value is strictly positive. |
561+
| `PositiveOrZeroPipe` | Validates that the value is positive or zero. |
562+
| `SizePipe` | Validates that the size of the value is within range. |
563+
564+
## Advanced Examples
565+
566+
### Pipes
567+
568+
Transform parameters with pipes:
569+
570+
```TypeScript
571+
import {Get, RestController, Query, ParseNumberPipe} from "bootgs";
572+
573+
@RestController("users")
574+
export class UserController {
575+
576+
@Get("details")
577+
getUserDetails(@Query("id", ParseNumberPipe) id: number): object {
578+
return {
579+
userId: id,
580+
message: "Success!"
581+
};
582+
}
583+
}
584+
```
585+
586+
### Global Error Handling
587+
588+
Use `@ControllerAdvice` to handle exceptions globally across the whole application:
589+
590+
```TypeScript
591+
import {ControllerAdvice, ExceptionHandler, ResponseStatus} from "bootgs";
592+
593+
@ControllerAdvice()
594+
export class GlobalExceptionHandler {
595+
596+
@ExceptionHandler(Error)
597+
@ResponseStatus(500)
598+
handleError(error: Error): object {
599+
return {
600+
status: "Error",
601+
message: error.message
602+
};
603+
}
604+
}
605+
```
606+
607+
## Contributors
608+
609+
<a href="https://github.com/felipepmdias">
610+
<img src="https://github.com/felipepmdias.png" width="50" height="50" style="border-radius: 50%" alt="felipepmdias" />
611+
</a>
612+
<a href="https://github.com/kosmo-ds">
613+
<img src="https://github.com/kosmo-ds.png" width="50" height="50" style="border-radius: 50%" alt="kosmo-ds" />
614+
</a>
615+
388616
## Contributing
389617

390618
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct,

config/eslint/common-ignores.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Linter } from "eslint";
66
* @see https://eslint.org/docs/latest/use/configure/ignore
77
*/
88
const config: Linter.Config = {
9-
ignores: [ "dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js" ]
9+
ignores: ["dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js"]
1010
};
1111

1212
export default config;

config/eslint/env-appsscript.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ import type { Linter } from "eslint";
44
* Google Apps Script environment settings.
55
*/
66
const config: Linter.Config = {
7-
files: [ "src/**/*.{js,mjs,cjs,ts,jsx,tsx}" ],
7+
files: ["src/**/*.{js,mjs,cjs,ts,jsx,tsx}"],
88
rules: {
99
"no-restricted-globals": [
1010
"error",
1111
{
1212
name: "setTimeout",
13-
message:
14-
"Use Utilities.sleep for synchronous pauses. Async timers are not supported in GAS."
13+
message: "Use Utilities.sleep for synchronous pauses. timers are not supported in GAS."
1514
},
16-
{ name: "setInterval", message: "Async timers are not supported in GAS." },
17-
{ name: "clearTimeout", message: "Async timers are not supported in GAS." },
18-
{ name: "clearInterval", message: "Async timers are not supported in GAS." },
15+
{ name: "setInterval", message: "timers are not supported in GAS." },
16+
{ name: "clearTimeout", message: "timers are not supported in GAS." },
17+
{ name: "clearInterval", message: "timers are not supported in GAS." },
1918
{ name: "fetch", message: "Use UrlFetchApp.fetch instead." },
2019
{ name: "atob", message: "Use Utilities.base64Decode instead." },
2120
{ name: "btoa", message: "Use Utilities.base64Encode instead." },

config/eslint/lang-javascript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Linter } from "eslint";
77
* @see {@link https://eslint.org/docs/latest/rules/ ESLint rules}
88
*/
99
const config: Linter.Config = {
10-
files: [ "**/*.{js,mjs,cjs}" ],
10+
files: ["**/*.{js,mjs,cjs}"],
1111
...js.configs.recommended
1212
};
1313

0 commit comments

Comments
 (0)