@balena/pinejs 17.0.0-build-large-file-uploads-293e65ee371a69130834c50fef2f7f42cc18133f-1 → 17.0.0-build-17-x-0447fb6e94bafa136206d060acda2f2df4c00ad2-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/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +2295 -17
- package/CHANGELOG.md +860 -5
- package/out/bin/abstract-sql-compiler.js +1 -1
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +1 -1
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/database-layer/db.js +30 -19
- package/out/database-layer/db.js.map +1 -1
- package/out/http-transactions/transactions.js +2 -2
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.js +8 -9
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/sync.js +6 -6
- package/out/migrator/sync.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +103 -108
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.js +1 -1
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/errors.js +3 -0
- package/out/sbvr-api/errors.js.map +1 -1
- package/out/sbvr-api/hooks.js +4 -5
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/permissions.js +3 -3
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +5 -6
- package/out/sbvr-api/sbvr-utils.js +16 -15
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/uri-parser.js +1 -1
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/server-glue/module.js +0 -2
- package/out/server-glue/module.js.map +1 -1
- package/out/webresource-handler/handlers/NoopHandler.d.ts +1 -3
- package/out/webresource-handler/handlers/NoopHandler.js +0 -6
- package/out/webresource-handler/handlers/NoopHandler.js.map +1 -1
- package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
- package/out/webresource-handler/handlers/S3Handler.js +104 -0
- package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
- package/out/webresource-handler/handlers/index.d.ts +1 -0
- package/out/webresource-handler/handlers/index.js +1 -0
- package/out/webresource-handler/handlers/index.js.map +1 -1
- package/out/webresource-handler/index.d.ts +7 -31
- package/out/webresource-handler/index.js +22 -23
- package/out/webresource-handler/index.js.map +1 -1
- package/package.json +40 -39
- package/src/bin/abstract-sql-compiler.ts +1 -1
- package/src/bin/sbvr-compiler.ts +1 -1
- package/src/http-transactions/transactions.js +2 -2
- package/src/migrator/async.ts +10 -11
- package/src/migrator/sync.ts +6 -6
- package/src/sbvr-api/abstract-sql.ts +1 -1
- package/src/sbvr-api/permissions.ts +3 -3
- package/src/sbvr-api/sbvr-utils.ts +24 -23
- package/src/sbvr-api/uri-parser.ts +1 -1
- package/src/server-glue/module.ts +0 -2
- package/src/webresource-handler/handlers/NoopHandler.ts +1 -14
- package/src/webresource-handler/handlers/S3Handler.ts +143 -0
- package/src/webresource-handler/handlers/index.ts +1 -0
- package/src/webresource-handler/index.ts +22 -64
- package/tsconfig.json +1 -1
- package/out/webresource-handler/multipartUpload.d.ts +0 -16
- package/out/webresource-handler/multipartUpload.js +0 -182
- package/out/webresource-handler/multipartUpload.js.map +0 -1
- package/out/webresource-handler/webresource.sbvr +0 -63
- package/src/webresource-handler/multipartUpload.ts +0 -275
- package/src/webresource-handler/webresource.sbvr +0 -63
@@ -671,20 +671,20 @@ export const executeModels = async (
|
|
671
671
|
await validateModel(tx, apiRoot);
|
672
672
|
}
|
673
673
|
|
674
|
-
|
675
|
-
|
676
|
-
|
674
|
+
api[apiRoot] = new PinejsClient('/' + apiRoot + '/');
|
675
|
+
|
676
|
+
logger[apiRoot] = { ...console };
|
677
677
|
if (model.logging != null) {
|
678
678
|
const defaultSetting = model.logging?.default ?? true;
|
679
|
-
const
|
679
|
+
const log = logger[apiRoot];
|
680
680
|
for (const k of Object.keys(model.logging)) {
|
681
681
|
const key = k as keyof Console;
|
682
682
|
if (
|
683
683
|
key !== 'Console' &&
|
684
|
-
typeof
|
684
|
+
typeof log[key] === 'function' &&
|
685
685
|
!(model.logging?.[key] ?? defaultSetting)
|
686
686
|
) {
|
687
|
-
|
687
|
+
log[key] = _.noop;
|
688
688
|
}
|
689
689
|
}
|
690
690
|
}
|
@@ -856,7 +856,7 @@ export const runRule = (() => {
|
|
856
856
|
translator.addTypes(sbvrTypes);
|
857
857
|
return async (vocab: string, rule: string) => {
|
858
858
|
const seModel = models[vocab].se;
|
859
|
-
const
|
859
|
+
const log = logger[vocab];
|
860
860
|
let lfModel: LFModel;
|
861
861
|
let slfModel: LFModel;
|
862
862
|
let abstractSqlModel: AbstractSQLCompiler.AbstractSqlModel;
|
@@ -867,7 +867,7 @@ export const runRule = (() => {
|
|
867
867
|
'Process',
|
868
868
|
);
|
869
869
|
} catch (e) {
|
870
|
-
|
870
|
+
log.error('Error parsing rule', rule, e);
|
871
871
|
throw new Error(`Error parsing rule'${rule}': ${e}`);
|
872
872
|
}
|
873
873
|
|
@@ -881,7 +881,7 @@ export const runRule = (() => {
|
|
881
881
|
translator.reset();
|
882
882
|
abstractSqlModel = translator.match(slfModel, 'Process');
|
883
883
|
} catch (e) {
|
884
|
-
|
884
|
+
log.error('Error compiling rule', rule, e);
|
885
885
|
throw new Error(`Error compiling rule '${rule}': ${e}`);
|
886
886
|
}
|
887
887
|
|
@@ -992,6 +992,9 @@ export const runRule = (() => {
|
|
992
992
|
};
|
993
993
|
})();
|
994
994
|
|
995
|
+
/**
|
996
|
+
* This type shows the passthrough properties that the internal pinejs client instance accepts
|
997
|
+
*/
|
995
998
|
export type Passthrough = AnyObject & {
|
996
999
|
req?: {
|
997
1000
|
user?: User;
|
@@ -999,9 +1002,7 @@ export type Passthrough = AnyObject & {
|
|
999
1002
|
tx?: Db.Tx;
|
1000
1003
|
};
|
1001
1004
|
|
1002
|
-
export class PinejsClient extends PinejsClientCore
|
1003
|
-
// @ts-expect-error This is actually assigned by `super` so it is always declared but that isn't detected here
|
1004
|
-
public passthrough: Passthrough;
|
1005
|
+
export class PinejsClient extends PinejsClientCore {
|
1005
1006
|
public async _request({
|
1006
1007
|
method,
|
1007
1008
|
url,
|
@@ -1021,11 +1022,11 @@ export class PinejsClient extends PinejsClientCore<PinejsClient> {
|
|
1021
1022
|
}
|
1022
1023
|
}
|
1023
1024
|
|
1024
|
-
export type LoggingClient = PinejsClient & {
|
1025
|
-
logger: Console;
|
1026
|
-
};
|
1027
1025
|
export const api: {
|
1028
|
-
[vocab: string]:
|
1026
|
+
[vocab: string]: PinejsClient;
|
1027
|
+
} = {};
|
1028
|
+
export const logger: {
|
1029
|
+
[vocab: string]: Console;
|
1029
1030
|
} = {};
|
1030
1031
|
|
1031
1032
|
// We default to guest only permissions if no req object is passed in
|
@@ -1224,7 +1225,7 @@ export const getModel = (vocabulary: string) => {
|
|
1224
1225
|
|
1225
1226
|
const runODataRequest = (req: Express.Request, vocabulary: string) => {
|
1226
1227
|
if (env.DEBUG) {
|
1227
|
-
|
1228
|
+
logger[vocabulary].log('Parsing', req.method, req.url);
|
1228
1229
|
}
|
1229
1230
|
|
1230
1231
|
// Get the hooks for the current method/vocabulary as we know it,
|
@@ -1535,10 +1536,10 @@ const runRequest = async (
|
|
1535
1536
|
tx: Db.Tx,
|
1536
1537
|
request: uriParser.ODataRequest,
|
1537
1538
|
): Promise<Response> => {
|
1538
|
-
const
|
1539
|
+
const log = logger[request.vocabulary];
|
1539
1540
|
|
1540
1541
|
if (env.DEBUG) {
|
1541
|
-
|
1542
|
+
log.log('Running', req.method, req.url);
|
1542
1543
|
}
|
1543
1544
|
let result: Db.Result | number | undefined;
|
1544
1545
|
|
@@ -1566,7 +1567,7 @@ const runRequest = async (
|
|
1566
1567
|
} catch (err: any) {
|
1567
1568
|
if (err instanceof db.DatabaseError) {
|
1568
1569
|
prettifyConstraintError(err, request);
|
1569
|
-
|
1570
|
+
log.error(err);
|
1570
1571
|
// Override the error message so we don't leak any internal db info
|
1571
1572
|
err.message = 'Database error';
|
1572
1573
|
throw err;
|
@@ -1580,7 +1581,7 @@ const runRequest = async (
|
|
1580
1581
|
err instanceof TypeError ||
|
1581
1582
|
err instanceof URIError
|
1582
1583
|
) {
|
1583
|
-
|
1584
|
+
log.error(err);
|
1584
1585
|
throw new InternalRequestError();
|
1585
1586
|
}
|
1586
1587
|
throw err;
|
@@ -1740,7 +1741,7 @@ const runQuery = async (
|
|
1740
1741
|
);
|
1741
1742
|
|
1742
1743
|
if (env.DEBUG) {
|
1743
|
-
|
1744
|
+
logger[vocabulary].log(query, values);
|
1744
1745
|
}
|
1745
1746
|
|
1746
1747
|
// We only add the returning clause if it's been requested and `affectedIds` hasn't been populated yet
|
@@ -1848,7 +1849,7 @@ const respondPost = async (
|
|
1848
1849
|
id,
|
1849
1850
|
);
|
1850
1851
|
if (env.DEBUG) {
|
1851
|
-
|
1852
|
+
logger[vocab].log('Insert ID: ', request.resourceName, id);
|
1852
1853
|
}
|
1853
1854
|
|
1854
1855
|
let result: AnyObject = { d: [{ id }] };
|
@@ -421,7 +421,7 @@ export const translateUri = <
|
|
421
421
|
request.values = new Proxy(request.values, {
|
422
422
|
set: (obj: ODataRequest['values'], prop: string, value) => {
|
423
423
|
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
424
|
-
sbvrUtils.
|
424
|
+
sbvrUtils.logger[request.vocabulary].warn(
|
425
425
|
`Assigning a new request.values property '${prop}' however it will be ignored`,
|
426
426
|
);
|
427
427
|
}
|
@@ -5,7 +5,6 @@ import './sbvr-loader';
|
|
5
5
|
import * as dbModule from '../database-layer/db';
|
6
6
|
import * as configLoader from '../config-loader/config-loader';
|
7
7
|
import * as migrator from '../migrator/sync';
|
8
|
-
import * as webResourceHandler from '../webresource-handler';
|
9
8
|
import type * as migratorUtils from '../migrator/utils';
|
10
9
|
|
11
10
|
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
@@ -64,7 +63,6 @@ export const init = async <T extends string>(
|
|
64
63
|
await sbvrUtils.setup(app, db);
|
65
64
|
const cfgLoader = await configLoader.setup(app);
|
66
65
|
await cfgLoader.loadConfig(migrator.config);
|
67
|
-
await cfgLoader.loadConfig(webResourceHandler.config);
|
68
66
|
|
69
67
|
const promises: Array<Promise<void>> = [];
|
70
68
|
if (process.env.SBVR_SERVER_ENABLED) {
|
@@ -1,10 +1,5 @@
|
|
1
1
|
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
2
|
-
import type {
|
3
|
-
BeginMultipartUploadHandlerResponse,
|
4
|
-
IncomingFile,
|
5
|
-
UploadResponse,
|
6
|
-
WebResourceHandler,
|
7
|
-
} from '..';
|
2
|
+
import type { IncomingFile, UploadResponse, WebResourceHandler } from '..';
|
8
3
|
|
9
4
|
export class NoopHandler implements WebResourceHandler {
|
10
5
|
public async handleFile(resource: IncomingFile): Promise<UploadResponse> {
|
@@ -23,12 +18,4 @@ export class NoopHandler implements WebResourceHandler {
|
|
23
18
|
public async onPreRespond(webResource: WebResource): Promise<WebResource> {
|
24
19
|
return webResource;
|
25
20
|
}
|
26
|
-
|
27
|
-
public async beginMultipartUpload(): Promise<BeginMultipartUploadHandlerResponse> {
|
28
|
-
return { fileKey: 'noop', uploadId: 'noop', uploadParts: [] };
|
29
|
-
}
|
30
|
-
|
31
|
-
public async commitMultipartUpload(): Promise<WebResource> {
|
32
|
-
return { filename: 'noop', href: 'noop' };
|
33
|
-
}
|
34
21
|
}
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import {
|
2
|
+
FileSizeExceededError,
|
3
|
+
type IncomingFile,
|
4
|
+
normalizeHref,
|
5
|
+
type UploadResponse,
|
6
|
+
WebResourceError,
|
7
|
+
type WebResourceHandler,
|
8
|
+
} from '..';
|
9
|
+
import {
|
10
|
+
S3Client,
|
11
|
+
type S3ClientConfig,
|
12
|
+
DeleteObjectCommand,
|
13
|
+
type PutObjectCommandInput,
|
14
|
+
GetObjectCommand,
|
15
|
+
} from '@aws-sdk/client-s3';
|
16
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
17
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
18
|
+
|
19
|
+
import { randomUUID } from 'crypto';
|
20
|
+
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
21
|
+
import memoize from 'memoizee';
|
22
|
+
|
23
|
+
export interface S3HandlerProps {
|
24
|
+
region: string;
|
25
|
+
accessKey: string;
|
26
|
+
secretKey: string;
|
27
|
+
endpoint: string;
|
28
|
+
bucket: string;
|
29
|
+
maxSize?: number;
|
30
|
+
signedUrlExpireTimeSeconds?: number;
|
31
|
+
signedUrlCacheExpireTimeSeconds?: number;
|
32
|
+
}
|
33
|
+
|
34
|
+
export class S3Handler implements WebResourceHandler {
|
35
|
+
private readonly config: S3ClientConfig;
|
36
|
+
private readonly bucket: string;
|
37
|
+
private readonly maxFileSize: number;
|
38
|
+
|
39
|
+
protected readonly signedUrlExpireTimeSeconds: number;
|
40
|
+
protected readonly signedUrlCacheExpireTimeSeconds: number;
|
41
|
+
protected cachedGetSignedUrl: (fileKey: string) => Promise<string>;
|
42
|
+
|
43
|
+
private client: S3Client;
|
44
|
+
|
45
|
+
constructor(config: S3HandlerProps) {
|
46
|
+
this.config = {
|
47
|
+
region: config.region,
|
48
|
+
credentials: {
|
49
|
+
accessKeyId: config.accessKey,
|
50
|
+
secretAccessKey: config.secretKey,
|
51
|
+
},
|
52
|
+
endpoint: config.endpoint,
|
53
|
+
forcePathStyle: true,
|
54
|
+
};
|
55
|
+
|
56
|
+
this.signedUrlExpireTimeSeconds =
|
57
|
+
config.signedUrlExpireTimeSeconds ?? 86400; // 24h
|
58
|
+
this.signedUrlCacheExpireTimeSeconds =
|
59
|
+
config.signedUrlCacheExpireTimeSeconds ?? 82800; // 22h
|
60
|
+
|
61
|
+
this.maxFileSize = config.maxSize ?? 52428800;
|
62
|
+
this.bucket = config.bucket;
|
63
|
+
this.client = new S3Client(this.config);
|
64
|
+
|
65
|
+
// Memoize expects maxAge in MS and s3 signing method in seconds.
|
66
|
+
// Normalization to use only seconds and therefore convert here from seconds to MS
|
67
|
+
this.cachedGetSignedUrl = memoize(this.s3SignUrl, {
|
68
|
+
maxAge: this.signedUrlCacheExpireTimeSeconds * 1000,
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
public async handleFile(resource: IncomingFile): Promise<UploadResponse> {
|
73
|
+
let size = 0;
|
74
|
+
const key = `${resource.fieldname}_${randomUUID()}_${
|
75
|
+
resource.originalname
|
76
|
+
}`;
|
77
|
+
const params: PutObjectCommandInput = {
|
78
|
+
Bucket: this.bucket,
|
79
|
+
Key: key,
|
80
|
+
Body: resource.stream,
|
81
|
+
ContentType: resource.mimetype,
|
82
|
+
};
|
83
|
+
const upload = new Upload({ client: this.client, params });
|
84
|
+
|
85
|
+
upload.on('httpUploadProgress', async (ev) => {
|
86
|
+
size = ev.total ?? ev.loaded!;
|
87
|
+
if (size > this.maxFileSize) {
|
88
|
+
await upload.abort();
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
try {
|
93
|
+
await upload.done();
|
94
|
+
} catch (err: any) {
|
95
|
+
resource.stream.resume();
|
96
|
+
if (size > this.maxFileSize) {
|
97
|
+
throw new FileSizeExceededError(this.maxFileSize);
|
98
|
+
}
|
99
|
+
throw new WebResourceError(err);
|
100
|
+
}
|
101
|
+
|
102
|
+
const filename = this.getS3URL(key);
|
103
|
+
return { size, filename };
|
104
|
+
}
|
105
|
+
|
106
|
+
public async removeFile(href: string): Promise<void> {
|
107
|
+
const fileKey = this.getKeyFromHref(href);
|
108
|
+
|
109
|
+
const command = new DeleteObjectCommand({
|
110
|
+
Bucket: this.bucket,
|
111
|
+
Key: fileKey,
|
112
|
+
});
|
113
|
+
|
114
|
+
await this.client.send(command);
|
115
|
+
}
|
116
|
+
|
117
|
+
public async onPreRespond(webResource: WebResource): Promise<WebResource> {
|
118
|
+
if (webResource.href != null) {
|
119
|
+
const fileKey = this.getKeyFromHref(webResource.href);
|
120
|
+
webResource.href = await this.cachedGetSignedUrl(fileKey);
|
121
|
+
}
|
122
|
+
return webResource;
|
123
|
+
}
|
124
|
+
|
125
|
+
private s3SignUrl(fileKey: string): Promise<string> {
|
126
|
+
const command = new GetObjectCommand({
|
127
|
+
Bucket: this.bucket,
|
128
|
+
Key: fileKey,
|
129
|
+
});
|
130
|
+
return getSignedUrl(this.client, command, {
|
131
|
+
expiresIn: this.signedUrlExpireTimeSeconds,
|
132
|
+
});
|
133
|
+
}
|
134
|
+
|
135
|
+
private getS3URL(key: string): string {
|
136
|
+
return `${this.config.endpoint}/${this.bucket}/${key}`;
|
137
|
+
}
|
138
|
+
|
139
|
+
private getKeyFromHref(href: string): string {
|
140
|
+
const hrefWithoutParams = normalizeHref(href);
|
141
|
+
return hrefWithoutParams.substring(hrefWithoutParams.lastIndexOf('/') + 1);
|
142
|
+
}
|
143
|
+
}
|
@@ -13,8 +13,7 @@ import {
|
|
13
13
|
} from '@balena/odata-to-abstract-sql';
|
14
14
|
import { errors, permissions } from '../server-glue/module';
|
15
15
|
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
16
|
-
import
|
17
|
-
import { multipartUploadHooks } from './multipartUpload';
|
16
|
+
import { TypedError } from 'typed-error';
|
18
17
|
|
19
18
|
export * from './handlers';
|
20
19
|
|
@@ -31,44 +30,19 @@ export interface UploadResponse {
|
|
31
30
|
filename: string;
|
32
31
|
}
|
33
32
|
|
34
|
-
export interface BeginMultipartUploadPayload {
|
35
|
-
filename: string;
|
36
|
-
content_type: string;
|
37
|
-
size: number;
|
38
|
-
chunk_size: number;
|
39
|
-
}
|
40
|
-
|
41
|
-
export interface UploadPart {
|
42
|
-
url: string;
|
43
|
-
chunkSize: number;
|
44
|
-
partNumber: number;
|
45
|
-
}
|
46
|
-
|
47
|
-
export interface BeginMultipartUploadHandlerResponse {
|
48
|
-
uploadParts: UploadPart[];
|
49
|
-
fileKey: string;
|
50
|
-
uploadId: string;
|
51
|
-
}
|
52
|
-
|
53
|
-
export interface CommitMultipartUploadPayload {
|
54
|
-
fileKey: string;
|
55
|
-
uploadId: string;
|
56
|
-
filename: string;
|
57
|
-
providerCommitData?: AnyObject;
|
58
|
-
}
|
59
|
-
|
60
33
|
export interface WebResourceHandler {
|
61
34
|
handleFile: (resource: IncomingFile) => Promise<UploadResponse>;
|
62
35
|
removeFile: (fileReference: string) => Promise<void>;
|
63
36
|
onPreRespond: (webResource: WebResource) => Promise<WebResource>;
|
37
|
+
}
|
38
|
+
|
39
|
+
export class WebResourceError extends TypedError {}
|
64
40
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
commitInfo: CommitMultipartUploadPayload,
|
71
|
-
) => Promise<WebResource>;
|
41
|
+
export class FileSizeExceededError extends WebResourceError {
|
42
|
+
name = 'FileSizeExceededError';
|
43
|
+
constructor(maxSize: number) {
|
44
|
+
super(`File size exceeded the limit of ${maxSize} bytes.`);
|
45
|
+
}
|
72
46
|
}
|
73
47
|
|
74
48
|
type WebResourcesDbResponse = {
|
@@ -77,7 +51,7 @@ type WebResourcesDbResponse = {
|
|
77
51
|
|
78
52
|
const getLogger = (vocab?: string): Console => {
|
79
53
|
if (vocab) {
|
80
|
-
return sbvrUtils.
|
54
|
+
return sbvrUtils.logger[vocab] ?? console;
|
81
55
|
}
|
82
56
|
return console;
|
83
57
|
};
|
@@ -219,12 +193,17 @@ export const getUploaderMiddlware = (
|
|
219
193
|
next();
|
220
194
|
} catch (err: any) {
|
221
195
|
await clearFiles();
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
196
|
+
|
197
|
+
if (err instanceof FileSizeExceededError) {
|
198
|
+
return sbvrUtils.handleHttpErrors(
|
199
|
+
req,
|
200
|
+
res,
|
201
|
+
new errors.BadRequestError(err.message),
|
202
|
+
);
|
203
|
+
}
|
204
|
+
|
205
|
+
getLogger(getApiRoot(req)).error('Error uploading file', err);
|
206
|
+
next(err);
|
228
207
|
}
|
229
208
|
});
|
230
209
|
|
@@ -237,7 +216,7 @@ export const getUploaderMiddlware = (
|
|
237
216
|
};
|
238
217
|
};
|
239
218
|
|
240
|
-
|
219
|
+
const getWebResourceFields = (
|
241
220
|
request: uriParser.ODataRequest,
|
242
221
|
useTranslations = true,
|
243
222
|
): string[] => {
|
@@ -270,8 +249,6 @@ const throwIfWebresourceNotInMultipart = (
|
|
270
249
|
{ req, request }: HookArgs,
|
271
250
|
) => {
|
272
251
|
if (
|
273
|
-
request.custom.isAction !== 'beginUpload' &&
|
274
|
-
request.custom.isAction !== 'commitUpload' &&
|
275
252
|
!req.is?.('multipart') &&
|
276
253
|
webResourceFields.some((field) => request.values[field] != null)
|
277
254
|
) {
|
@@ -470,23 +447,4 @@ export const setupUploadHooks = (
|
|
470
447
|
resourceName,
|
471
448
|
getCreateWebResourceHooks(handler),
|
472
449
|
);
|
473
|
-
|
474
|
-
sbvrUtils.addPureHook(
|
475
|
-
'POST',
|
476
|
-
apiRoot,
|
477
|
-
resourceName,
|
478
|
-
multipartUploadHooks(handler),
|
479
|
-
);
|
480
|
-
};
|
481
|
-
|
482
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
483
|
-
const webresourceModel: string = require('./webresource.sbvr');
|
484
|
-
export const config = {
|
485
|
-
models: [
|
486
|
-
{
|
487
|
-
apiRoot: 'webresource',
|
488
|
-
modelText: webresourceModel,
|
489
|
-
modelName: 'webresource',
|
490
|
-
},
|
491
|
-
] as sbvrUtils.ExecutableModel[],
|
492
450
|
};
|
package/tsconfig.json
CHANGED
@@ -1,16 +0,0 @@
|
|
1
|
-
import type { BeginMultipartUploadPayload, UploadPart, WebResourceHandler } from '.';
|
2
|
-
import type { ODataRequest } from '../sbvr-api/uri-parser';
|
3
|
-
import { sbvrUtils } from '../server-glue/module';
|
4
|
-
export interface PendingUpload extends BeginMultipartUploadPayload {
|
5
|
-
fieldName: string;
|
6
|
-
fileKey: string;
|
7
|
-
uploadId: string;
|
8
|
-
}
|
9
|
-
export interface BeginUploadResponse {
|
10
|
-
[fieldName: string]: {
|
11
|
-
uuid: string;
|
12
|
-
uploadParts: UploadPart[];
|
13
|
-
};
|
14
|
-
}
|
15
|
-
export declare const multipartUploadHooks: (webResourceHandler: WebResourceHandler) => sbvrUtils.Hooks;
|
16
|
-
export declare const beginUpload: (webResourceHandler: WebResourceHandler, odataRequest: ODataRequest, actorId?: number) => Promise<BeginUploadResponse>;
|
@@ -1,182 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.beginUpload = exports.multipartUploadHooks = void 0;
|
4
|
-
const node_crypto_1 = require("node:crypto");
|
5
|
-
const _1 = require(".");
|
6
|
-
const sbvr_utils_1 = require("../sbvr-api/sbvr-utils");
|
7
|
-
const module_1 = require("../server-glue/module");
|
8
|
-
const MB = 1024 * 1024;
|
9
|
-
const multipartUploadHooks = (webResourceHandler) => {
|
10
|
-
return {
|
11
|
-
POSTPARSE: async ({ req, request, tx, api: applicationApi }) => {
|
12
|
-
if (request.odataQuery.property?.resource === 'beginUpload') {
|
13
|
-
const uploadParams = await validateBeginUpload(request, applicationApi);
|
14
|
-
tx = await module_1.sbvrUtils.db.transaction();
|
15
|
-
req.tx = tx;
|
16
|
-
request.tx = tx;
|
17
|
-
request.method = 'PATCH';
|
18
|
-
request.values = uploadParams;
|
19
|
-
request.odataQuery.resource = request.resourceName;
|
20
|
-
delete request.odataQuery.property;
|
21
|
-
request.custom.isAction = 'beginUpload';
|
22
|
-
}
|
23
|
-
else if (request.odataQuery.property?.resource === 'commitUpload') {
|
24
|
-
const commitPayload = await validateCommitUpload(request, applicationApi);
|
25
|
-
const webresource = await webResourceHandler.commitMultipartUpload({
|
26
|
-
fileKey: commitPayload.metadata.fileKey,
|
27
|
-
uploadId: commitPayload.metadata.uploadId,
|
28
|
-
filename: commitPayload.metadata.filename,
|
29
|
-
providerCommitData: commitPayload.providerCommitData,
|
30
|
-
});
|
31
|
-
await sbvr_utils_1.api.webresource.patch({
|
32
|
-
resource: 'multipart_upload',
|
33
|
-
body: {
|
34
|
-
status: 'completed',
|
35
|
-
},
|
36
|
-
options: {
|
37
|
-
$filter: {
|
38
|
-
uuid: commitPayload.uuid,
|
39
|
-
},
|
40
|
-
},
|
41
|
-
passthrough: {
|
42
|
-
tx: tx,
|
43
|
-
},
|
44
|
-
});
|
45
|
-
request.method = 'PATCH';
|
46
|
-
request.values = {
|
47
|
-
[commitPayload.metadata.fieldName]: webresource,
|
48
|
-
};
|
49
|
-
request.odataQuery.resource = request.resourceName;
|
50
|
-
delete request.odataQuery.property;
|
51
|
-
request.custom.isAction = 'commitUpload';
|
52
|
-
request.custom.commitUploadPayload = webresource;
|
53
|
-
}
|
54
|
-
},
|
55
|
-
PRERESPOND: async ({ req, request, response, tx }) => {
|
56
|
-
if (request.custom.isAction === 'beginUpload') {
|
57
|
-
await tx.rollback();
|
58
|
-
response.statusCode = 200;
|
59
|
-
response.body = await (0, exports.beginUpload)(webResourceHandler, request, req.user?.actor);
|
60
|
-
}
|
61
|
-
else if (request.custom.isAction === 'commitUpload') {
|
62
|
-
response.body = await webResourceHandler.onPreRespond(request.custom.commitUploadPayload);
|
63
|
-
}
|
64
|
-
},
|
65
|
-
};
|
66
|
-
};
|
67
|
-
exports.multipartUploadHooks = multipartUploadHooks;
|
68
|
-
const beginUpload = async (webResourceHandler, odataRequest, actorId) => {
|
69
|
-
const payload = odataRequest.values;
|
70
|
-
const fieldName = Object.keys(payload)[0];
|
71
|
-
const metadata = payload[fieldName];
|
72
|
-
const { fileKey, uploadId, uploadParts } = await webResourceHandler.beginMultipartUpload(fieldName, metadata);
|
73
|
-
const uuid = (0, node_crypto_1.randomUUID)();
|
74
|
-
try {
|
75
|
-
await sbvr_utils_1.api.webresource.post({
|
76
|
-
resource: 'multipart_upload',
|
77
|
-
body: {
|
78
|
-
uuid,
|
79
|
-
resource_name: odataRequest.resourceName,
|
80
|
-
field_name: fieldName,
|
81
|
-
resource_id: odataRequest.affectedIds?.[0],
|
82
|
-
upload_id: uploadId,
|
83
|
-
file_key: fileKey,
|
84
|
-
status: 'pending',
|
85
|
-
filename: metadata.filename,
|
86
|
-
content_type: metadata.content_type,
|
87
|
-
size: metadata.size,
|
88
|
-
chunk_size: metadata.chunk_size,
|
89
|
-
expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000,
|
90
|
-
is_created_by__actor: actorId,
|
91
|
-
},
|
92
|
-
});
|
93
|
-
}
|
94
|
-
catch (err) {
|
95
|
-
console.error('failed to start multipart upload', err);
|
96
|
-
throw new module_1.errors.BadRequestError('Failed to start multipart upload');
|
97
|
-
}
|
98
|
-
return { [fieldName]: { uuid, uploadParts } };
|
99
|
-
};
|
100
|
-
exports.beginUpload = beginUpload;
|
101
|
-
const validateBeginUpload = async (request, applicationApi) => {
|
102
|
-
if (request.odataQuery.key == null) {
|
103
|
-
throw new module_1.errors.BadRequestError();
|
104
|
-
}
|
105
|
-
await applicationApi.post({
|
106
|
-
url: request.url.substring(1).replace('beginUpload', 'canAccess'),
|
107
|
-
body: { method: 'PATCH' },
|
108
|
-
});
|
109
|
-
const fieldNames = Object.keys(request.values);
|
110
|
-
if (fieldNames.length !== 1) {
|
111
|
-
throw new module_1.errors.BadRequestError('You can only get upload url for one field at a time');
|
112
|
-
}
|
113
|
-
const [fieldName] = fieldNames;
|
114
|
-
const webResourceFields = (0, _1.getWebResourceFields)(request, false);
|
115
|
-
if (!webResourceFields.includes(fieldName)) {
|
116
|
-
throw new module_1.errors.BadRequestError(`You must provide a valid webresource field from: ${JSON.stringify(webResourceFields)}`);
|
117
|
-
}
|
118
|
-
const beginUploadPayload = parseBeginUploadPayload(request.values[fieldName]);
|
119
|
-
if (beginUploadPayload == null) {
|
120
|
-
throw new module_1.errors.BadRequestError('Invalid file metadata');
|
121
|
-
}
|
122
|
-
const uploadMetadataCheck = {
|
123
|
-
...beginUploadPayload,
|
124
|
-
href: 'metadata_check',
|
125
|
-
};
|
126
|
-
return { [fieldName]: uploadMetadataCheck };
|
127
|
-
};
|
128
|
-
const parseBeginUploadPayload = (payload) => {
|
129
|
-
if (typeof payload !== 'object') {
|
130
|
-
return null;
|
131
|
-
}
|
132
|
-
let { filename, content_type, size, chunk_size } = payload;
|
133
|
-
if (typeof filename !== 'string' ||
|
134
|
-
typeof content_type !== 'string' ||
|
135
|
-
typeof size !== 'number' ||
|
136
|
-
(chunk_size != null && typeof chunk_size !== 'number') ||
|
137
|
-
(chunk_size != null && chunk_size < 5 * MB)) {
|
138
|
-
return null;
|
139
|
-
}
|
140
|
-
if (chunk_size == null) {
|
141
|
-
chunk_size = 5 * MB;
|
142
|
-
}
|
143
|
-
return { filename, content_type, size, chunk_size };
|
144
|
-
};
|
145
|
-
const validateCommitUpload = async (request, applicationApi) => {
|
146
|
-
if (request.odataQuery.key == null) {
|
147
|
-
throw new module_1.errors.BadRequestError();
|
148
|
-
}
|
149
|
-
await applicationApi.post({
|
150
|
-
url: request.url.substring(1).replace('commitUpload', 'canAccess'),
|
151
|
-
body: { method: 'PATCH' },
|
152
|
-
});
|
153
|
-
const { uuid, providerCommitData } = request.values;
|
154
|
-
if (typeof uuid !== 'string') {
|
155
|
-
throw new module_1.errors.BadRequestError('Invalid uuid type');
|
156
|
-
}
|
157
|
-
const [multipartUpload] = (await sbvr_utils_1.api.webresource.get({
|
158
|
-
resource: 'multipart_upload',
|
159
|
-
options: {
|
160
|
-
$select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
|
161
|
-
$filter: {
|
162
|
-
uuid,
|
163
|
-
status: 'pending',
|
164
|
-
expiry_date: { $gt: { $now: {} } },
|
165
|
-
},
|
166
|
-
},
|
167
|
-
passthrough: {
|
168
|
-
tx: request.tx,
|
169
|
-
},
|
170
|
-
}));
|
171
|
-
if (multipartUpload == null) {
|
172
|
-
throw new module_1.errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
|
173
|
-
}
|
174
|
-
const metadata = {
|
175
|
-
fileKey: multipartUpload.file_key,
|
176
|
-
uploadId: multipartUpload.upload_id,
|
177
|
-
filename: multipartUpload.filename,
|
178
|
-
fieldName: multipartUpload.field_name,
|
179
|
-
};
|
180
|
-
return { uuid, providerCommitData, metadata };
|
181
|
-
};
|
182
|
-
//# sourceMappingURL=multipartUpload.js.map
|