@balena/pinejs 17.0.0-build-large-file-uploads-293e65ee371a69130834c50fef2f7f42cc18133f-1 → 17.0.0-build-17-x-74b0e8403edbd7922c684e3ce19bdb42ac41846b-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 -12
- 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 +21 -30
- 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,16 +992,7 @@ export const runRule = (() => {
|
|
992
992
|
};
|
993
993
|
})();
|
994
994
|
|
995
|
-
export
|
996
|
-
req?: {
|
997
|
-
user?: User;
|
998
|
-
};
|
999
|
-
tx?: Db.Tx;
|
1000
|
-
};
|
1001
|
-
|
1002
|
-
export class PinejsClient extends PinejsClientCore<PinejsClient> {
|
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;
|
995
|
+
export class PinejsClient extends PinejsClientCore {
|
1005
996
|
public async _request({
|
1006
997
|
method,
|
1007
998
|
url,
|
@@ -1021,11 +1012,11 @@ export class PinejsClient extends PinejsClientCore<PinejsClient> {
|
|
1021
1012
|
}
|
1022
1013
|
}
|
1023
1014
|
|
1024
|
-
export type LoggingClient = PinejsClient & {
|
1025
|
-
logger: Console;
|
1026
|
-
};
|
1027
1015
|
export const api: {
|
1028
|
-
[vocab: string]:
|
1016
|
+
[vocab: string]: PinejsClient;
|
1017
|
+
} = {};
|
1018
|
+
export const logger: {
|
1019
|
+
[vocab: string]: Console;
|
1029
1020
|
} = {};
|
1030
1021
|
|
1031
1022
|
// We default to guest only permissions if no req object is passed in
|
@@ -1224,7 +1215,7 @@ export const getModel = (vocabulary: string) => {
|
|
1224
1215
|
|
1225
1216
|
const runODataRequest = (req: Express.Request, vocabulary: string) => {
|
1226
1217
|
if (env.DEBUG) {
|
1227
|
-
|
1218
|
+
logger[vocabulary].log('Parsing', req.method, req.url);
|
1228
1219
|
}
|
1229
1220
|
|
1230
1221
|
// Get the hooks for the current method/vocabulary as we know it,
|
@@ -1535,10 +1526,10 @@ const runRequest = async (
|
|
1535
1526
|
tx: Db.Tx,
|
1536
1527
|
request: uriParser.ODataRequest,
|
1537
1528
|
): Promise<Response> => {
|
1538
|
-
const
|
1529
|
+
const log = logger[request.vocabulary];
|
1539
1530
|
|
1540
1531
|
if (env.DEBUG) {
|
1541
|
-
|
1532
|
+
log.log('Running', req.method, req.url);
|
1542
1533
|
}
|
1543
1534
|
let result: Db.Result | number | undefined;
|
1544
1535
|
|
@@ -1566,7 +1557,7 @@ const runRequest = async (
|
|
1566
1557
|
} catch (err: any) {
|
1567
1558
|
if (err instanceof db.DatabaseError) {
|
1568
1559
|
prettifyConstraintError(err, request);
|
1569
|
-
|
1560
|
+
log.error(err);
|
1570
1561
|
// Override the error message so we don't leak any internal db info
|
1571
1562
|
err.message = 'Database error';
|
1572
1563
|
throw err;
|
@@ -1580,7 +1571,7 @@ const runRequest = async (
|
|
1580
1571
|
err instanceof TypeError ||
|
1581
1572
|
err instanceof URIError
|
1582
1573
|
) {
|
1583
|
-
|
1574
|
+
log.error(err);
|
1584
1575
|
throw new InternalRequestError();
|
1585
1576
|
}
|
1586
1577
|
throw err;
|
@@ -1740,7 +1731,7 @@ const runQuery = async (
|
|
1740
1731
|
);
|
1741
1732
|
|
1742
1733
|
if (env.DEBUG) {
|
1743
|
-
|
1734
|
+
logger[vocabulary].log(query, values);
|
1744
1735
|
}
|
1745
1736
|
|
1746
1737
|
// We only add the returning clause if it's been requested and `affectedIds` hasn't been populated yet
|
@@ -1848,7 +1839,7 @@ const respondPost = async (
|
|
1848
1839
|
id,
|
1849
1840
|
);
|
1850
1841
|
if (env.DEBUG) {
|
1851
|
-
|
1842
|
+
logger[vocab].log('Insert ID: ', request.resourceName, id);
|
1852
1843
|
}
|
1853
1844
|
|
1854
1845
|
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
|