@postxl/generators 1.15.1 → 1.17.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.
- package/dist/backend-excel-io/excel-io.generator.d.ts +2 -1
- package/dist/backend-excel-io/excel-io.generator.js +2 -0
- package/dist/backend-excel-io/excel-io.generator.js.map +1 -1
- package/dist/backend-excel-io/template/excel-io.controller.ts +24 -54
- package/dist/backend-upload/index.d.ts +3 -0
- package/dist/backend-upload/index.js +40 -0
- package/dist/backend-upload/index.js.map +1 -0
- package/dist/backend-upload/template/src/index.ts +13 -0
- package/dist/backend-upload/template/src/upload.config.ts +15 -0
- package/dist/backend-upload/template/src/upload.controller.ts +53 -0
- package/dist/backend-upload/template/src/upload.guard.ts +39 -0
- package/dist/backend-upload/template/src/upload.module.ts +26 -0
- package/dist/backend-upload/template/src/upload.service.ts +333 -0
- package/dist/backend-upload/template/src/upload.types.ts +37 -0
- package/dist/backend-upload/template/src/uploaded-file.decorator.ts +12 -0
- package/dist/backend-upload/template/tsconfig.lib.json +10 -0
- package/dist/backend-upload/upload.generator.d.ts +16 -0
- package/dist/backend-upload/upload.generator.js +107 -0
- package/dist/backend-upload/upload.generator.js.map +1 -0
- package/dist/base/template/.claude/commands/README.md +65 -0
- package/dist/base/template/.claude/settings.json +3 -1
- package/dist/base/template/.github/.copilot-prompts.json +22 -0
- package/dist/e2e/e2e.generator.js +43 -1
- package/dist/e2e/e2e.generator.js.map +1 -1
- package/dist/e2e/template/.claude/commands/prepare-e2e-tests.md +251 -0
- package/dist/e2e/template/.claude/commands/run-e2e-tests.md +221 -0
- package/dist/e2e/template/scripts/e2e.sh +398 -0
- package/dist/frontend-forms/generators/model/forms.generator.js +191 -0
- package/dist/frontend-forms/generators/model/forms.generator.js.map +1 -1
- package/dist/frontend-tables/generators/model-table.generator.js +16 -2
- package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
- package/dist/generators.js +2 -0
- package/dist/generators.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gDAAiC;AAEjC,6DAA8C;AAC9C,iDAAsC;AAEtC,wDAA2E;AAC3E,kDAA+E;AAC/E,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,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
|
+
{"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,
|
|
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
|
|
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
|
-
@
|
|
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:
|
|
88
|
-
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
|
-
@
|
|
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:
|
|
118
|
-
filename:
|
|
113
|
+
data: uploadData.buffer,
|
|
114
|
+
filename: uploadData.filename,
|
|
119
115
|
includeDetailedUnchangedRecords: parseMultipartBooleanField(
|
|
120
|
-
|
|
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,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
|
+
}
|