@postxl/generators 1.15.1 → 1.16.0

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.
Files changed (29) hide show
  1. package/dist/backend-excel-io/excel-io.generator.d.ts +2 -1
  2. package/dist/backend-excel-io/excel-io.generator.js +2 -0
  3. package/dist/backend-excel-io/excel-io.generator.js.map +1 -1
  4. package/dist/backend-excel-io/template/excel-io.controller.ts +24 -54
  5. package/dist/backend-upload/index.d.ts +3 -0
  6. package/dist/backend-upload/index.js +40 -0
  7. package/dist/backend-upload/index.js.map +1 -0
  8. package/dist/backend-upload/template/src/index.ts +13 -0
  9. package/dist/backend-upload/template/src/upload.config.ts +15 -0
  10. package/dist/backend-upload/template/src/upload.controller.ts +53 -0
  11. package/dist/backend-upload/template/src/upload.guard.ts +39 -0
  12. package/dist/backend-upload/template/src/upload.module.ts +26 -0
  13. package/dist/backend-upload/template/src/upload.service.ts +333 -0
  14. package/dist/backend-upload/template/src/upload.types.ts +37 -0
  15. package/dist/backend-upload/template/src/uploaded-file.decorator.ts +12 -0
  16. package/dist/backend-upload/template/tsconfig.lib.json +10 -0
  17. package/dist/backend-upload/upload.generator.d.ts +16 -0
  18. package/dist/backend-upload/upload.generator.js +107 -0
  19. package/dist/backend-upload/upload.generator.js.map +1 -0
  20. package/dist/frontend-forms/generators/model/forms.generator.js +191 -0
  21. package/dist/frontend-forms/generators/model/forms.generator.js.map +1 -1
  22. package/dist/frontend-tables/generators/model-table.generator.js +16 -2
  23. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  24. package/dist/generators.js +2 -0
  25. package/dist/generators.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +5 -2
  28. package/dist/index.js.map +1 -1
  29. package/package.json +4 -4
@@ -1,10 +1,11 @@
1
1
  import * as Generator from '@postxl/generator';
2
2
  import { WithActions } from '../backend-actions';
3
3
  import { WithBackend } from '../backend-core';
4
+ import { WithUpload } from '../backend-upload';
4
5
  import { WithView } from '../backend-view';
5
6
  import { WithDecoders } from '../decoders';
6
7
  import { WithTypes } from '../types';
7
- type ContextRequirements = WithView<WithTypes<WithDecoders<WithActions<WithBackend<Generator.Context>>>>>;
8
+ type ContextRequirements = WithUpload<WithView<WithTypes<WithDecoders<WithActions<WithBackend<Generator.Context>>>>>>;
8
9
  export type ContextResult = WithExcelIo<ContextRequirements>;
