@opengis/fastify-table 2.3.0 → 2.3.2
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/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/server/plugins/upload/index.d.ts +3 -0
- package/dist/server/plugins/upload/index.d.ts.map +1 -1
- package/dist/server/plugins/upload/index.js +8 -0
- package/dist/server/plugins/upload/s3.d.ts +15 -4
- package/dist/server/plugins/upload/s3.d.ts.map +1 -1
- package/dist/server/plugins/upload/s3.js +306 -54
- package/dist/server/plugins/upload/startUpload.d.ts.map +1 -1
- package/dist/server/plugins/upload/startUpload.js +4 -2
- package/dist/server/plugins/upload/uploadChunk.d.ts.map +1 -1
- package/dist/server/plugins/upload/uploadChunk.js +18 -6
- package/dist/server/routes/table/controllers/suggest.d.ts.map +1 -1
- package/dist/server/routes/table/controllers/suggest.js +24 -5
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AA6FA,iBAAe,MAAM,CAAC,OAAO,EAAE,GAAG,iBAiLjC;;AACD,wBAEG"}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import metricPlugin from "./server/plugins/metric/index.js";
|
|
|
34
34
|
import redisPlugin from "./server/plugins/redis/index.js";
|
|
35
35
|
import loggerPlugin from "./server/plugins/logger/index.js";
|
|
36
36
|
import authPlugin from "./server/plugins/auth/index.js";
|
|
37
|
+
import chunkedUploadPlugin from "./server/plugins/upload/index.js";
|
|
37
38
|
// utils
|
|
38
39
|
import execMigrations from "./server/plugins/migration/exec.migrations.js";
|
|
39
40
|
import pgClients from "./server/plugins/pg/pgClients.js";
|
|
@@ -113,6 +114,7 @@ async function plugin(fastify) {
|
|
|
113
114
|
execMigrations(path.join(cwd, "server/migrations"), pgClients.client, true).catch((err) => console.warn(err.toString()));
|
|
114
115
|
// plugins / utils / funcs
|
|
115
116
|
authPlugin(fastify); // fastify-auth api + hooks integrated to core
|
|
117
|
+
chunkedUploadPlugin(fastify);
|
|
116
118
|
policyPlugin(fastify);
|
|
117
119
|
metricPlugin();
|
|
118
120
|
redisPlugin(fastify);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
|
1
2
|
export declare const fetchTimeoutMs: number;
|
|
2
3
|
export declare const prefix = "file/upload2";
|
|
3
4
|
export declare const uploadChunkDirectory = "/files/uploads";
|
|
4
5
|
export declare const fileDir: string;
|
|
5
6
|
export declare const metaDir: string;
|
|
7
|
+
declare function chunkedUploadPlugin(app: FastifyInstance): void;
|
|
8
|
+
export default chunkedUploadPlugin;
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAW1C,eAAO,MAAM,cAAc,QAE1B,CAAC;AAEF,eAAO,MAAM,MAAM,iBAAiB,CAAC;AAErC,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD,eAAO,MAAM,OAAO,QAEE,CAAC;AAEvB,eAAO,MAAM,OAAO,QAA4B,CAAC;AAKjD,iBAAS,mBAAmB,CAAC,GAAG,EAAE,eAAe,QAIhD;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -10,3 +10,11 @@ export const fileDir = path
|
|
|
10
10
|
.join(rootDir, uploadChunkDirectory)
|
|
11
11
|
.replace(/\\/g, "/");
|
|
12
12
|
export const metaDir = path.join(fileDir, "tmp");
|
|
13
|
+
import { cleanupNonRedisUploads } from "./s3.js";
|
|
14
|
+
// w/ out redis - chunked uploads are non-resumable in order to avoid unclosed multiparts (non-discoverable via s3 commands with current version of minio)
|
|
15
|
+
function chunkedUploadPlugin(app) {
|
|
16
|
+
app.addHook("onClose", async () => {
|
|
17
|
+
await cleanupNonRedisUploads();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export default chunkedUploadPlugin;
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export declare function cleanupNonRedisUploads(s3?: any): Promise<void>;
|
|
2
|
+
export declare function saveUploadMeta(id: string, meta: any, ttl?: number): Promise<void>;
|
|
3
|
+
export declare function refreshUploadTTL(id: string, ttl?: number, s3?: any): Promise<void>;
|
|
4
|
+
export declare function deleteUploadMeta(id: string, key?: string): Promise<void>;
|
|
5
|
+
export declare function getUploadMeta(id: string): Promise<any>;
|
|
6
|
+
export declare function getResumableUploadId(key: string): Promise<any>;
|
|
7
|
+
export declare function cleanupStaleUploads(s3?: any): Promise<void>;
|
|
1
8
|
export declare function findUpload({ id }: {
|
|
2
9
|
id: string;
|
|
3
10
|
}, s3?: any): Promise<{
|
|
@@ -9,16 +16,13 @@ export declare function findUpload({ id }: {
|
|
|
9
16
|
uploaded: any;
|
|
10
17
|
parts: any;
|
|
11
18
|
}>;
|
|
12
|
-
export declare function listUploads({ Bucket }: {
|
|
13
|
-
Bucket: string;
|
|
14
|
-
}, s3?: any): Promise<any>;
|
|
15
19
|
export declare function startUpload({ Bucket, Key, ContentType, fileSize, }: {
|
|
16
20
|
Bucket: string;
|
|
17
21
|
Key: string;
|
|
18
22
|
ContentType: string;
|
|
19
23
|
fileSize: number;
|
|
20
24
|
}, s3?: any): Promise<{
|
|
21
|
-
Key:
|
|
25
|
+
Key: string;
|
|
22
26
|
Bucket: any;
|
|
23
27
|
UploadId: any;
|
|
24
28
|
fileSize: any;
|
|
@@ -50,7 +54,14 @@ export declare function uploadChunk({ id, chunk, partNumber, }: {
|
|
|
50
54
|
export declare function finishUpload({ id, }: {
|
|
51
55
|
id: string;
|
|
52
56
|
}, s3?: any): Promise<{
|
|
57
|
+
finished: boolean;
|
|
53
58
|
uploaded: any;
|
|
59
|
+
filepath: any;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function abortUpload({ id, }: {
|
|
62
|
+
id: string;
|
|
63
|
+
}, s3?: any): Promise<{
|
|
64
|
+
aborted: boolean;
|
|
54
65
|
}>;
|
|
55
66
|
export declare function getUploadStatus({ id, Key, Bucket, }: {
|
|
56
67
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/s3.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/s3.ts"],"names":[],"mappings":"AAiGA,wBAAsB,sBAAsB,CAAC,EAAE,MAAW,iBAwCzD;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,GAAG,EACT,GAAG,SAAqB,iBAazB;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,GAAG,SAAqB,EACxB,EAAE,MAAW,iBAsBd;AAED,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,iBA0B9D;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,gBAQ7C;AAED,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,gBAMrD;AAED,wBAAsB,mBAAmB,CAAC,EAAE,MAAW,iBAyEtD;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,MAAW;;;;;;;;GAsCrE;AAKD,wBAAsB,WAAW,CAC/B,EACE,MAAM,EACN,GAAG,EACH,WAAW,EACX,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,EACD,EAAE,MAAW;;;;;;;;;;;;;;;;GAsDd;AAED,wBAAsB,WAAW,CAC/B,EACE,EAAE,EACF,KAAK,EACL,UAAU,GACX,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB,EACD,EAAE,MAAW;;;;;;;;GA0Bd;AAED,wBAAsB,YAAY,CAChC,EACE,EAAE,GACH,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;CACZ,EACD,EAAE,MAAW;;;;GAuCd;AAED,wBAAsB,WAAW,CAC/B,EACE,EAAE,GACH,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;CACZ,EACD,EAAE,MAAW;;GAwBd;AAED,wBAAsB,eAAe,CACnC,EACE,EAAE,EACF,GAAG,EACH,MAAM,GACP,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,EACD,EAAE,MAAW;;;;;;;;;;;;;;GAoEd"}
|
|
@@ -1,57 +1,272 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand,
|
|
2
|
+
import { CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, HeadObjectCommand, ListPartsCommand, AbortMultipartUploadCommand, } from "@aws-sdk/client-s3";
|
|
3
3
|
import config from "../../../config.js";
|
|
4
4
|
import s3Client from "../file/providers/s3/client.js";
|
|
5
5
|
import rclient from "../redis/client.js";
|
|
6
|
+
import logger from "../logger/getLogger.js";
|
|
6
7
|
const uploads = new Map();
|
|
8
|
+
const resumableUploads = new Map();
|
|
9
|
+
// non-redis upload timeout handlers
|
|
10
|
+
const uploadTimeouts = new Map();
|
|
11
|
+
const UPLOAD_TTL_SECONDS = 60 * 60;
|
|
12
|
+
// expire gracefully to give cleanup worker time to abort stale uploads
|
|
13
|
+
const CLEANUP_GRACE_SECONDS = 60 * 60 * 24;
|
|
14
|
+
const REDIS_TTL = UPLOAD_TTL_SECONDS + CLEANUP_GRACE_SECONDS;
|
|
15
|
+
const uploadMetaKey = (id) => `upload:${id}:meta`;
|
|
16
|
+
const uploadExpireKey = (id) => `upload:${id}:expires`;
|
|
17
|
+
const resumableKey = (key) => `resumable:${key}`;
|
|
18
|
+
function clearUploadTimeout(id) {
|
|
19
|
+
const timeout = uploadTimeouts.get(id);
|
|
20
|
+
if (timeout) {
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
uploadTimeouts.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function scheduleNonRedisCleanup(id, s3 = s3Client) {
|
|
26
|
+
if (!s3)
|
|
27
|
+
throw new Error("No S3 client");
|
|
28
|
+
if (config.redis)
|
|
29
|
+
return;
|
|
30
|
+
clearUploadTimeout(id);
|
|
31
|
+
const timeout = setTimeout(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const meta = uploads.get(id);
|
|
34
|
+
if (!meta)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
38
|
+
Bucket: meta.Bucket,
|
|
39
|
+
Key: meta.Key,
|
|
40
|
+
UploadId: id,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error("AbortMultipartUpload failed", {
|
|
45
|
+
uploadId: id,
|
|
46
|
+
Bucket: meta.Bucket,
|
|
47
|
+
Key: meta.Key,
|
|
48
|
+
error: err?.message,
|
|
49
|
+
});
|
|
50
|
+
logger.file("upload/abort/error", {
|
|
51
|
+
uploadId: id,
|
|
52
|
+
Bucket: meta.Bucket,
|
|
53
|
+
Key: meta.Key,
|
|
54
|
+
error: err?.message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
uploads.delete(id);
|
|
58
|
+
if (resumableUploads.get(meta.Key) === id) {
|
|
59
|
+
resumableUploads.delete(meta.Key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(err);
|
|
64
|
+
logger.error(err);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearUploadTimeout(id);
|
|
68
|
+
}
|
|
69
|
+
}, UPLOAD_TTL_SECONDS * 1000);
|
|
70
|
+
// don't keep node process alive
|
|
71
|
+
timeout.unref?.();
|
|
72
|
+
uploadTimeouts.set(id, timeout);
|
|
73
|
+
}
|
|
74
|
+
// used via onClose hook to avoid memory leaks
|
|
75
|
+
export async function cleanupNonRedisUploads(s3 = s3Client) {
|
|
76
|
+
if (!s3)
|
|
77
|
+
throw new Error("No S3 client");
|
|
78
|
+
if (config.redis)
|
|
79
|
+
return;
|
|
80
|
+
console.log("cleanupNonRedisUploads start");
|
|
81
|
+
const entries = [...uploads.entries()];
|
|
82
|
+
for (const [id, meta] of entries) {
|
|
83
|
+
try {
|
|
84
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
85
|
+
Bucket: meta.Bucket,
|
|
86
|
+
Key: meta.Key,
|
|
87
|
+
UploadId: id,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error("AbortMultipartUpload failed", {
|
|
92
|
+
uploadId: id,
|
|
93
|
+
Bucket: meta.Bucket,
|
|
94
|
+
Key: meta.Key,
|
|
95
|
+
error: err?.message,
|
|
96
|
+
});
|
|
97
|
+
logger.file("upload/abort/error", {
|
|
98
|
+
uploadId: id,
|
|
99
|
+
Bucket: meta.Bucket,
|
|
100
|
+
Key: meta.Key,
|
|
101
|
+
error: err?.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
clearUploadTimeout(id);
|
|
105
|
+
uploads.delete(id);
|
|
106
|
+
if (resumableUploads.get(meta.Key) === id) {
|
|
107
|
+
resumableUploads.delete(meta.Key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.log("open unfinished multipart uploads closed", entries.length);
|
|
111
|
+
}
|
|
112
|
+
export async function saveUploadMeta(id, meta, ttl = UPLOAD_TTL_SECONDS) {
|
|
113
|
+
if (!config.redis)
|
|
114
|
+
return;
|
|
115
|
+
const expiresAt = Date.now() + UPLOAD_TTL_SECONDS * 1000;
|
|
116
|
+
await Promise.all([
|
|
117
|
+
rclient.set(uploadMetaKey(id), JSON.stringify(meta), "EX", REDIS_TTL),
|
|
118
|
+
rclient.set(uploadExpireKey(id), String(expiresAt), "EX", ttl),
|
|
119
|
+
rclient.set(resumableKey(meta.Key), id, "EX", ttl),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
export async function refreshUploadTTL(id, ttl = UPLOAD_TTL_SECONDS, s3 = s3Client) {
|
|
123
|
+
if (!s3)
|
|
124
|
+
throw new Error("No S3 client");
|
|
125
|
+
if (!config.redis) {
|
|
126
|
+
await scheduleNonRedisCleanup(id, s3);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const meta = await getUploadMeta(id);
|
|
130
|
+
if (!meta)
|
|
131
|
+
return;
|
|
132
|
+
const expiresAt = Date.now() + ttl * 1000;
|
|
133
|
+
await Promise.all([
|
|
134
|
+
rclient.expire(uploadMetaKey(id), ttl),
|
|
135
|
+
rclient.set(uploadExpireKey(id), String(expiresAt), "EX", ttl),
|
|
136
|
+
rclient.expire(resumableKey(meta.Key), ttl),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
export async function deleteUploadMeta(id, key) {
|
|
140
|
+
clearUploadTimeout(id);
|
|
141
|
+
if (!config.redis) {
|
|
142
|
+
const meta = uploads.get(id);
|
|
143
|
+
uploads.delete(id);
|
|
144
|
+
const redisKey = key || meta?.Key;
|
|
145
|
+
if (redisKey && resumableUploads.get(redisKey) === id) {
|
|
146
|
+
resumableUploads.delete(redisKey);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const redisKey = key || (await getUploadMeta(id))?.Key;
|
|
151
|
+
const keys = [uploadMetaKey(id), uploadExpireKey(id)];
|
|
152
|
+
if (redisKey) {
|
|
153
|
+
keys.push(resumableKey(redisKey));
|
|
154
|
+
}
|
|
155
|
+
await rclient.del(keys);
|
|
156
|
+
}
|
|
157
|
+
export async function getUploadMeta(id) {
|
|
158
|
+
if (!config.redis) {
|
|
159
|
+
return uploads.get(id);
|
|
160
|
+
}
|
|
161
|
+
const raw = await rclient.get(uploadMetaKey(id));
|
|
162
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
163
|
+
}
|
|
164
|
+
export async function getResumableUploadId(key) {
|
|
165
|
+
if (!config.redis) {
|
|
166
|
+
return resumableUploads.get(key);
|
|
167
|
+
}
|
|
168
|
+
return rclient.get(resumableKey(key));
|
|
169
|
+
}
|
|
170
|
+
export async function cleanupStaleUploads(s3 = s3Client) {
|
|
171
|
+
if (!s3)
|
|
172
|
+
throw new Error("No S3 client");
|
|
173
|
+
if (!config.redis) {
|
|
174
|
+
await cleanupNonRedisUploads(s3);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let cursor = "0";
|
|
178
|
+
do {
|
|
179
|
+
const [nextCursor, keys] = await rclient.scan(cursor, "MATCH", "upload:*:expires", "COUNT", 100);
|
|
180
|
+
cursor = nextCursor;
|
|
181
|
+
for (const expireKey of keys) {
|
|
182
|
+
try {
|
|
183
|
+
const expiresAt = Number(await rclient.get(expireKey));
|
|
184
|
+
if (!expiresAt || expiresAt > Date.now()) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const uploadId = expireKey
|
|
188
|
+
.replace("upload:", "")
|
|
189
|
+
.replace(":expires", "");
|
|
190
|
+
const meta = await getUploadMeta(uploadId);
|
|
191
|
+
if (!meta) {
|
|
192
|
+
await rclient.del(expireKey);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const { Bucket, Key } = meta;
|
|
196
|
+
try {
|
|
197
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
198
|
+
Bucket,
|
|
199
|
+
Key,
|
|
200
|
+
UploadId: uploadId,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.error("AbortMultipartUpload failed", {
|
|
205
|
+
uploadId,
|
|
206
|
+
Bucket,
|
|
207
|
+
Key,
|
|
208
|
+
error: err?.message,
|
|
209
|
+
});
|
|
210
|
+
logger.file("upload/abort/error", {
|
|
211
|
+
uploadId,
|
|
212
|
+
Bucket,
|
|
213
|
+
Key,
|
|
214
|
+
error: err?.message,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
await deleteUploadMeta(uploadId, Key);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
console.error(err);
|
|
221
|
+
logger.error(err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} while (cursor !== "0");
|
|
225
|
+
}
|
|
7
226
|
export async function findUpload({ id }, s3 = s3Client) {
|
|
8
|
-
|
|
9
|
-
|
|
227
|
+
if (!s3)
|
|
228
|
+
throw new Error("No S3 client");
|
|
229
|
+
const cachedInfo = await getUploadMeta(id);
|
|
230
|
+
const { Key, Bucket, fileSize: cacheSize, ContentType: cacheContentType, } = cachedInfo || {};
|
|
10
231
|
const parts = await s3
|
|
11
232
|
.send(new ListPartsCommand({
|
|
12
233
|
Bucket,
|
|
13
234
|
Key,
|
|
14
235
|
UploadId: id,
|
|
15
236
|
}))
|
|
16
|
-
.catch((
|
|
237
|
+
.catch(() => null);
|
|
17
238
|
return {
|
|
18
239
|
UploadId: parts ? id : undefined,
|
|
19
240
|
Key,
|
|
20
241
|
Bucket,
|
|
21
242
|
fileSize: cacheSize,
|
|
22
243
|
ContentType: cacheContentType,
|
|
23
|
-
uploaded: parts
|
|
24
|
-
|
|
25
|
-
parts: parts.Parts?.map((p) => ({
|
|
244
|
+
uploaded: parts?.Parts?.reduce((sum, part) => sum + (part.Size || 0), 0) || 0,
|
|
245
|
+
parts: parts?.Parts?.map((p) => ({
|
|
26
246
|
ETag: p.ETag,
|
|
27
247
|
PartNumber: p.PartNumber,
|
|
28
248
|
})) || [],
|
|
29
249
|
};
|
|
30
250
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
return res.Uploads.map((upload) => ({
|
|
37
|
-
key: upload.Key,
|
|
38
|
-
uploadId: upload.UploadId,
|
|
39
|
-
initiated: upload.Initiated,
|
|
40
|
-
owner: upload.Owner?.DisplayName,
|
|
41
|
-
storageClass: upload.StorageClass,
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
251
|
+
// todo: optional - keep originalFileName, else - id + extension
|
|
252
|
+
// + implement rotateFilenameS3 logic, like const filename = await rotateFilenameS3({ Name: originalFilename, Bucket, Prefix }, s3Client);
|
|
253
|
+
// ! if originalFileName - check names with spaces - s3 mights throw error - encode request or at upload?
|
|
44
254
|
export async function startUpload({ Bucket, Key, ContentType, fileSize, }, s3 = s3Client) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
255
|
+
if (!s3)
|
|
256
|
+
throw new Error("No S3 client");
|
|
257
|
+
const cachedUploadId = await getResumableUploadId(Key);
|
|
258
|
+
const cachedMeta = cachedUploadId
|
|
259
|
+
? await getUploadMeta(cachedUploadId)
|
|
260
|
+
: undefined;
|
|
261
|
+
if (cachedUploadId && cachedMeta) {
|
|
262
|
+
await refreshUploadTTL(cachedUploadId, UPLOAD_TTL_SECONDS, s3);
|
|
48
263
|
return {
|
|
49
|
-
Key
|
|
50
|
-
Bucket:
|
|
51
|
-
UploadId,
|
|
52
|
-
fileSize:
|
|
53
|
-
extension:
|
|
54
|
-
ContentType:
|
|
264
|
+
Key,
|
|
265
|
+
Bucket: cachedMeta.Bucket,
|
|
266
|
+
UploadId: cachedUploadId,
|
|
267
|
+
fileSize: cachedMeta.fileSize,
|
|
268
|
+
extension: cachedMeta.extension,
|
|
269
|
+
ContentType: cachedMeta.ContentType,
|
|
55
270
|
cache: true,
|
|
56
271
|
};
|
|
57
272
|
}
|
|
@@ -61,22 +276,29 @@ export async function startUpload({ Bucket, Key, ContentType, fileSize, }, s3 =
|
|
|
61
276
|
ContentType,
|
|
62
277
|
}));
|
|
63
278
|
const extension = path.extname(Key).substring(1);
|
|
279
|
+
const meta = {
|
|
280
|
+
Key,
|
|
281
|
+
Bucket,
|
|
282
|
+
fileSize,
|
|
283
|
+
extension,
|
|
284
|
+
ContentType,
|
|
285
|
+
};
|
|
64
286
|
if (config.redis) {
|
|
65
|
-
await
|
|
287
|
+
await saveUploadMeta(res.UploadId, meta);
|
|
66
288
|
}
|
|
67
289
|
else {
|
|
68
|
-
uploads.set(res.UploadId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fileSize,
|
|
72
|
-
extension,
|
|
73
|
-
ContentType,
|
|
74
|
-
});
|
|
290
|
+
uploads.set(res.UploadId, meta);
|
|
291
|
+
resumableUploads.set(Key, res.UploadId);
|
|
292
|
+
await scheduleNonRedisCleanup(res.UploadId, s3);
|
|
75
293
|
}
|
|
76
|
-
return {
|
|
294
|
+
return {
|
|
295
|
+
UploadId: res.UploadId,
|
|
296
|
+
};
|
|
77
297
|
}
|
|
78
298
|
export async function uploadChunk({ id, chunk, partNumber, }, s3 = s3Client) {
|
|
79
|
-
|
|
299
|
+
if (!s3)
|
|
300
|
+
throw new Error("No S3 client");
|
|
301
|
+
const upload = await findUpload({ id }, s3);
|
|
80
302
|
if (!upload?.UploadId) {
|
|
81
303
|
throw new Error("upload not found");
|
|
82
304
|
}
|
|
@@ -90,17 +312,20 @@ export async function uploadChunk({ id, chunk, partNumber, }, s3 = s3Client) {
|
|
|
90
312
|
PartNumber: partNumber,
|
|
91
313
|
Body: chunk,
|
|
92
314
|
}));
|
|
315
|
+
await refreshUploadTTL(id, UPLOAD_TTL_SECONDS, s3);
|
|
93
316
|
return upload;
|
|
94
317
|
}
|
|
95
318
|
export async function finishUpload({ id, }, s3 = s3Client) {
|
|
96
|
-
|
|
97
|
-
|
|
319
|
+
if (!s3)
|
|
320
|
+
throw new Error("No S3 client");
|
|
321
|
+
const redisCache = await getUploadMeta(id);
|
|
322
|
+
const { fileSize: cachedSize, Key, Bucket } = redisCache || {};
|
|
98
323
|
const size = cachedSize ? Number(cachedSize) : undefined;
|
|
99
324
|
const upload = await findUpload({ id }, s3);
|
|
100
325
|
if (!upload?.uploaded) {
|
|
101
326
|
throw new Error("File not uploaded");
|
|
102
327
|
}
|
|
103
|
-
if (size && size
|
|
328
|
+
if (size && size !== upload.uploaded) {
|
|
104
329
|
throw new Error("File size mismatch");
|
|
105
330
|
}
|
|
106
331
|
await s3.send(new CompleteMultipartUploadCommand({
|
|
@@ -111,21 +336,48 @@ export async function finishUpload({ id, }, s3 = s3Client) {
|
|
|
111
336
|
Parts: upload.parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
112
337
|
},
|
|
113
338
|
}));
|
|
114
|
-
|
|
115
|
-
return {
|
|
339
|
+
await deleteUploadMeta(id, Key);
|
|
340
|
+
return {
|
|
341
|
+
finished: true,
|
|
342
|
+
uploaded: upload.uploaded,
|
|
343
|
+
filepath: Key.replace(config.folder, ""),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
export async function abortUpload({ id, }, s3 = s3Client) {
|
|
347
|
+
if (!s3)
|
|
348
|
+
throw new Error("No S3 client");
|
|
349
|
+
const meta = await getUploadMeta(id);
|
|
350
|
+
if (!meta) {
|
|
351
|
+
return {
|
|
352
|
+
aborted: false,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
356
|
+
Bucket: meta.Bucket,
|
|
357
|
+
Key: meta.Key,
|
|
358
|
+
UploadId: id,
|
|
359
|
+
}));
|
|
360
|
+
await deleteUploadMeta(id, meta.Key);
|
|
361
|
+
return {
|
|
362
|
+
aborted: true,
|
|
363
|
+
};
|
|
116
364
|
}
|
|
117
365
|
export async function getUploadStatus({ id, Key, Bucket, }, s3 = s3Client) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
366
|
+
if (!s3)
|
|
367
|
+
throw new Error("No S3 client");
|
|
368
|
+
const redisCache = id ? await getUploadMeta(id) : undefined;
|
|
369
|
+
const { fileSize: cachedSize, Key: redisKey, Bucket: redisBucket, } = redisCache || {};
|
|
121
370
|
if (!redisKey && Key && Bucket) {
|
|
122
371
|
const res = await s3
|
|
123
372
|
.send(new HeadObjectCommand({
|
|
124
373
|
Bucket,
|
|
125
374
|
Key,
|
|
126
375
|
}))
|
|
127
|
-
.catch((
|
|
128
|
-
return {
|
|
376
|
+
.catch(() => false);
|
|
377
|
+
return {
|
|
378
|
+
exists: !!res,
|
|
379
|
+
uploaded: res?.ContentLength,
|
|
380
|
+
};
|
|
129
381
|
}
|
|
130
382
|
const parts = await s3
|
|
131
383
|
.send(new ListPartsCommand({
|
|
@@ -133,7 +385,7 @@ export async function getUploadStatus({ id, Key, Bucket, }, s3 = s3Client) {
|
|
|
133
385
|
Key: redisKey,
|
|
134
386
|
UploadId: id,
|
|
135
387
|
}))
|
|
136
|
-
.catch((
|
|
388
|
+
.catch(() => ({
|
|
137
389
|
Parts: [],
|
|
138
390
|
}));
|
|
139
391
|
const res = id && !parts.Parts?.length
|
|
@@ -142,17 +394,17 @@ export async function getUploadStatus({ id, Key, Bucket, }, s3 = s3Client) {
|
|
|
142
394
|
Bucket: redisBucket,
|
|
143
395
|
Key: redisKey,
|
|
144
396
|
}))
|
|
145
|
-
.catch((
|
|
397
|
+
.catch(() => false)
|
|
146
398
|
: false;
|
|
147
399
|
return {
|
|
148
|
-
exists: !!res || !!parts
|
|
400
|
+
exists: !!res || !!parts?.Parts?.length,
|
|
149
401
|
finished: !!res,
|
|
150
402
|
uploaded: res
|
|
151
403
|
? res.ContentLength
|
|
152
|
-
: parts
|
|
404
|
+
: parts?.Parts?.reduce((acc, curr) => acc + (curr.Size || 0), 0) || 0,
|
|
153
405
|
id,
|
|
154
406
|
size: cachedSize ? Number(cachedSize) : undefined,
|
|
155
|
-
parts: parts
|
|
407
|
+
parts: parts?.Parts?.map((p) => ({
|
|
156
408
|
ETag: p.ETag,
|
|
157
409
|
PartNumber: p.PartNumber,
|
|
158
410
|
})) || [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startUpload.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/startUpload.ts"],"names":[],"mappings":"AAiBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,QAAQ,EACR,IAAI,EACJ,MAAM,GACP,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,
|
|
1
|
+
{"version":3,"file":"startUpload.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/startUpload.ts"],"names":[],"mappings":"AAiBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,QAAQ,EACR,IAAI,EACJ,MAAM,GACP,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,gBA+EA"}
|
|
@@ -20,18 +20,19 @@ export default async function startUpload({ host, id, fileName, size, subdir, })
|
|
|
20
20
|
.join(uploadChunkDirectory, subdir || "")
|
|
21
21
|
.replace(/\\/g, "/");
|
|
22
22
|
if (config.s3) {
|
|
23
|
-
const filepath = path.posix.join(relativeDirpath, `${id1}.${extension}`);
|
|
23
|
+
const filepath = path.posix.join(config.folder || '', relativeDirpath, `${id1}.${extension}`);
|
|
24
24
|
const { UploadId } = await startUploadS3({
|
|
25
25
|
Bucket: config.s3?.containerName || "work",
|
|
26
26
|
Key: filepath[0] === "/" ? filepath?.slice(1) : filepath,
|
|
27
27
|
ContentType: mimes[extension],
|
|
28
28
|
fileSize: size,
|
|
29
29
|
});
|
|
30
|
-
return { UploadId, provider: "s3" };
|
|
30
|
+
return { id: UploadId, UploadId, provider: "s3" };
|
|
31
31
|
}
|
|
32
32
|
const key = id1.split("-").pop();
|
|
33
33
|
const meta = {
|
|
34
34
|
id: id1,
|
|
35
|
+
UploadId: id1,
|
|
35
36
|
key,
|
|
36
37
|
fileName,
|
|
37
38
|
size,
|
|
@@ -56,6 +57,7 @@ export default async function startUpload({ host, id, fileName, size, subdir, })
|
|
|
56
57
|
subdir,
|
|
57
58
|
metaPath: undefined,
|
|
58
59
|
relativeDirpath,
|
|
60
|
+
provider: "fs"
|
|
59
61
|
};
|
|
60
62
|
await writeFile(path.join(metaDir, `${id1}.json`), JSON.stringify(metaData, null, 2));
|
|
61
63
|
return { ...meta, metaPath: undefined };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/uploadChunk.ts"],"names":[],"mappings":"AAqBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,IAAI,EACJ,MAAM,EACN,GAAG,EACH,IAAI,GACL,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,
|
|
1
|
+
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/uploadChunk.ts"],"names":[],"mappings":"AAqBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,IAAI,EACJ,MAAM,EACN,GAAG,EACH,IAAI,GACL,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,gBAwIA"}
|
|
@@ -39,14 +39,16 @@ export default async function uploadChunk({ host, id, body, offset, end, size, }
|
|
|
39
39
|
return { error: "Chunk size mismatch", code: 400 };
|
|
40
40
|
}
|
|
41
41
|
const relpath = path.posix.join(uploadChunkDirectory, `${id}.${meta.extension}`);
|
|
42
|
-
const filepath = path.posix.join(config.folder, uploadChunkDirectory, `${id}.${meta.extension}`);
|
|
43
42
|
const res = await getFileSize(relpath, { fallback: false }).catch((err) => ({
|
|
44
43
|
error: err.toString(),
|
|
44
|
+
code: err.code,
|
|
45
45
|
}));
|
|
46
|
-
if (res.error &&
|
|
46
|
+
if (res.error &&
|
|
47
|
+
res.error !== "Error: file not found" &&
|
|
48
|
+
res.code !== "ENOENT") {
|
|
47
49
|
throw new Error(res.error);
|
|
48
50
|
}
|
|
49
|
-
if (
|
|
51
|
+
if (!res.error && res > meta.size) {
|
|
50
52
|
throw new Error("uploaded size exceeds file size");
|
|
51
53
|
}
|
|
52
54
|
// append chunk to existing file
|
|
@@ -67,8 +69,16 @@ export default async function uploadChunk({ host, id, body, offset, end, size, }
|
|
|
67
69
|
// save metadata
|
|
68
70
|
const fileBytes = Buffer.from(JSON.stringify(metaData, null, 2));
|
|
69
71
|
await writeFile(path.join(metaDir, `${id}.json`), fileBytes);
|
|
70
|
-
const payload = {
|
|
71
|
-
|
|
72
|
+
const payload = {
|
|
73
|
+
uploaded: meta.uploaded,
|
|
74
|
+
finished,
|
|
75
|
+
filepath: path.posix.join(meta.relativeDirpath, meta.fileName),
|
|
76
|
+
};
|
|
77
|
+
const hookData = await applyHook("afterChunkedUpload", {
|
|
78
|
+
id,
|
|
79
|
+
meta,
|
|
80
|
+
payload,
|
|
81
|
+
});
|
|
72
82
|
return hookData || payload;
|
|
73
83
|
}
|
|
74
84
|
return { uploaded: meta.uploaded, finished };
|
|
@@ -78,7 +88,9 @@ export default async function uploadChunk({ host, id, body, offset, end, size, }
|
|
|
78
88
|
}
|
|
79
89
|
const url = `${host}/${prefix}/${id}`;
|
|
80
90
|
const range = `bytes ${offset || 0}-${end}/${size}`;
|
|
81
|
-
|
|
91
|
+
if (config.trace || process.env.NODE_ENV === "test") {
|
|
92
|
+
console.log("PATCH request", url, range, body.length);
|
|
93
|
+
}
|
|
82
94
|
const res = await fetch(url, {
|
|
83
95
|
method: "PATCH",
|
|
84
96
|
headers: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/controllers/suggest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAuBpD,wBAAsB,kBAAkB,CAAC,EACvC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,UAAU,EACV,GAAG,EACH,EAAqB,GACtB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,UAAU,CAAC;CACjB;;;;;
|
|
1
|
+
{"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/controllers/suggest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAuBpD,wBAAsB,kBAAkB,CAAC,EACvC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,UAAU,EACV,GAAG,EACH,EAAqB,GACtB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,UAAU,CAAC;CACjB;;;;;UAwFA;AAED,wBAA8B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBAyXzD"}
|
|
@@ -27,6 +27,14 @@ export async function getTableColumnMeta({ table, template, column, selectName,
|
|
|
27
27
|
.filter((el) => !el.disabled && el.sql && el.sql.includes(column) && false)
|
|
28
28
|
.map((el, i) => ` left join lateral (${el.sql}) ${el.name || `t${i}`} on 1=1 `)
|
|
29
29
|
?.join("") || "";
|
|
30
|
+
if (arr?.length && !arr.find((el) => el.id === "null")) {
|
|
31
|
+
arr.push({
|
|
32
|
+
color: "#B8B8B8",
|
|
33
|
+
icon: null,
|
|
34
|
+
id: "null",
|
|
35
|
+
text: "None",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
30
38
|
// w/out select
|
|
31
39
|
const searchQueryDefault = startsWith && key
|
|
32
40
|
? `(left(lower("text"::text),${key.length}) = $1 )`
|
|
@@ -176,7 +184,7 @@ export default async function suggest(req, reply) {
|
|
|
176
184
|
if (tableName && pg?.pk?.[tableName] && columnName) {
|
|
177
185
|
const qRes = await pg.queryCache(sqlCls, { table: tableName });
|
|
178
186
|
const vals = (query.count
|
|
179
|
-
? qRes.rows?.map?.((el) => el.value?.toString?.())
|
|
187
|
+
? qRes.rows?.map?.((el) => el.value?.toString?.() || null)
|
|
180
188
|
: qRes.rows?.[0]?.array_agg) || [];
|
|
181
189
|
const lower = query.key?.toLowerCase?.();
|
|
182
190
|
const data1 = query.key || query.val
|
|
@@ -185,7 +193,9 @@ export default async function suggest(req, reply) {
|
|
|
185
193
|
(el[lang] || el.text)?.toLowerCase()?.indexOf(lower) !== -1)
|
|
186
194
|
?.filter((el) => !query.val || query.val?.split?.(",")?.includes?.(el.id))
|
|
187
195
|
: arr;
|
|
188
|
-
const data2 = data1.filter((el) => el.id
|
|
196
|
+
const data2 = data1.filter((el) => el.id === "null"
|
|
197
|
+
? vals.includes(null)
|
|
198
|
+
: el.id && vals.includes(el.id.toString()));
|
|
189
199
|
const data = data2.slice(0, Math.min(query.limit || limit, limit));
|
|
190
200
|
if (data.length && query.count) {
|
|
191
201
|
const counts = qRes.rows?.reduce?.((acc, curr) => ({
|
|
@@ -245,7 +255,7 @@ export default async function suggest(req, reply) {
|
|
|
245
255
|
};
|
|
246
256
|
}
|
|
247
257
|
// select, with search by text support
|
|
248
|
-
const search = query.key ? searchQuery : null;
|
|
258
|
+
const search = typeof query.key === "string" ? searchQuery : null;
|
|
249
259
|
// filter by value
|
|
250
260
|
const val = query.val
|
|
251
261
|
? `${meta.pk}=any('{${query.val.replace(/'/g, "''")}}')`
|
|
@@ -258,13 +268,22 @@ export default async function suggest(req, reply) {
|
|
|
258
268
|
const where = [search, val, filter, meta.pk ? `${meta.pk} is not null` : null]
|
|
259
269
|
.filter(Boolean)
|
|
260
270
|
.join(" and ") || "true";
|
|
261
|
-
const sqlSuggest = `with c(id,text) as ( ${meta.original.replace(/{{parent}}/gi, parent)} where ${where} ${meta.original.toLowerCase().includes("order by") ? "" : "order by 2"}) select * from c where ${filter} limit ${Math.min(query.limit || meta.limit || limit, limit)}`.replace(/{{uid}}/g, user?.uid || "0");
|
|
271
|
+
// const sqlSuggest = `with c(id,text) as ( ${meta.original.replace(/{{parent}}/gi, parent)} where ${where} ${meta.original.toLowerCase().includes("order by") ? "" : "order by 2"}) select * from c where ${filter} limit ${Math.min(query.limit || meta.limit || limit, limit)}`.replace(/{{uid}}/g, user?.uid || "0");
|
|
272
|
+
const sqlSuggest = `${meta.original}
|
|
273
|
+
WHERE EXISTS ( SELECT 1 FROM ${tableName} o WHERE o.${column} IS NOT DISTINCT FROM c.id ) and ${val || "true"} and ${search || "true"}
|
|
274
|
+
${meta.original.toLowerCase().includes("order by") ? "" : "order by 2"}
|
|
275
|
+
LIMIT ${Math.min(query.limit || meta.limit || limit, limit)}`
|
|
276
|
+
.replace(/{{parent}}/gi, parent)
|
|
277
|
+
.replace(/{{uid}}/g, user?.uid || "0");
|
|
262
278
|
if (query.sql && debugMode) {
|
|
263
279
|
return sqlSuggest;
|
|
264
280
|
}
|
|
265
|
-
const { rows: dataNew } = await pg.query(sqlSuggest, query.key ? [query.key.toLowerCase()] : []);
|
|
281
|
+
const { rows: dataNew } = await pg.query(sqlSuggest, typeof query.key === "string" ? [query.key.toLowerCase()] : []);
|
|
266
282
|
// in case id / text = Boolean
|
|
267
283
|
const data = dataNew.filter((el) => Object.hasOwn(el, "id") && Object.hasOwn(el, "text"));
|
|
284
|
+
data
|
|
285
|
+
.filter((el) => el.id === null)
|
|
286
|
+
.forEach((el) => Object.assign(el, { id: "null", text: "None" }));
|
|
268
287
|
// table:column + select
|
|
269
288
|
if (tableName && column) {
|
|
270
289
|
const { name = selectName || column, type = "select" } = getColumnCLS(tableName, column) || {};
|