@avleon/core 0.0.39 → 0.0.42-rc0.1
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 +113 -28
- package/dist/cache.test.d.ts +1 -0
- package/dist/cache.test.js +36 -0
- package/dist/controller.d.ts +2 -0
- package/dist/controller.js +13 -0
- package/dist/controller.test.d.ts +1 -0
- package/dist/controller.test.js +111 -0
- package/dist/environment-variables.test.d.ts +1 -0
- package/dist/environment-variables.test.js +70 -0
- package/dist/exceptions/http-exceptions.d.ts +1 -0
- package/dist/exceptions/http-exceptions.js +3 -1
- package/dist/file-storage.d.ts +44 -9
- package/dist/file-storage.js +209 -59
- package/dist/file-storage.test.d.ts +1 -0
- package/dist/file-storage.test.js +104 -0
- package/dist/helpers.test.d.ts +1 -0
- package/dist/helpers.test.js +95 -0
- package/dist/icore.d.ts +7 -5
- package/dist/icore.js +191 -69
- package/dist/icore.test.d.ts +1 -0
- package/dist/icore.test.js +14 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +8 -1
- package/dist/kenx-provider.test.d.ts +1 -0
- package/dist/kenx-provider.test.js +36 -0
- package/dist/logger.test.d.ts +1 -0
- package/dist/logger.test.js +42 -0
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.js +7 -0
- package/dist/middleware.test.d.ts +1 -0
- package/dist/middleware.test.js +121 -0
- package/dist/multipart.test.d.ts +1 -0
- package/dist/multipart.test.js +87 -0
- package/dist/openapi.test.d.ts +1 -0
- package/dist/openapi.test.js +111 -0
- package/dist/params.test.d.ts +1 -0
- package/dist/params.test.js +83 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +79 -0
- package/dist/route-methods.test.d.ts +1 -0
- package/dist/route-methods.test.js +129 -0
- package/dist/swagger-schema.d.ts +42 -0
- package/dist/swagger-schema.js +331 -58
- package/dist/swagger-schema.test.d.ts +1 -0
- package/dist/swagger-schema.test.js +105 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.js +2 -0
- package/dist/validation.test.d.ts +1 -0
- package/dist/validation.test.js +61 -0
- package/dist/websocket.test.d.ts +1 -0
- package/dist/websocket.test.js +27 -0
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Avleon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
 
