@modern-admin/feature-upload 0.1.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/graphql.d.ts +36 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +141 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.d.ts +3 -0
- package/dist/nest/index.d.ts.map +1 -0
- package/dist/nest/index.js +4 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/upload-sweeper.service.d.ts +32 -0
- package/dist/nest/upload-sweeper.service.d.ts.map +1 -0
- package/dist/nest/upload-sweeper.service.js +68 -0
- package/dist/nest/upload-sweeper.service.js.map +1 -0
- package/dist/nest/upload.controller.d.ts +51 -0
- package/dist/nest/upload.controller.d.ts.map +1 -0
- package/dist/nest/upload.controller.js +220 -0
- package/dist/nest/upload.controller.js.map +1 -0
- package/dist/nest/upload.module.d.ts +28 -0
- package/dist/nest/upload.module.d.ts.map +1 -0
- package/dist/nest/upload.module.js +51 -0
- package/dist/nest/upload.module.js.map +1 -0
- package/dist/nest/upload.tokens.d.ts +23 -0
- package/dist/nest/upload.tokens.d.ts.map +1 -0
- package/dist/nest/upload.tokens.js +6 -0
- package/dist/nest/upload.tokens.js.map +1 -0
- package/dist/pending-registry.d.ts +60 -0
- package/dist/pending-registry.d.ts.map +1 -0
- package/dist/pending-registry.js +106 -0
- package/dist/pending-registry.js.map +1 -0
- package/dist/providers/local.d.ts +36 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +44 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/s3.d.ts +96 -0
- package/dist/providers/s3.d.ts.map +1 -0
- package/dist/providers/s3.js +164 -0
- package/dist/providers/s3.js.map +1 -0
- package/dist/registry.d.ts +27 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +25 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-feature.d.ts +31 -0
- package/dist/upload-feature.d.ts.map +1 -0
- package/dist/upload-feature.js +170 -0
- package/dist/upload-feature.js.map +1 -0
- package/package.json +94 -0
- package/src/graphql.ts +185 -0
- package/src/index.ts +22 -0
- package/src/nest/index.ts +4 -0
- package/src/nest/upload-sweeper.service.ts +52 -0
- package/src/nest/upload.controller.ts +228 -0
- package/src/nest/upload.module.ts +44 -0
- package/src/nest/upload.tokens.ts +24 -0
- package/src/pending-registry.ts +115 -0
- package/src/providers/local.ts +63 -0
- package/src/providers/s3.ts +226 -0
- package/src/registry.ts +39 -0
- package/src/types.ts +137 -0
- package/src/upload-feature.ts +208 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL extension for the upload feature — mirrors the REST upload
|
|
3
|
+
* controller (`POST /admin/api/resources/:id/actions/upload` and `DELETE`)
|
|
4
|
+
* with two mutations:
|
|
5
|
+
*
|
|
6
|
+
* - `adminUpload(resourceId, field, file)` — upload a single file via the
|
|
7
|
+
* `Upload` scalar (multipart/form-data per the GraphQL multipart spec).
|
|
8
|
+
* Returns `UploadedFileInfo`.
|
|
9
|
+
* - `adminCancelUpload(resourceId, field, key)` — cancel a still-pending
|
|
10
|
+
* upload (file uploaded but record not yet saved). Returns `Boolean`.
|
|
11
|
+
*
|
|
12
|
+
* The extension is wired in the host application:
|
|
13
|
+
* ```ts
|
|
14
|
+
* ModernAdminGraphqlModule.forRoot({
|
|
15
|
+
* extensions: [uploadGraphqlExtension()],
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Authorisation re-uses the same `ModernAdmin.invoke()` access checks via
|
|
20
|
+
* `findResource(resourceId, currentAdmin)`.
|
|
21
|
+
*
|
|
22
|
+
* `@modern-admin/graphql` is an *optional peer dependency* of feature-upload —
|
|
23
|
+
* if you do not use the GraphQL transport you do not need to import this
|
|
24
|
+
* module at all.
|
|
25
|
+
*/
|
|
26
|
+
import { type ExtensionContext, type GraphqlSchemaExtension } from '@modern-admin/graphql';
|
|
27
|
+
export interface UploadGraphqlExtensionOptions {
|
|
28
|
+
/**
|
|
29
|
+
* TTL applied when registering an uploaded key as pending. Mirrors the
|
|
30
|
+
* `pendingTtlMs` option on `ModernAdminUploadModule.forRoot()` — set both
|
|
31
|
+
* to the same value to keep REST and GraphQL behaviour identical.
|
|
32
|
+
*/
|
|
33
|
+
pendingTtlMs?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function uploadGraphqlExtension(options?: UploadGraphqlExtensionOptions): (ctx: ExtensionContext) => GraphqlSchemaExtension;
|
|
36
|
+
//# sourceMappingURL=graphql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AASH,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAE5B,MAAM,uBAAuB,CAAA;AAS9B,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,6BAAkC,GAC1C,CAAC,GAAG,EAAE,gBAAgB,KAAK,sBAAsB,CAwFnD"}
|
package/dist/graphql.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL extension for the upload feature — mirrors the REST upload
|
|
3
|
+
* controller (`POST /admin/api/resources/:id/actions/upload` and `DELETE`)
|
|
4
|
+
* with two mutations:
|
|
5
|
+
*
|
|
6
|
+
* - `adminUpload(resourceId, field, file)` — upload a single file via the
|
|
7
|
+
* `Upload` scalar (multipart/form-data per the GraphQL multipart spec).
|
|
8
|
+
* Returns `UploadedFileInfo`.
|
|
9
|
+
* - `adminCancelUpload(resourceId, field, key)` — cancel a still-pending
|
|
10
|
+
* upload (file uploaded but record not yet saved). Returns `Boolean`.
|
|
11
|
+
*
|
|
12
|
+
* The extension is wired in the host application:
|
|
13
|
+
* ```ts
|
|
14
|
+
* ModernAdminGraphqlModule.forRoot({
|
|
15
|
+
* extensions: [uploadGraphqlExtension()],
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Authorisation re-uses the same `ModernAdmin.invoke()` access checks via
|
|
20
|
+
* `findResource(resourceId, currentAdmin)`.
|
|
21
|
+
*
|
|
22
|
+
* `@modern-admin/graphql` is an *optional peer dependency* of feature-upload —
|
|
23
|
+
* if you do not use the GraphQL transport you do not need to import this
|
|
24
|
+
* module at all.
|
|
25
|
+
*/
|
|
26
|
+
import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType, GraphQLString, GraphQLInt, } from 'graphql';
|
|
27
|
+
import { ForbiddenError, ResourceNotFoundError } from '@modern-admin/core';
|
|
28
|
+
import { UploadProviderRegistry } from './registry.js';
|
|
29
|
+
import { PendingUploadsRegistry } from './pending-registry.js';
|
|
30
|
+
/** Default TTL for pending upload entries, mirrored from the controller. */
|
|
31
|
+
const DEFAULT_PENDING_TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
export function uploadGraphqlExtension(options = {}) {
|
|
33
|
+
const ttlMs = options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
34
|
+
return ({ Upload }) => {
|
|
35
|
+
const UploadedFileInfoType = new GraphQLObjectType({
|
|
36
|
+
name: 'UploadedFileInfo',
|
|
37
|
+
description: 'Metadata returned for one successfully uploaded file.',
|
|
38
|
+
fields: () => ({
|
|
39
|
+
key: { type: new GraphQLNonNull(GraphQLString) },
|
|
40
|
+
url: { type: new GraphQLNonNull(GraphQLString) },
|
|
41
|
+
name: { type: new GraphQLNonNull(GraphQLString) },
|
|
42
|
+
size: { type: new GraphQLNonNull(GraphQLInt) },
|
|
43
|
+
mimeType: { type: new GraphQLNonNull(GraphQLString) },
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
name: 'feature-upload',
|
|
48
|
+
types: [UploadedFileInfoType],
|
|
49
|
+
mutations: {
|
|
50
|
+
adminUpload: {
|
|
51
|
+
type: new GraphQLNonNull(UploadedFileInfoType),
|
|
52
|
+
description: 'Upload a single file for an upload-enabled property. Multi-file ' +
|
|
53
|
+
'properties accept one mutation invocation per file.',
|
|
54
|
+
args: {
|
|
55
|
+
resourceId: { type: new GraphQLNonNull(GraphQLString) },
|
|
56
|
+
field: { type: new GraphQLNonNull(GraphQLString) },
|
|
57
|
+
file: { type: new GraphQLNonNull(Upload) },
|
|
58
|
+
},
|
|
59
|
+
async resolve(_src, args, ctx) {
|
|
60
|
+
const { resourceId, field } = args;
|
|
61
|
+
const { providerId, registered } = resolveProperty(ctx, resourceId, field);
|
|
62
|
+
const upload = args.file;
|
|
63
|
+
const computedKey = registered.uploadPath
|
|
64
|
+
? registered.uploadPath(upload.filename)
|
|
65
|
+
: undefined;
|
|
66
|
+
const key = await registered.provider.upload({
|
|
67
|
+
originalName: upload.filename,
|
|
68
|
+
mimeType: upload.mimeType,
|
|
69
|
+
size: upload.size,
|
|
70
|
+
buffer: upload.buffer,
|
|
71
|
+
}, computedKey);
|
|
72
|
+
PendingUploadsRegistry.track(key, providerId, ttlMs);
|
|
73
|
+
const url = await registered.provider.getUrl(key);
|
|
74
|
+
const info = {
|
|
75
|
+
key,
|
|
76
|
+
url,
|
|
77
|
+
name: upload.filename,
|
|
78
|
+
size: upload.size,
|
|
79
|
+
mimeType: upload.mimeType,
|
|
80
|
+
};
|
|
81
|
+
return info;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
adminCancelUpload: {
|
|
85
|
+
type: new GraphQLNonNull(GraphQLBoolean),
|
|
86
|
+
description: 'Cancel a still-pending upload (file uploaded but record not yet ' +
|
|
87
|
+
'saved). Returns false when the key is no longer pending.',
|
|
88
|
+
args: {
|
|
89
|
+
resourceId: { type: new GraphQLNonNull(GraphQLString) },
|
|
90
|
+
field: { type: new GraphQLNonNull(GraphQLString) },
|
|
91
|
+
key: { type: new GraphQLNonNull(GraphQLString) },
|
|
92
|
+
},
|
|
93
|
+
async resolve(_src, args, ctx) {
|
|
94
|
+
const { resourceId, field, key } = args;
|
|
95
|
+
// Validate the resource/field exists & is upload-enabled. Mirrors
|
|
96
|
+
// the REST controller's check — prevents leaking arbitrary
|
|
97
|
+
// resource ids to authenticated clients.
|
|
98
|
+
resolveProperty(ctx, resourceId, field);
|
|
99
|
+
return PendingUploadsRegistry.cancel(key);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the resource + property and ensure it is wired to an upload
|
|
108
|
+
* provider. Mirrors the private helper in the REST upload controller — kept
|
|
109
|
+
* private to this module to avoid coupling. Throws GraphQL-friendly Error
|
|
110
|
+
* objects (not Nest exceptions) so they surface as standard GraphQL errors.
|
|
111
|
+
*/
|
|
112
|
+
function resolveProperty(ctx, resourceId, field) {
|
|
113
|
+
let resource;
|
|
114
|
+
try {
|
|
115
|
+
resource = ctx.admin.findResource(resourceId);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof ResourceNotFoundError)
|
|
119
|
+
throw new Error(err.message);
|
|
120
|
+
if (err instanceof ForbiddenError)
|
|
121
|
+
throw new Error(err.message);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
const decorator = resource.decorate();
|
|
125
|
+
const prop = decorator.getPropertyByKey(field);
|
|
126
|
+
if (!prop) {
|
|
127
|
+
throw new Error(`Property "${field}" not found on resource "${resourceId}"`);
|
|
128
|
+
}
|
|
129
|
+
const propJson = prop.toJSON();
|
|
130
|
+
const providerId = propJson.custom?.uploadProviderId;
|
|
131
|
+
if (!providerId) {
|
|
132
|
+
throw new Error(`Property "${field}" on resource "${resourceId}" is not configured for upload. ` +
|
|
133
|
+
'Apply uploadFeature() to the resource.');
|
|
134
|
+
}
|
|
135
|
+
const registered = UploadProviderRegistry.get(providerId);
|
|
136
|
+
if (!registered) {
|
|
137
|
+
throw new Error(`Upload provider "${providerId}" is not registered.`);
|
|
138
|
+
}
|
|
139
|
+
return { providerId, registered };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=graphql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.js","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { uploadFeature } from './upload-feature.js';
|
|
2
|
+
export { UploadProviderRegistry } from './registry.js';
|
|
3
|
+
export { PendingUploadsRegistry } from './pending-registry.js';
|
|
4
|
+
export { LocalUploadProvider, type LocalUploadOptions } from './providers/local.js';
|
|
5
|
+
export { S3UploadProvider, type S3UploadOptions } from './providers/s3.js';
|
|
6
|
+
export type { IUploadProvider, UploadedFile, UploadedFileInfo, UploadFeatureOptions, UploadPropertyConfig, } from './types.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAG9D,OAAO,EAAE,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACnF,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAG1E,YAAY,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @modern-admin/feature-upload — file upload plugin for modern-admin resources.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// import { uploadFeature, LocalUploadProvider, S3UploadProvider } from '@modern-admin/feature-upload'
|
|
5
|
+
// import { ModernAdminUploadModule } from '@modern-admin/feature-upload/nest'
|
|
6
|
+
export { uploadFeature } from './upload-feature.js';
|
|
7
|
+
export { UploadProviderRegistry } from './registry.js';
|
|
8
|
+
export { PendingUploadsRegistry } from './pending-registry.js';
|
|
9
|
+
// Built-in providers
|
|
10
|
+
export { LocalUploadProvider } from './providers/local.js';
|
|
11
|
+
export { S3UploadProvider } from './providers/s3.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,SAAS;AACT,wGAAwG;AACxG,gFAAgF;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAE9D,qBAAqB;AACrB,OAAO,EAAE,mBAAmB,EAA2B,MAAM,sBAAsB,CAAA;AACnF,OAAO,EAAE,gBAAgB,EAAwB,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nest/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,KAAK,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/nest/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,OAAO,EAAE,uBAAuB,EAAuC,MAAM,oBAAoB,CAAA;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadSweeperService — periodic background task that purges expired entries
|
|
3
|
+
* from `PendingUploadsRegistry`.
|
|
4
|
+
*
|
|
5
|
+
* The interval starts when Nest calls `onModuleInit` and stops on
|
|
6
|
+
* `onModuleDestroy`. Set `sweepIntervalMs: 0` in the module options to
|
|
7
|
+
* disable scheduling (useful for tests that drive `sweep()` manually).
|
|
8
|
+
*
|
|
9
|
+
* TODO(roadmap): replace the in-process `setInterval` driver with a BullMQ
|
|
10
|
+
* (Redis-backed) job queue. Reasons:
|
|
11
|
+
* - the registry will move to Redis in the multi-instance deployment story,
|
|
12
|
+
* and the sweeper job belongs in the same place;
|
|
13
|
+
* - BullMQ gives us cron scheduling, retries on transient storage errors,
|
|
14
|
+
* visibility/metrics through Bull Board, and crash safety (a process
|
|
15
|
+
* restart resumes scheduled sweeps instead of forgetting them);
|
|
16
|
+
* - per-key cancel can become a delayed job (`removeJob` on confirm) which
|
|
17
|
+
* is more efficient than a periodic full-table scan once we have many
|
|
18
|
+
* pending entries.
|
|
19
|
+
* The migration should keep the same `PendingUploadsRegistry` API surface so
|
|
20
|
+
* call sites (controller, hooks) do not need to change — only the storage
|
|
21
|
+
* (Map → Redis) and the sweeper driver (setInterval → BullMQ Worker) flip.
|
|
22
|
+
*/
|
|
23
|
+
import { type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
|
|
24
|
+
import { type ModernAdminUploadModuleOptions } from './upload.tokens.js';
|
|
25
|
+
export declare class UploadSweeperService implements OnModuleInit, OnModuleDestroy {
|
|
26
|
+
private readonly options;
|
|
27
|
+
private timer;
|
|
28
|
+
constructor(options: ModernAdminUploadModuleOptions);
|
|
29
|
+
onModuleInit(): void;
|
|
30
|
+
onModuleDestroy(): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=upload-sweeper.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-sweeper.service.d.ts","sourceRoot":"","sources":["../../src/nest/upload-sweeper.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAsB,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE5F,OAAO,EAAyB,KAAK,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAI/F,qBACa,oBAAqB,YAAW,YAAY,EAAE,eAAe;IAG7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFnE,OAAO,CAAC,KAAK,CAA8C;gBAEC,OAAO,EAAE,8BAA8B;IAEnG,YAAY,IAAI,IAAI;IAUpB,eAAe,IAAI,IAAI;CAMxB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadSweeperService — periodic background task that purges expired entries
|
|
3
|
+
* from `PendingUploadsRegistry`.
|
|
4
|
+
*
|
|
5
|
+
* The interval starts when Nest calls `onModuleInit` and stops on
|
|
6
|
+
* `onModuleDestroy`. Set `sweepIntervalMs: 0` in the module options to
|
|
7
|
+
* disable scheduling (useful for tests that drive `sweep()` manually).
|
|
8
|
+
*
|
|
9
|
+
* TODO(roadmap): replace the in-process `setInterval` driver with a BullMQ
|
|
10
|
+
* (Redis-backed) job queue. Reasons:
|
|
11
|
+
* - the registry will move to Redis in the multi-instance deployment story,
|
|
12
|
+
* and the sweeper job belongs in the same place;
|
|
13
|
+
* - BullMQ gives us cron scheduling, retries on transient storage errors,
|
|
14
|
+
* visibility/metrics through Bull Board, and crash safety (a process
|
|
15
|
+
* restart resumes scheduled sweeps instead of forgetting them);
|
|
16
|
+
* - per-key cancel can become a delayed job (`removeJob` on confirm) which
|
|
17
|
+
* is more efficient than a periodic full-table scan once we have many
|
|
18
|
+
* pending entries.
|
|
19
|
+
* The migration should keep the same `PendingUploadsRegistry` API surface so
|
|
20
|
+
* call sites (controller, hooks) do not need to change — only the storage
|
|
21
|
+
* (Map → Redis) and the sweeper driver (setInterval → BullMQ Worker) flip.
|
|
22
|
+
*/
|
|
23
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
24
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
25
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
26
|
+
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;
|
|
27
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
28
|
+
};
|
|
29
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
30
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
31
|
+
};
|
|
32
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
33
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
34
|
+
};
|
|
35
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
36
|
+
import { PendingUploadsRegistry } from '../pending-registry.js';
|
|
37
|
+
import { UPLOAD_MODULE_OPTIONS } from './upload.tokens.js';
|
|
38
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
39
|
+
let UploadSweeperService = class UploadSweeperService {
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
onModuleInit() {
|
|
45
|
+
const interval = this.options.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
|
|
46
|
+
if (interval <= 0)
|
|
47
|
+
return;
|
|
48
|
+
this.timer = setInterval(() => {
|
|
49
|
+
void PendingUploadsRegistry.sweep();
|
|
50
|
+
}, interval);
|
|
51
|
+
// Allow the Node.js process to exit even if the timer is still scheduled.
|
|
52
|
+
if (typeof this.timer.unref === 'function')
|
|
53
|
+
this.timer.unref();
|
|
54
|
+
}
|
|
55
|
+
onModuleDestroy() {
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
UploadSweeperService = __decorate([
|
|
63
|
+
Injectable(),
|
|
64
|
+
__param(0, Inject(UPLOAD_MODULE_OPTIONS)),
|
|
65
|
+
__metadata("design:paramtypes", [Object])
|
|
66
|
+
], UploadSweeperService);
|
|
67
|
+
export { UploadSweeperService };
|
|
68
|
+
//# sourceMappingURL=upload-sweeper.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-sweeper.service.js","sourceRoot":"","sources":["../../src/nest/upload-sweeper.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;;;;;;;;;;;AAEH,OAAO,EAAE,MAAM,EAAE,UAAU,EAA2C,MAAM,gBAAgB,CAAA;AAC5F,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAuC,MAAM,oBAAoB,CAAA;AAE/F,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AAGxC,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAG/B,YAA2C,OAAwD;QAAvC,YAAO,GAAP,OAAO,CAAgC;QAF3F,UAAK,GAA0C,IAAI,CAAA;IAE2C,CAAC;IAEvG,YAAY;QACV,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,IAAI,yBAAyB,CAAA;QAC1E,IAAI,QAAQ,IAAI,CAAC;YAAE,OAAM;QACzB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC5B,KAAK,sBAAsB,CAAC,KAAK,EAAE,CAAA;QACrC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACZ,0EAA0E;QAC1E,IAAI,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,UAAU;YAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IAChE,CAAC;IAED,eAAe;QACb,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACnB,CAAC;IACH,CAAC;CACF,CAAA;AArBY,oBAAoB;IADhC,UAAU,EAAE;IAIE,WAAA,MAAM,CAAC,qBAAqB,CAAC,CAAA;;GAH/B,oBAAoB,CAqBhC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadController — handles file uploads for `type: 'file'` properties.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints
|
|
5
|
+
* ---------
|
|
6
|
+
* POST /admin/api/resources/:resourceId/actions/upload?field=<propertyPath>
|
|
7
|
+
* Content-Type: multipart/form-data
|
|
8
|
+
* Accepts one or more files (any field names). Returns `UploadedFileInfo[]` —
|
|
9
|
+
* callers writing to a single-value `'file'` property take the first item;
|
|
10
|
+
* `isArray: true` properties consume the whole array.
|
|
11
|
+
*
|
|
12
|
+
* DELETE /admin/api/resources/:resourceId/actions/upload?field=<path>&key=<key>
|
|
13
|
+
* Cancels a still-pending upload (uploaded but not yet saved). Calls
|
|
14
|
+
* `PendingUploadsRegistry.cancel(key)` which deletes the file from
|
|
15
|
+
* storage. No-op (404) if the key is no longer pending — protects already
|
|
16
|
+
* persisted files from being deleted via this endpoint.
|
|
17
|
+
*
|
|
18
|
+
* Authorisation re-uses `ModernAdminAuthGuard` from `@modern-admin/nest` —
|
|
19
|
+
* requires `ModernAdminModule.forRoot()` to be registered (globally or as a
|
|
20
|
+
* parent module).
|
|
21
|
+
*/
|
|
22
|
+
import type { IncomingMessage } from 'node:http';
|
|
23
|
+
import { type ModernAdmin } from '@modern-admin/core';
|
|
24
|
+
import { type ModernAdminUploadModuleOptions } from './upload.tokens.js';
|
|
25
|
+
import type { UploadedFileInfo } from '../types.js';
|
|
26
|
+
export declare class UploadController {
|
|
27
|
+
private readonly admin;
|
|
28
|
+
private readonly moduleOptions;
|
|
29
|
+
constructor(admin: ModernAdmin, moduleOptions: ModernAdminUploadModuleOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Upload one or more files for a specific resource property.
|
|
32
|
+
*
|
|
33
|
+
* @param resourceId Admin resource id (e.g. `'users'`)
|
|
34
|
+
* @param field Property path (e.g. `'avatar'` / `'gallery'`)
|
|
35
|
+
* @param req Raw Express / Node.js `IncomingMessage`
|
|
36
|
+
*/
|
|
37
|
+
upload(resourceId: string, field: string, req: IncomingMessage): Promise<UploadedFileInfo[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Cancel a still-pending upload — delete the file from storage immediately.
|
|
40
|
+
* Used by the editor when the user removes a file *before* saving the form.
|
|
41
|
+
*
|
|
42
|
+
* Returns 204 if the cancel succeeded, 404 if the key is not pending.
|
|
43
|
+
* The "not pending" branch protects already-persisted files: once a file
|
|
44
|
+
* has been confirmed via the action hooks it can no longer be removed via
|
|
45
|
+
* this endpoint — only via the regular edit/delete actions.
|
|
46
|
+
*/
|
|
47
|
+
cancel(resourceId: string, field: string, key: string): Promise<void>;
|
|
48
|
+
/** Resolve the resource + property and ensure it is wired to an upload provider. */
|
|
49
|
+
private resolveProperty;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=upload.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.controller.d.ts","sourceRoot":"","sources":["../../src/nest/upload.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAA;AAI3B,OAAO,EAAyB,KAAK,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAC/F,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAiEjE,qBAIa,gBAAgB;IAEH,OAAO,CAAC,QAAQ,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ,CAAC,aAAa;gBADtB,KAAK,EAAE,WAAW,EACT,aAAa,EAAE,8BAA8B;IAG/F;;;;;;OAMG;IAEG,MAAM,CACW,UAAU,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,EACtB,GAAG,EAAE,eAAe,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA2B9B;;;;;;;;OAQG;IAGG,MAAM,CACW,UAAU,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC;IAchB,oFAAoF;IACpF,OAAO,CAAC,eAAe;CAkCxB"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadController — handles file uploads for `type: 'file'` properties.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints
|
|
5
|
+
* ---------
|
|
6
|
+
* POST /admin/api/resources/:resourceId/actions/upload?field=<propertyPath>
|
|
7
|
+
* Content-Type: multipart/form-data
|
|
8
|
+
* Accepts one or more files (any field names). Returns `UploadedFileInfo[]` —
|
|
9
|
+
* callers writing to a single-value `'file'` property take the first item;
|
|
10
|
+
* `isArray: true` properties consume the whole array.
|
|
11
|
+
*
|
|
12
|
+
* DELETE /admin/api/resources/:resourceId/actions/upload?field=<path>&key=<key>
|
|
13
|
+
* Cancels a still-pending upload (uploaded but not yet saved). Calls
|
|
14
|
+
* `PendingUploadsRegistry.cancel(key)` which deletes the file from
|
|
15
|
+
* storage. No-op (404) if the key is no longer pending — protects already
|
|
16
|
+
* persisted files from being deleted via this endpoint.
|
|
17
|
+
*
|
|
18
|
+
* Authorisation re-uses `ModernAdminAuthGuard` from `@modern-admin/nest` —
|
|
19
|
+
* requires `ModernAdminModule.forRoot()` to be registered (globally or as a
|
|
20
|
+
* parent module).
|
|
21
|
+
*/
|
|
22
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
23
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
24
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
25
|
+
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;
|
|
26
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
27
|
+
};
|
|
28
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
29
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
30
|
+
};
|
|
31
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
32
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
33
|
+
};
|
|
34
|
+
import { BadRequestException, Controller, Delete, ForbiddenException, HttpCode, Inject, InternalServerErrorException, NotFoundException, Post, Param, Query, Req, UseGuards, } from '@nestjs/common';
|
|
35
|
+
import { ApiCookieAuth, ApiTags } from '@nestjs/swagger';
|
|
36
|
+
import Busboy from 'busboy';
|
|
37
|
+
import { ForbiddenError, ResourceNotFoundError, } from '@modern-admin/core';
|
|
38
|
+
import { MODERN_ADMIN, ModernAdminAuthGuard } from '@modern-admin/nest';
|
|
39
|
+
import { UploadProviderRegistry } from '../registry.js';
|
|
40
|
+
import { PendingUploadsRegistry } from '../pending-registry.js';
|
|
41
|
+
import { UPLOAD_MODULE_OPTIONS } from './upload.tokens.js';
|
|
42
|
+
/** Reads every file from a multipart/form-data stream using busboy. */
|
|
43
|
+
function parseAllFiles(req) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let bb;
|
|
46
|
+
try {
|
|
47
|
+
bb = Busboy({ headers: req.headers });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
reject(new BadRequestException('Request is not multipart/form-data'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const files = [];
|
|
54
|
+
/** Number of streams that have started but not yet ended. */
|
|
55
|
+
let pending = 0;
|
|
56
|
+
let finished = false;
|
|
57
|
+
let settled = false;
|
|
58
|
+
let firstError;
|
|
59
|
+
const tryResolve = () => {
|
|
60
|
+
if (settled || !finished || pending > 0)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (firstError)
|
|
64
|
+
reject(firstError);
|
|
65
|
+
else if (files.length === 0)
|
|
66
|
+
reject(new BadRequestException('No file found in request body'));
|
|
67
|
+
else
|
|
68
|
+
resolve(files);
|
|
69
|
+
};
|
|
70
|
+
bb.on('file', (_fieldname, stream, info) => {
|
|
71
|
+
pending++;
|
|
72
|
+
const chunks = [];
|
|
73
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
74
|
+
stream.on('end', () => {
|
|
75
|
+
const buffer = Buffer.concat(chunks);
|
|
76
|
+
files.push({
|
|
77
|
+
originalName: info.filename || 'upload',
|
|
78
|
+
mimeType: info.mimeType || 'application/octet-stream',
|
|
79
|
+
size: buffer.length,
|
|
80
|
+
buffer,
|
|
81
|
+
});
|
|
82
|
+
pending--;
|
|
83
|
+
tryResolve();
|
|
84
|
+
});
|
|
85
|
+
stream.on('error', (err) => {
|
|
86
|
+
firstError = firstError ?? err;
|
|
87
|
+
pending--;
|
|
88
|
+
tryResolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
bb.on('finish', () => {
|
|
92
|
+
finished = true;
|
|
93
|
+
tryResolve();
|
|
94
|
+
});
|
|
95
|
+
bb.on('error', (err) => {
|
|
96
|
+
firstError = firstError ?? err;
|
|
97
|
+
finished = true;
|
|
98
|
+
tryResolve();
|
|
99
|
+
});
|
|
100
|
+
req.pipe(bb);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let UploadController = class UploadController {
|
|
104
|
+
constructor(admin, moduleOptions) {
|
|
105
|
+
this.admin = admin;
|
|
106
|
+
this.moduleOptions = moduleOptions;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Upload one or more files for a specific resource property.
|
|
110
|
+
*
|
|
111
|
+
* @param resourceId Admin resource id (e.g. `'users'`)
|
|
112
|
+
* @param field Property path (e.g. `'avatar'` / `'gallery'`)
|
|
113
|
+
* @param req Raw Express / Node.js `IncomingMessage`
|
|
114
|
+
*/
|
|
115
|
+
async upload(resourceId, field, req) {
|
|
116
|
+
const { providerId, registered } = this.resolveProperty(resourceId, field);
|
|
117
|
+
// 1 — Parse the multipart body (collect every file).
|
|
118
|
+
const files = await parseAllFiles(req);
|
|
119
|
+
// 2 — If the property is single-value, only the first file is honoured.
|
|
120
|
+
// The frontend sends one anyway; this is a safety bound.
|
|
121
|
+
const accepted = registered.isArray ? files : files.slice(0, 1);
|
|
122
|
+
// 3 — Upload each file, computing the storage key per-file so a custom
|
|
123
|
+
// `uploadPath` generator can produce unique keys for each upload.
|
|
124
|
+
const ttlMs = this.moduleOptions.pendingTtlMs ?? 60 * 60 * 1000;
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const file of accepted) {
|
|
127
|
+
const computedKey = registered.uploadPath ? registered.uploadPath(file.originalName) : undefined;
|
|
128
|
+
const key = await registered.provider.upload(file, computedKey);
|
|
129
|
+
// Track as pending — it will be confirmed by `new.after`/`edit.after`
|
|
130
|
+
// when the form is saved, or cleaned up by the sweeper when the TTL
|
|
131
|
+
// expires (whichever comes first).
|
|
132
|
+
PendingUploadsRegistry.track(key, providerId, ttlMs);
|
|
133
|
+
const url = await registered.provider.getUrl(key);
|
|
134
|
+
results.push({ key, url, name: file.originalName, size: file.size, mimeType: file.mimeType });
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Cancel a still-pending upload — delete the file from storage immediately.
|
|
140
|
+
* Used by the editor when the user removes a file *before* saving the form.
|
|
141
|
+
*
|
|
142
|
+
* Returns 204 if the cancel succeeded, 404 if the key is not pending.
|
|
143
|
+
* The "not pending" branch protects already-persisted files: once a file
|
|
144
|
+
* has been confirmed via the action hooks it can no longer be removed via
|
|
145
|
+
* this endpoint — only via the regular edit/delete actions.
|
|
146
|
+
*/
|
|
147
|
+
async cancel(resourceId, field, key) {
|
|
148
|
+
if (!key)
|
|
149
|
+
throw new BadRequestException('Missing "key" query parameter');
|
|
150
|
+
// Validate that the resource/field is actually upload-enabled. This avoids
|
|
151
|
+
// leaking the existence of arbitrary resource ids to unauthenticated callers
|
|
152
|
+
// (the auth guard already requires authentication).
|
|
153
|
+
this.resolveProperty(resourceId, field);
|
|
154
|
+
const cancelled = await PendingUploadsRegistry.cancel(key);
|
|
155
|
+
if (!cancelled) {
|
|
156
|
+
throw new NotFoundException('Key is not pending — already saved or unknown');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ─── Internals ─────────────────────────────────────────────────────────────
|
|
160
|
+
/** Resolve the resource + property and ensure it is wired to an upload provider. */
|
|
161
|
+
resolveProperty(resourceId, field) {
|
|
162
|
+
let resource;
|
|
163
|
+
try {
|
|
164
|
+
resource = this.admin.findResource(resourceId);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (err instanceof ResourceNotFoundError)
|
|
168
|
+
throw new NotFoundException(err.message);
|
|
169
|
+
if (err instanceof ForbiddenError)
|
|
170
|
+
throw new ForbiddenException(err.message);
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
const decorator = resource.decorate();
|
|
174
|
+
const prop = decorator.getPropertyByKey(field);
|
|
175
|
+
if (!prop) {
|
|
176
|
+
throw new NotFoundException(`Property "${field}" not found on resource "${resourceId}"`);
|
|
177
|
+
}
|
|
178
|
+
const propJson = prop.toJSON();
|
|
179
|
+
const providerId = propJson.custom?.uploadProviderId;
|
|
180
|
+
if (!providerId) {
|
|
181
|
+
throw new BadRequestException(`Property "${field}" on resource "${resourceId}" is not configured for upload. ` +
|
|
182
|
+
'Apply uploadFeature() to the resource.');
|
|
183
|
+
}
|
|
184
|
+
const registered = UploadProviderRegistry.get(providerId);
|
|
185
|
+
if (!registered) {
|
|
186
|
+
throw new InternalServerErrorException(`Upload provider "${providerId}" is not registered.`);
|
|
187
|
+
}
|
|
188
|
+
return { providerId, registered };
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
__decorate([
|
|
192
|
+
Post('upload'),
|
|
193
|
+
__param(0, Param('resourceId')),
|
|
194
|
+
__param(1, Query('field')),
|
|
195
|
+
__param(2, Req()),
|
|
196
|
+
__metadata("design:type", Function),
|
|
197
|
+
__metadata("design:paramtypes", [String, String, Function]),
|
|
198
|
+
__metadata("design:returntype", Promise)
|
|
199
|
+
], UploadController.prototype, "upload", null);
|
|
200
|
+
__decorate([
|
|
201
|
+
Delete('upload'),
|
|
202
|
+
HttpCode(204),
|
|
203
|
+
__param(0, Param('resourceId')),
|
|
204
|
+
__param(1, Query('field')),
|
|
205
|
+
__param(2, Query('key')),
|
|
206
|
+
__metadata("design:type", Function),
|
|
207
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
208
|
+
__metadata("design:returntype", Promise)
|
|
209
|
+
], UploadController.prototype, "cancel", null);
|
|
210
|
+
UploadController = __decorate([
|
|
211
|
+
ApiTags('Admin / Uploads'),
|
|
212
|
+
ApiCookieAuth('session'),
|
|
213
|
+
Controller('admin/api/resources/:resourceId/actions'),
|
|
214
|
+
UseGuards(ModernAdminAuthGuard),
|
|
215
|
+
__param(0, Inject(MODERN_ADMIN)),
|
|
216
|
+
__param(1, Inject(UPLOAD_MODULE_OPTIONS)),
|
|
217
|
+
__metadata("design:paramtypes", [Function, Object])
|
|
218
|
+
], UploadController);
|
|
219
|
+
export { UploadController };
|
|
220
|
+
//# sourceMappingURL=upload.controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.controller.js","sourceRoot":"","sources":["../../src/nest/upload.controller.ts"],"names":[],"mappings":"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"}
|