9
10
  export type WithExcelIo<Context> = Generator.ExtendContext<Context, {
10
11
  excelIo: ExcelIoContext;
@@ -39,6 +39,7 @@ const Generator = __importStar(require("@postxl/generator"));
39
39
  const generator_1 = require("@postxl/generator");
40
40
  const backend_actions_1 = require("../backend-actions");
41
41
  const backend_core_1 = require("../backend-core");
42
+ const backend_upload_1 = require("../backend-upload");
42
43
  const backend_view_1 = require("../backend-view");
43
44
  const decoders_1 = require("../decoders");
44
45
  const types_1 = require("../types");
@@ -50,6 +51,7 @@ exports.generator = {
50
51
  backend_core_1.backendGeneratorId,
51
52
  backend_actions_1.backendActionsGeneratorId,
52
53
  backend_view_1.backendViewGeneratorId,
54
+ backend_upload_1.backendUploadGeneratorId,
53
55
  decoders_1.decodersGeneratorId,
54
56
  types_1.typesGeneratorId,
55
57
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"excel-io.generator.js","sourceRoot":"","sources":["../../src/backend-excel-io/excel-io.generator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"excel-io.generator.js","sourceRoot":"","sources":["../../src/backend-excel-io/excel-io.generator.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gDAAiC;AAEjC,6DAA8C;AAC9C,iDAAsC;AAEtC,wDAA2E;AAC3E,kDAA+E;AAC/E,sDAAwE;AACxE,kDAAkE;AAClE,0CAA+D;AAC/D,oCAAsD;AAEtD,wFAAgF;AAYnE,QAAA,WAAW,GAAG,SAAS,CAAC,sBAAsB,CAAC,kBAAkB,CAAC,CAAA;AAElE,QAAA,SAAS,GAAiC;IACrD,EAAE,EAAE,mBAAW;IACf,QAAQ,EAAE;QACR,iCAAkB;QAClB,2CAAyB;QACzB,qCAAsB;QACtB,yCAAwB;QACxB,8BAAmB;QACnB,wBAAgB;KACjB;IAED,QAAQ,EAAE,CAAsC,OAAgB,EAAiB,EAAE;QACjF,MAAM,MAAM,GAA8B;YACxC,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,eAAe,CAAC;YAC5C,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,2BAA2B,CAAC;SACzE,CAAA;QAED,MAAM,aAAa,GAAiB;YAClC,IAAI,EAAE,SAAS,CAAC,mBAAmB,CAAC,UAAU,CAAC;YAC/C,WAAW,EAAE,MAAM;YACnB,qBAAqB,EAAE;gBACrB,IAAI,EAAE,IAAA,cAAE,EAAC,eAAe,CAAC;aAC1B;SACF,CAAA;QAED,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QAE3C,MAAM,OAAO,GAA8B;YACzC,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,gBAAgB,CAAC;YAC7C,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,4BAA4B,CAAC;SAC1E,CAAA;QAED,MAAM,UAAU,GAA8B;YAC5C,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,mBAAmB,CAAC;YAChD,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,+BAA+B,CAAC;SAC7E,CAAA;QAED,MAAM,OAAO,GAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,CAAA;QAE/D,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAA;IAChC,CAAC;IAED,QAAQ,EAAE,KAAK,EAAiC,OAAgB,EAAoB,EAAE;QACpF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,iBAAiB,EAAE,CAAA;QAEhD,MAAM,MAAM,CAAC,QAAQ,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mCAAmC,CAAC;YACtE,UAAU,EAAE,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;SAC9D,CAAC,CAAA;QACF,MAAM,MAAM,CAAC,QAAQ,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,+BAA+B,CAAC;YAClE,UAAU,EAAE,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;SAC1D,CAAC,CAAA;QACF,MAAM,MAAM,CAAC,QAAQ,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,sBAAsB,CAAC;YACzD,UAAU,EAAE,WAAW;SACxB,CAAC,CAAA;QAEF,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAA,mDAAsB,EAAC,OAAO,CAAC,CAAC,CAAA;QAE7F,MAAM,CAAC,KAAK,CACV,UAAU,EACV;;;CAGL,CACI,CAAA;QAED,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,iBAAiB,EAAE,CAAA;QAC7C,GAAG,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;QAErD,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,SAAS,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAA;QAEvE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAA;QAEvE,OAAO,OAAO,CAAA;IAChB,CAAC;CACF,CAAA"}
@@ -1,8 +1,11 @@
1
1
  import { AuthGuard, type Viewer } from '@authentication/auth.guard'
2
2
  import { ViewerParam } from '@authentication/viewer.decorator'
3
- import { Controller, Get, Logger, Param, Post, Query, Req, Res, UseGuards } from '@nestjs/common'
3
+ import { Controller, Get, Logger, Param, Post, Query, Res, UseGuards } from '@nestjs/common'
4
+ import { UploadGuard } from '@upload/upload.guard'
5
+ import { UploadedFileData } from '@upload/uploaded-file.decorator'
6
+ import type { UploadedFileDataPayload } from '@upload/upload.types'
4
7
 
5
- import { FastifyReply, FastifyRequest } from 'fastify'
8
+ import { FastifyReply } from 'fastify'
6
9
 
7
10
  import { ExcelIoService } from './excel-io.service'
8
11
 
@@ -68,24 +71,21 @@ export class ExcelIoController {
68
71
  }
69
72
 
70
73
  @Post('import')
74
+ @UseGuards(
75
+ UploadGuard({
76
+ maxFileSizeBytes: MAX_IMPORT_FILE_SIZE_BYTES,
77
+ allowedFileTypes: ['excel'],
78
+ }),
79
+ )
71
80
  async importExcel(
72
81
  @ViewerParam() viewer: Viewer,
73
- @Req() req: FastifyRequest,
82
+ @UploadedFileData() uploadData: UploadedFileDataPayload,
74
83
  @Res({ passthrough: true }) res: FastifyReply,
75
84
  ): Promise<void> {
76
85
  try {
77
- const data = await req.file()
78
- if (!data) {
79
- throw new Error('No file uploaded')
80
- }
81
-
82
- const fileBuffer = await toBufferWithLimit(data.file, MAX_IMPORT_FILE_SIZE_BYTES)
83
- if (fileBuffer.length === 0) {
84
- throw new Error('Uploaded file is empty')
85
- }
86
86
  const result = await this.excelIoService.importExcel({
87
- data: fileBuffer,
88
- filename: data.filename,
87
+ data: uploadData.buffer,
88
+ filename: uploadData.filename,
89
89
  user: viewer.user,
90
90
  })
91
91
 
@@ -97,27 +97,23 @@ export class ExcelIoController {
97
97
  }
98
98
 
99
99
  @Post('import/preview')
100
+ @UseGuards(
101
+ UploadGuard({
102
+ maxFileSizeBytes: MAX_IMPORT_FILE_SIZE_BYTES,
103
+ allowedFileTypes: ['excel'],
104
+ }),
105
+ )
100
106
  async previewImportExcel(
101
107
  @ViewerParam() _viewer: Viewer,
102
- @Req() req: FastifyRequest,
108
+ @UploadedFileData() uploadData: UploadedFileDataPayload,
103
109
  @Res({ passthrough: true }) res: FastifyReply,
104
110
  ): Promise<void> {
105
111
  try {
106
- const data = await req.file()
107
- if (!data) {
108
- throw new Error('No file uploaded')
109
- }
110
-
111
- const fileBuffer = await toBufferWithLimit(data.file, MAX_IMPORT_FILE_SIZE_BYTES)
112
- if (fileBuffer.length === 0) {
113
- throw new Error('Uploaded file is empty')
114
- }
115
-
116
112
  const result = await this.excelIoService.previewImportExcel({
117
- data: fileBuffer,
118
- filename: data.filename,
113
+ data: uploadData.buffer,
114
+ filename: uploadData.filename,
119
115
  includeDetailedUnchangedRecords: parseMultipartBooleanField(
120
- data.fields,
116
+ uploadData.fields,
121
117
  'includeDetailedUnchangedRecords',
122
118
  false,
123
119
  ),
@@ -131,32 +127,6 @@ export class ExcelIoController {
131
127
  }
132
128
  }
133
129
 
134
- async function toBufferWithLimit(stream: AsyncIterable<unknown>, maxBytes: number): Promise<Buffer> {
135
- const chunks: Buffer[] = []
136
- let size = 0
137
-
138
- for await (const chunk of stream) {
139
- const buffer = toBuffer(chunk)
140
- size += buffer.length
141
- if (size > maxBytes) {
142
- throw new Error(`Uploaded file exceeds ${Math.floor(maxBytes / (1024 * 1024))} MB limit`)
143
- }
144
- chunks.push(buffer)
145
- }
146
-
147
- return Buffer.concat(chunks)
148
- }
149
-
150
- function toBuffer(chunk: unknown): Buffer {
151
- if (Buffer.isBuffer(chunk)) {
152
- return chunk
153
- }
154
- if (chunk instanceof Uint8Array) {
155
- return Buffer.from(chunk)
156
- }
157
- return Buffer.from(String(chunk))
158
- }
159
-
160
130
  function parseJsonQuery(value?: string): unknown {
161
131
  if (!value) {
162
132
  return undefined
@@ -0,0 +1,3 @@
1
+ export declare const backendUploadGenerator: import("@postxl/generator").GeneratorInterface;
2
+ export declare const backendUploadGeneratorId: string & import("zod").$brand<"PXL.GeneratorInterfaceId">;
3
+ export type { WithUpload } from './upload.generator';
@@ -0,0 +1,40 @@
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 __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.backendUploadGeneratorId = exports.backendUploadGenerator = void 0;
37
+ const Generator = __importStar(require("./upload.generator"));
38
+ exports.backendUploadGenerator = Generator.generator;
39
+ exports.backendUploadGeneratorId = Generator.generatorId;
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend-upload/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAElC,QAAA,sBAAsB,GAAG,SAAS,CAAC,SAAS,CAAA;AAC5C,QAAA,wBAAwB,GAAG,SAAS,CAAC,WAAW,CAAA"}
@@ -0,0 +1,13 @@
1
+ export { UploadModule } from './upload.module'
2
+ export { UploadService } from './upload.service'
3
+ export { UploadController } from './upload.controller'
4
+ export { UploadConfig } from './upload.config'
5
+ export { UploadGuard } from './upload.guard'
6
+ export { UploadedFileData } from './uploaded-file.decorator'
7
+ export type {
8
+ UploadAllowedFileType,
9
+ UploadGuardOptions,
10
+ UploadRequest,
11
+ UploadStorageMode,
12
+ UploadedFileDataPayload,
13
+ } from './upload.types'
@@ -0,0 +1,15 @@
1
+ import { Injectable } from '@nestjs/common'
2
+
3
+ import type { UploadStorageMode } from './upload.types'
4
+
5
+ export type UploadConfigValues = Readonly<{
6
+ storage: UploadStorageMode
7
+ localDirectory: string
8
+ dryRun: boolean
9
+ maxSizeBytes: number
10
+ }>
11
+
12
+ @Injectable()
13
+ export class UploadConfig {
14
+ constructor(public readonly values: UploadConfigValues) {}
15
+ }
@@ -0,0 +1,53 @@
1
+ import { AuthGuard, type Viewer } from '@authentication/auth.guard'
2
+ import { ViewerParam } from '@authentication/viewer.decorator'
3
+ import { Controller, Get, Param, Post, Res, UseGuards } from '@nestjs/common'
4
+ import type { FileId } from '@types'
5
+ import { toFileId } from '@types'
6
+
7
+ import { FastifyReply } from 'fastify'
8
+
9
+ import { UploadedFileData } from './uploaded-file.decorator'
10
+ import { UploadGuard } from './upload.guard'
11
+ import { UploadService } from './upload.service'
12
+ import type { UploadedFileDataPayload } from './upload.types'
13
+
14
+ @UseGuards(AuthGuard)
15
+ @Controller('upload')
16
+ export class UploadController {
17
+ constructor(private readonly uploadService: UploadService) {}
18
+
19
+ @UseGuards(
20
+ UploadGuard({
21
+ maxFileSizeBytes: 30 * 1024 * 1024,
22
+ }),
23
+ )
24
+ @Post('')
25
+ upload(@UploadedFileData() uploadData: UploadedFileDataPayload) {
26
+ return {
27
+ success: true,
28
+ result: {
29
+ file: uploadData.file,
30
+ },
31
+ }
32
+ }
33
+
34
+ @Get(':fileId/download')
35
+ async downloadById(
36
+ @Param('fileId') fileIdRaw: string,
37
+ @ViewerParam() viewer: Viewer,
38
+ @Res({ passthrough: true }) res: FastifyReply,
39
+ ): Promise<void> {
40
+ const fileId: FileId = toFileId(fileIdRaw)
41
+ const file = await this.uploadService.getStoredFileRecord({ fileId, user: viewer.user })
42
+ const buffer = await this.uploadService.getBufferFromFileRecord({ fileId, location: file.url })
43
+
44
+ const filename = file.name || `file-${fileId}`
45
+ const asciiFilename = filename.replaceAll(/[^A-Za-z0-9._-]/g, '_')
46
+ const encodedFilename = encodeURIComponent(filename)
47
+
48
+ res.type(file.mimetype || 'application/octet-stream')
49
+ res.header('Content-Disposition', `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`)
50
+ res.header('Access-Control-Expose-Headers', 'Content-Disposition')
51
+ res.send(buffer)
52
+ }
53
+ }
@@ -0,0 +1,39 @@
1
+ import { BadRequestException, CanActivate, ExecutionContext, Injectable, Type, mixin } from '@nestjs/common'
2
+
3
+ import { DEFAULT_MAX_SIZE_BYTES, type UploadGuardOptions, type UploadRequest } from './upload.types'
4
+ import { UploadService } from './upload.service'
5
+
6
+ export function UploadGuard(options?: UploadGuardOptions): Type<CanActivate> {
7
+ @Injectable()
8
+ class UploadGuardMixin implements CanActivate {
9
+ constructor(private readonly uploadService: UploadService) {}
10
+
11
+ async canActivate(context: ExecutionContext): Promise<boolean> {
12
+ const req = context.switchToHttp().getRequest<UploadRequest>()
13
+ if (!req.isMultipart?.()) {
14
+ throw new BadRequestException("Request must use 'multipart/form-data'")
15
+ }
16
+
17
+ const multipartFile = await req.file({
18
+ limits: {
19
+ fileSize: options?.maxFileSizeBytes ?? DEFAULT_MAX_SIZE_BYTES,
20
+ },
21
+ })
22
+
23
+ if (!multipartFile) {
24
+ throw new BadRequestException('No file uploaded')
25
+ }
26
+
27
+ const user = req.viewer?.user ?? this.uploadService.getRootUser()
28
+ req.uploadedFileData = await this.uploadService.processUpload({
29
+ multipartFile,
30
+ user,
31
+ options,
32
+ })
33
+
34
+ return true
35
+ }
36
+ }
37
+
38
+ return mixin(UploadGuardMixin)
39
+ }
@@ -0,0 +1,26 @@
1
+ import { DynamicModule } from '@nestjs/common'
2
+ import { ViewModule } from '@view/view.module'
3
+
4
+ import type { UploadConfigValues } from './upload.config'
5
+ import { UploadConfig } from './upload.config'
6
+ import { UploadController } from './upload.controller'
7
+ import { UploadService } from './upload.service'
8
+
9
+ export class UploadModule {
10
+ static forRoot(config: UploadConfigValues): DynamicModule {
11
+ return {
12
+ module: UploadModule,
13
+ imports: [ViewModule],
14
+ providers: [
15
+ UploadService,
16
+ {
17
+ provide: UploadConfig,
18
+ useValue: new UploadConfig(config),
19
+ },
20
+ ],
21
+ controllers: [UploadController],
22
+ exports: [UploadService],
23
+ global: true,
24
+ }
25
+ }
26
+ }