|
|
4
|
+
## ⚠️ WARNING
|
|
4
5
|
|
|
5
6
|
> **🚧 This project is in active development.**
|
|
6
|
-
>
|
|
7
|
-
> It is **not stable** and **not ready** for live environments.
|
|
8
|
-
> Use **only for testing, experimentation, or internal evaluation**.
|
|
9
|
-
>
|
|
10
|
-
> ####❗ Risks of using this in production:
|
|
11
|
-
>
|
|
12
|
-
> - 🔄 Breaking changes may be introduced at any time
|
|
13
|
-
> - 🧪 Features are experimental and may be unstable
|
|
14
|
-
> - 🔐 Security has not been audited
|
|
15
|
-
> - 💥 Potential for data loss or critical errors
|
|
16
|
-
>
|
|
17
|
-
> **Please do not deploy this in production environments.**
|
|
18
7
|
|
|
19
8
|
## Overview
|
|
20
9
|
|
|
@@ -69,11 +58,11 @@ Avleon is a powerful, TypeScript-based web framework built on top of Fastify, de
|
|
|
69
58
|
## Installation
|
|
70
59
|
|
|
71
60
|
```bash
|
|
72
|
-
|
|
61
|
+
npx @avleon/cli new myapp
|
|
73
62
|
# or
|
|
74
|
-
yarn
|
|
63
|
+
yarn dlx @avleon/cli new myapp
|
|
75
64
|
# or
|
|
76
|
-
pnpm
|
|
65
|
+
pnpm dlx @avleon/cli new myapp
|
|
77
66
|
```
|
|
78
67
|
|
|
79
68
|
## Quick Start
|
|
@@ -81,7 +70,7 @@ pnpm add @avleon/core
|
|
|
81
70
|
### Minimal
|
|
82
71
|
|
|
83
72
|
```typescript
|
|
84
|
-
import { Avleon
|
|
73
|
+
import { Avleon } from "@avleon/core";
|
|
85
74
|
|
|
86
75
|
const app = Avleon.createApplication();
|
|
87
76
|
app.mapGet("/", () => "Hello, Avleon");
|
|
@@ -123,7 +112,9 @@ const app = Avleon.createApplication();
|
|
|
123
112
|
// Configure and run the application
|
|
124
113
|
app.useCors();
|
|
125
114
|
app.useControllers([UserController]);
|
|
126
|
-
app.
|
|
115
|
+
// For auto register controller `app.useControllers({auto:true});`
|
|
116
|
+
|
|
117
|
+
app.run(); // or app.run(port)
|
|
127
118
|
```
|
|
128
119
|
|
|
129
120
|
### Controllers
|
|
@@ -253,7 +244,9 @@ class UserController {
|
|
|
253
244
|
Secure your API with authentication and authorization:
|
|
254
245
|
|
|
255
246
|
```typescript
|
|
256
|
-
@
|
|
247
|
+
import { CanAuthorize } from "@avleon/core";
|
|
248
|
+
|
|
249
|
+
@CanAuthorize
|
|
257
250
|
class JwtAuthorization extends AuthorizeMiddleware {
|
|
258
251
|
authorize(roles: string[]) {
|
|
259
252
|
return async (req: IRequest) => {
|
|
@@ -382,6 +375,66 @@ app.useOpenApi(OpenApiConfig, (config) => {
|
|
|
382
375
|
|
|
383
376
|
### Database Integration
|
|
384
377
|
|
|
378
|
+
## 1. Knex
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const app = Avleon.createApplication();
|
|
382
|
+
app.useKnex({
|
|
383
|
+
client: 'mysql',
|
|
384
|
+
connection: {
|
|
385
|
+
host: '127.0.0.1',
|
|
386
|
+
port: 3306,
|
|
387
|
+
user: 'your_database_user',
|
|
388
|
+
password: 'your_database_password',
|
|
389
|
+
database: 'myapp_test',
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
or using config class
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
@AppConfig
|
|
397
|
+
export class KnexConfig implements IConfig {
|
|
398
|
+
// config method is mendatory
|
|
399
|
+
// config method has access to environment variables by default
|
|
400
|
+
config(env: Environment) {
|
|
401
|
+
return {
|
|
402
|
+
client: 'mysql',
|
|
403
|
+
connection: {
|
|
404
|
+
host: env.get("DB_HOST") || '127.0.0.1',
|
|
405
|
+
port: env.get("DB_PORT") || 3306,
|
|
406
|
+
user: env.get("DB_USER")|| 'your_database_user',
|
|
407
|
+
password: env.get("DB_PASS") || 'your_database_password',
|
|
408
|
+
database: env.get("DB_NAME") || 'myapp_test',
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// now we can register it with our app
|
|
415
|
+
|
|
416
|
+
app.useKenx(KnexConfig)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Exmaple uses
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { DB, AppService } from "@avleon/core";
|
|
423
|
+
|
|
424
|
+
@AppService
|
|
425
|
+
export class UsersService{
|
|
426
|
+
constructor(
|
|
427
|
+
private readonly db: DB
|
|
428
|
+
){}
|
|
429
|
+
|
|
430
|
+
async findAll(){
|
|
431
|
+
const result = await this.db.client.select("*").from("users");
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## 2. Typeorm
|
|
385
438
|
Connect to databases using TypeORM:
|
|
386
439
|
|
|
387
440
|
```typescript
|
|
@@ -402,9 +455,9 @@ Or use the config class:
|
|
|
402
455
|
|
|
403
456
|
```typescript
|
|
404
457
|
// datasource.config.ts
|
|
405
|
-
import {
|
|
458
|
+
import { AppConfig, IConfig } from "@avleon/core";
|
|
406
459
|
|
|
407
|
-
@
|
|
460
|
+
@AppConfig
|
|
408
461
|
export class DataSourceConfig implements IConfig {
|
|
409
462
|
// config method is mendatory
|
|
410
463
|
// config method has access to environment variables by default
|
|
@@ -451,24 +504,53 @@ export class UserService {
|
|
|
451
504
|
}
|
|
452
505
|
```
|
|
453
506
|
|
|
454
|
-
### File Uploads
|
|
507
|
+
### File Uploads & File Storage
|
|
455
508
|
|
|
456
509
|
Handle file uploads with multipart support:
|
|
457
510
|
|
|
458
511
|
```typescript
|
|
459
512
|
// Configure multipart file uploads
|
|
460
513
|
app.useMultipart({
|
|
461
|
-
destination: path.join(process.cwd(), 'uploads'),
|
|
514
|
+
destination: path.join(process.cwd(), 'public/uploads'),
|
|
462
515
|
limits: {
|
|
463
516
|
fileSize: 5 * 1024 * 1024 // 5MB
|
|
464
517
|
}
|
|
465
518
|
});
|
|
466
|
-
|
|
519
|
+
```
|
|
520
|
+
```typescript
|
|
467
521
|
// In your controller
|
|
522
|
+
import {FileStorage} from '@avleon/core';
|
|
523
|
+
|
|
524
|
+
//inject FileStorage into constructor
|
|
525
|
+
constructor(
|
|
526
|
+
private readonly fileStorage: FileStorage
|
|
527
|
+
){}
|
|
528
|
+
|
|
529
|
+
@OpenApi({
|
|
530
|
+
description: "Uploading single file"
|
|
531
|
+
body:{
|
|
532
|
+
type:"object",
|
|
533
|
+
properties:{
|
|
534
|
+
file:{
|
|
535
|
+
type:"string",
|
|
536
|
+
format:"binary"
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
required:["file"]
|
|
540
|
+
}
|
|
541
|
+
})
|
|
468
542
|
@Post('/upload')
|
|
469
|
-
async
|
|
543
|
+
async uploadSingleFile(@UploadFile('file') file: MultipartFile) {
|
|
470
544
|
// Process uploaded file
|
|
471
|
-
|
|
545
|
+
const result = await this.fileStorage.save(file);
|
|
546
|
+
// or with new name
|
|
547
|
+
// const result = await this.fileStorage.save(file, {as:newname.ext});
|
|
548
|
+
// result
|
|
549
|
+
// {
|
|
550
|
+
// uploadPath:"/uplaod",
|
|
551
|
+
// staticPath: "/static/"
|
|
552
|
+
//}
|
|
553
|
+
return result;
|
|
472
554
|
}
|
|
473
555
|
```
|
|
474
556
|
|
|
@@ -477,6 +559,9 @@ async uploadFile(@MultipartFile() file: any) {
|
|
|
477
559
|
Serve static files:
|
|
478
560
|
|
|
479
561
|
```typescript
|
|
562
|
+
import path from 'path';
|
|
563
|
+
|
|
564
|
+
|
|
480
565
|
app.useStaticFiles({
|
|
481
566
|
path: path.join(process.cwd(), "public"),
|
|
482
567
|
prefix: "/static/",
|
|
@@ -552,7 +637,7 @@ app
|
|
|
552
637
|
// Handler function
|
|
553
638
|
})
|
|
554
639
|
.useMiddleware([AuthMiddleware])
|
|
555
|
-
.
|
|
640
|
+
.useOpenApi({
|
|
556
641
|
summary: "Get all users",
|
|
557
642
|
description: "Retrieves a list of all users",
|
|
558
643
|
tags: ["users"],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cache_1 = require("./cache");
|
|
4
|
+
describe("CacheManager (in-memory)", () => {
|
|
5
|
+
let cache;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
cache = new cache_1.CacheManager();
|
|
8
|
+
});
|
|
9
|
+
it("should set and get a value", async () => {
|
|
10
|
+
await cache.set("foo", "bar");
|
|
11
|
+
const result = await cache.get("foo");
|
|
12
|
+
expect(result).toBe("bar");
|
|
13
|
+
});
|
|
14
|
+
it("should return null for missing key", async () => {
|
|
15
|
+
const result = await cache.get("missing");
|
|
16
|
+
expect(result).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
it("should delete a key", async () => {
|
|
19
|
+
await cache.set("foo", "bar");
|
|
20
|
+
await cache.delete("foo");
|
|
21
|
+
const result = await cache.get("foo");
|
|
22
|
+
expect(result).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
it("should associate keys with tags and invalidate by tag", async () => {
|
|
25
|
+
await cache.set("a", 1, ["tag1"]);
|
|
26
|
+
await cache.set("b", 2, ["tag1", "tag2"]);
|
|
27
|
+
await cache.set("c", 3, ["tag2"]);
|
|
28
|
+
await cache.invalidateTag("tag1");
|
|
29
|
+
expect(await cache.get("a")).toBeNull();
|
|
30
|
+
expect(await cache.get("b")).toBeNull();
|
|
31
|
+
expect(await cache.get("c")).toBe(3);
|
|
32
|
+
});
|
|
33
|
+
it("should not fail when invalidating a non-existent tag", async () => {
|
|
34
|
+
await expect(cache.invalidateTag("nope")).resolves.not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
});
|
package/dist/controller.d.ts
CHANGED
package/dist/controller.js
CHANGED
|
@@ -6,10 +6,23 @@
|
|
|
6
6
|
* @url https://github.com/xtareq
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.REQUEST_METADATA_KEY = void 0;
|
|
10
|
+
exports.AvleonRequest = AvleonRequest;
|
|
9
11
|
exports.createControllerDecorator = createControllerDecorator;
|
|
10
12
|
exports.ApiController = ApiController;
|
|
11
13
|
const typedi_1 = require("typedi");
|
|
12
14
|
const container_1 = require("./container");
|
|
15
|
+
exports.REQUEST_METADATA_KEY = Symbol('avleon:request');
|
|
16
|
+
function AvleonRequest() {
|
|
17
|
+
return (target, propertyKey, parameterIndex) => {
|
|
18
|
+
const existingParams = Reflect.getMetadata(exports.REQUEST_METADATA_KEY, target, propertyKey) || [];
|
|
19
|
+
existingParams.push({
|
|
20
|
+
index: parameterIndex,
|
|
21
|
+
type: 'request',
|
|
22
|
+
});
|
|
23
|
+
Reflect.defineMetadata(exports.REQUEST_METADATA_KEY, existingParams, target, propertyKey);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
13
26
|
function createControllerDecorator(type = "web") {
|
|
14
27
|
return function (pathOrOptions, maybeOptions) {
|
|
15
28
|
return function (target) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
require("reflect-metadata");
|
|
43
|
+
const controller_1 = require("./controller");
|
|
44
|
+
const containerModule = __importStar(require("./container"));
|
|
45
|
+
const typedi_1 = require("typedi");
|
|
46
|
+
describe("Controller Decorators", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
describe("ApiController", () => {
|
|
51
|
+
it("should apply metadata and Service decorator when used as a class decorator", () => {
|
|
52
|
+
const registerControllerSpy = jest.spyOn(containerModule, "registerController");
|
|
53
|
+
let TestController = class TestController {
|
|
54
|
+
};
|
|
55
|
+
TestController = __decorate([
|
|
56
|
+
controller_1.ApiController
|
|
57
|
+
], TestController);
|
|
58
|
+
expect(Reflect.getMetadata(containerModule.API_CONTROLLER_METADATA_KEY, TestController)).toBe(true);
|
|
59
|
+
expect(registerControllerSpy).toHaveBeenCalledWith(TestController);
|
|
60
|
+
expect(Reflect.getMetadata(containerModule.CONTROLLER_META_KEY, TestController)).toEqual({
|
|
61
|
+
type: "api",
|
|
62
|
+
path: "/",
|
|
63
|
+
options: {},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("should apply metadata and Service decorator when used with a path", () => {
|
|
67
|
+
const registerControllerSpy = jest.spyOn(containerModule, "registerController");
|
|
68
|
+
let TestController = class TestController {
|
|
69
|
+
};
|
|
70
|
+
TestController = __decorate([
|
|
71
|
+
(0, controller_1.ApiController)("/test")
|
|
72
|
+
], TestController);
|
|
73
|
+
expect(Reflect.getMetadata(containerModule.API_CONTROLLER_METADATA_KEY, TestController)).toBe(true);
|
|
74
|
+
expect(registerControllerSpy).toHaveBeenCalledWith(TestController);
|
|
75
|
+
expect(Reflect.getMetadata(containerModule.CONTROLLER_META_KEY, TestController)).toEqual({
|
|
76
|
+
type: "api",
|
|
77
|
+
path: "/test",
|
|
78
|
+
options: {},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it("should apply metadata and Service decorator when used with options", () => {
|
|
82
|
+
const registerControllerSpy = jest.spyOn(containerModule, "registerController");
|
|
83
|
+
const options = { name: "Custom", path: "/custom", version: "1.0.0" };
|
|
84
|
+
let TestController = class TestController {
|
|
85
|
+
};
|
|
86
|
+
TestController = __decorate([
|
|
87
|
+
(0, controller_1.ApiController)(options)
|
|
88
|
+
], TestController);
|
|
89
|
+
expect(Reflect.getMetadata(containerModule.API_CONTROLLER_METADATA_KEY, TestController)).toBe(true);
|
|
90
|
+
expect(registerControllerSpy).toHaveBeenCalledWith(TestController);
|
|
91
|
+
expect(Reflect.getMetadata(containerModule.CONTROLLER_META_KEY, TestController)).toEqual({
|
|
92
|
+
type: "api",
|
|
93
|
+
path: "/custom",
|
|
94
|
+
options,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
const originalService = typedi_1.Service;
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
containerModule.Service = undefined;
|
|
100
|
+
expect(() => {
|
|
101
|
+
let TestController = class TestController {
|
|
102
|
+
};
|
|
103
|
+
TestController = __decorate([
|
|
104
|
+
controller_1.ApiController
|
|
105
|
+
], TestController);
|
|
106
|
+
}).toThrow("Service decorator is not a function");
|
|
107
|
+
// Restore
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
containerModule.Service = originalService;
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const environment_variables_1 = require("./environment-variables");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
jest.mock("fs");
|
|
10
|
+
const mockedFs = fs_1.default;
|
|
11
|
+
describe("Environment", () => {
|
|
12
|
+
const envFilePath = path_1.default.join(process.cwd(), ".env");
|
|
13
|
+
const envContent = "TEST_KEY=123\nANOTHER_KEY=abc";
|
|
14
|
+
const parsedEnv = { TEST_KEY: "123", ANOTHER_KEY: "abc" };
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.resetModules();
|
|
17
|
+
mockedFs.existsSync.mockClear();
|
|
18
|
+
mockedFs.readFileSync.mockClear();
|
|
19
|
+
process.env.TEST_KEY = "override";
|
|
20
|
+
process.env.ONLY_PROCESS = "proc";
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
delete process.env.TEST_KEY;
|
|
24
|
+
delete process.env.ONLY_PROCESS;
|
|
25
|
+
});
|
|
26
|
+
it("should get value from process.env if present", () => {
|
|
27
|
+
mockedFs.existsSync.mockReturnValue(true);
|
|
28
|
+
mockedFs.readFileSync.mockReturnValue(envContent);
|
|
29
|
+
const env = new environment_variables_1.Environment();
|
|
30
|
+
expect(env.get("TEST_KEY")).toBe("override");
|
|
31
|
+
});
|
|
32
|
+
it("should get value from .env file if not in process.env", () => {
|
|
33
|
+
mockedFs.existsSync.mockReturnValue(true);
|
|
34
|
+
mockedFs.readFileSync.mockReturnValue(envContent);
|
|
35
|
+
const env = new environment_variables_1.Environment();
|
|
36
|
+
expect(env.get("ANOTHER_KEY")).toBe("abc");
|
|
37
|
+
});
|
|
38
|
+
it("should return undefined for missing key", () => {
|
|
39
|
+
mockedFs.existsSync.mockReturnValue(true);
|
|
40
|
+
mockedFs.readFileSync.mockReturnValue(envContent);
|
|
41
|
+
const env = new environment_variables_1.Environment();
|
|
42
|
+
expect(env.get("MISSING_KEY")).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it("should throw EnvironmentVariableNotFound for getOrThrow if missing", () => {
|
|
45
|
+
mockedFs.existsSync.mockReturnValue(true);
|
|
46
|
+
mockedFs.readFileSync.mockReturnValue(envContent);
|
|
47
|
+
const env = new environment_variables_1.Environment();
|
|
48
|
+
expect(() => env.getOrThrow("MISSING_KEY")).toThrow();
|
|
49
|
+
});
|
|
50
|
+
it("should get all variables with process.env taking precedence", () => {
|
|
51
|
+
mockedFs.existsSync.mockReturnValue(true);
|
|
52
|
+
mockedFs.readFileSync.mockReturnValue(envContent);
|
|
53
|
+
const env = new environment_variables_1.Environment();
|
|
54
|
+
const all = env.getAll();
|
|
55
|
+
expect(all.TEST_KEY).toBe("override");
|
|
56
|
+
expect(all.ANOTHER_KEY).toBe("abc");
|
|
57
|
+
expect(all.ONLY_PROCESS).toBe("proc");
|
|
58
|
+
});
|
|
59
|
+
it("should handle missing .env file gracefully", () => {
|
|
60
|
+
mockedFs.existsSync.mockReturnValue(false);
|
|
61
|
+
const env = new environment_variables_1.Environment();
|
|
62
|
+
expect(env.get("ONLY_PROCESS")).toBe("proc");
|
|
63
|
+
expect(env.get("TEST_KEY")).toBe("override");
|
|
64
|
+
});
|
|
65
|
+
it("should return empty object if error occurs during parsing", () => {
|
|
66
|
+
mockedFs.existsSync.mockImplementation(() => { throw new Error("fs error"); });
|
|
67
|
+
const env = new environment_variables_1.Environment();
|
|
68
|
+
expect(env.getAll()).toEqual({});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -9,9 +9,11 @@ exports.HttpExceptions = exports.ForbiddenException = exports.UnauthorizedExcept
|
|
|
9
9
|
*/
|
|
10
10
|
class BaseHttpException extends Error {
|
|
11
11
|
constructor(message) {
|
|
12
|
-
|
|
12
|
+
const stringMessage = typeof message === "string" ? message : JSON.stringify(message);
|
|
13
|
+
super(stringMessage);
|
|
13
14
|
this.code = 500;
|
|
14
15
|
this.name = "HttpException";
|
|
16
|
+
this.payload = typeof message === "string" ? { message } : message;
|
|
15
17
|
}
|
|
16
18
|
isCustomException() {
|
|
17
19
|
return true;
|
package/dist/file-storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PathLike } from "fs";
|
|
1
|
+
import fs, { PathLike } from "fs";
|
|
2
2
|
import { MultipartFile } from "./multipart";
|
|
3
3
|
interface TransformOptions {
|
|
4
4
|
resize?: {
|
|
@@ -10,9 +10,9 @@ interface TransformOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export interface FileStorageInterface {
|
|
12
12
|
transform(options: TransformOptions): FileStorage;
|
|
13
|
-
save(file: MultipartFile, options?: SaveOptionsSingle): Promise<MultipartFile
|
|
14
|
-
saveAll(files: MultipartFile[], options?: SaveOptions): Promise<MultipartFile[]
|
|
15
|
-
remove(filepath:
|
|
13
|
+
save(file: MultipartFile, options?: SaveOptionsSingle): Promise<MultipartFile>;
|
|
14
|
+
saveAll(files: MultipartFile[], options?: SaveOptions): Promise<MultipartFile[]>;
|
|
15
|
+
remove(filepath: string): Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
export interface SaveOptions {
|
|
18
18
|
overwrite?: boolean;
|
|
@@ -23,12 +23,47 @@ export interface SaveOptionsSingle extends SaveOptions {
|
|
|
23
23
|
}
|
|
24
24
|
export declare class FileStorage implements FileStorageInterface {
|
|
25
25
|
private transformOptions;
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
private readonly baseDir;
|
|
27
|
+
private readonly maxFileSize;
|
|
28
|
+
constructor();
|
|
29
|
+
/**
|
|
30
|
+
* Set transformation options for the next save operation
|
|
31
|
+
*/
|
|
32
|
+
transform(options: TransformOptions): FileStorage;
|
|
33
|
+
getUploadFile(fliePath: string): Promise<Buffer<ArrayBufferLike>>;
|
|
34
|
+
download(filepath: PathLike): Promise<{
|
|
35
|
+
download: boolean;
|
|
36
|
+
stream: fs.ReadStream;
|
|
37
|
+
filename: string;
|
|
38
|
+
}>;
|
|
39
|
+
downloadAs(filepath: PathLike, filename: string): Promise<{
|
|
40
|
+
download: boolean;
|
|
41
|
+
stream: fs.ReadStream;
|
|
42
|
+
filename: string;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Save a single file with optional transformations
|
|
46
|
+
*/
|
|
47
|
+
save(f: MultipartFile, options?: SaveOptionsSingle): Promise<any>;
|
|
48
|
+
/**
|
|
49
|
+
* Save multiple files
|
|
50
|
+
*/
|
|
51
|
+
saveAll(files: MultipartFile[], options?: SaveOptions): Promise<any[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Remove a file from storage
|
|
54
|
+
*/
|
|
55
|
+
remove(filepath: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Process image with transformations using sharp
|
|
58
|
+
*/
|
|
31
59
|
private processImage;
|
|
60
|
+
/**
|
|
61
|
+
* Helper methods
|
|
62
|
+
*/
|
|
63
|
+
private isFileExists;
|
|
32
64
|
private ensureDirectoryExists;
|
|
65
|
+
private removeFileSync;
|
|
66
|
+
private isImageFile;
|
|
67
|
+
private validateFilename;
|
|
33
68
|
}
|
|
34
69
|
export {};
|