geonix 1.20.8 → 1.22.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/exports.js CHANGED
@@ -11,4 +11,6 @@ export { Request, Subscribe, Publish } from "./src/Request.js";
11
11
  export { RequestOptions } from "./src/RequestOptions.js";
12
12
  export { picoid as randomID } from "./src/Util.js";
13
13
 
14
- export { stats as StreamStats } from "./src/Stream.js";
14
+ export { stats as StreamStats } from "./src/Stream.js";
15
+
16
+ export { parseMultipart } from "./src/Util.js";
package/index.d.ts CHANGED
@@ -130,7 +130,11 @@ export class RequestOptionsClass {
130
130
  export function RequestOptions(options: any): RequestOptionsClass;
131
131
  export class Service {
132
132
  static serviceClasses: any[];
133
- static start(options?: {}): void;
133
+ /**
134
+ *
135
+ * @param {ServiceOptions} options
136
+ */
137
+ static start(options?: ServiceOptions): void;
134
138
  connections: Map<any, any>;
135
139
  $createConnection(streamId: any): Promise<boolean>;
136
140
  $getEnv(): {
@@ -148,17 +152,6 @@ export class Service {
148
152
  $getServiceInfo(): {};
149
153
  #private;
150
154
  }
151
- export type ServiceOptions = {
152
- middleware: {
153
- json: boolean;
154
- raw: boolean;
155
- cookies: boolean;
156
- };
157
- /**
158
- * Enable full beacon
159
- */
160
- fullBeacon: boolean;
161
- };
162
155
  /**
163
156
  * Converts data to stream
164
157
  *
@@ -174,6 +167,44 @@ export function streamToString(object: any, encoding: any): Promise<any>;
174
167
  export function streamToJSON(object: any): Promise<any>;
175
168
  export const stats: {};
176
169
  export const activeStreams: {};
170
+ type parseMultipartOptions = {
171
+ maxFileSize: number;
172
+ maxFiles: number;
173
+ useMemory: boolean;
174
+ };
175
+ type ParsedMultiPart = {
176
+ /**
177
+ * Field name of the part (extracted from Content-Disposition)
178
+ */
179
+ "name-": string;
180
+ /**
181
+ * - Filename of the part (extracted from Content-Disposition)
182
+ */
183
+ filename: string;
184
+ /**
185
+ * - Headers of the part
186
+ */
187
+ headers: Object;
188
+ /**
189
+ * - Path to the file when useMemory is false
190
+ */
191
+ bodyFile: string;
192
+ /**
193
+ * - Readable stream of the part
194
+ */
195
+ body: Readable;
196
+ };
197
+ type ServiceOptions = {
198
+ middleware: {
199
+ json: boolean;
200
+ raw: boolean;
201
+ cookies: boolean;
202
+ };
203
+ /**
204
+ * Enable full beacon
205
+ */
206
+ fullBeacon: boolean;
207
+ };
177
208
  /**
178
209
  * Parse nats:// URL
179
210
  * @param {string} url
@@ -182,7 +213,22 @@ export const activeStreams: {};
182
213
  export function parseURL(url: string): any;
183
214
  export function getFirstItemFromAsyncIterable(asyncIterable: any): Promise<any>;
184
215
  export function getNetworkAddresses(): any[];
216
+ /**
217
+ *
218
+ *
219
+ * @param {Request} req
220
+ * @param {parseMultipartOptions} options
221
+ * @returns ParsedMultiPart[]
222
+ */
223
+ export function parseMultipart(req: Request, _options: any): Promise<{
224
+ headers: {};
225
+ bodyFile: any;
226
+ body: any;
227
+ }[]>;
228
+ export function tempFilename(): any;
229
+ export function randomSafeId(size?: number): string;
185
230
  export function sleep(delay: number): Promise<any>;
231
+ export function nextTick(): Promise<any>;
186
232
  export function picoid(size?: number): any;
187
233
  export function hash(data: string | Buffer): any;
188
234
  export function createServerAtPort(port: number, pkg: Object, handler: Function): Promise<any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonix",
3
- "version": "1.20.8",
3
+ "version": "1.22.1",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "bin": {
@@ -16,8 +16,8 @@
16
16
  "author": "Davor Tarandek <dtarandek@tria.hr>",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "cookie-parser": "1.4.6",
20
- "express": "4.21.0",
19
+ "cookie-parser": "1.4.7",
20
+ "express": "4.21.1",
21
21
  "express-async-errors": "3.1.1",
22
22
  "express-ws": "5.0.2",
23
23
  "nats": "2.28.2",
package/src/Gateway.js CHANGED
@@ -102,8 +102,11 @@ export class Gateway {
102
102
  this.#api.use(raw, (req, res, next) => {
103
103
  stats.requests++;
104
104
 
105
- if (this.#router) { this.#router(req, res, next); }
106
- else { next(); }
105
+ if (this.#router) {
106
+ this.#router(req, res, next);
107
+ } else {
108
+ next();
109
+ }
107
110
  });
108
111
 
109
112
  this.#api.use((req, res, next) => {
package/src/Request.js CHANGED
@@ -143,7 +143,7 @@ export async function directRequest(identifier, method, args, context, options,
143
143
  * @param {string|number} payload
144
144
  */
145
145
  export async function Publish(subject, payload) {
146
- connection.publishRaw(`gx.sub.${subject}`, Buffer.from(payload));
146
+ connection.publishRaw(subject, Buffer.from(payload));
147
147
  }
148
148
 
149
149
  /**
@@ -158,7 +158,7 @@ export async function Subscribe(subject, callback) {
158
158
  return;
159
159
  }
160
160
 
161
- const subscription = await connection.subscribe(`gx.sub.${subject}`);
161
+ const subscription = await connection.subscribe(subject);
162
162
  for await (const event of subscription) {
163
163
  callback(event.data);
164
164
  }
package/src/Service.js CHANGED
@@ -20,12 +20,7 @@ const raw = express.raw({ type: "*/*", limit: "100mb" });
20
20
  const cookies = cookieParser();
21
21
 
22
22
  /**
23
- * @typedef {Object} ServiceOptions
24
- * @property {Object} middleware
25
- * @property {boolean} middleware.json Enable JSON middleware
26
- * @property {boolean} middleware.raw Enable RAW middleware
27
- * @property {boolean} middleware.cookies Enable cookies middleware
28
- * @property {boolean} fullBeacon Enable full beacon
23
+ * @type ServiceOptions
29
24
  */
30
25
  const defaultServiceOptions = {
31
26
  middleware: {
@@ -40,6 +35,10 @@ export class Service {
40
35
 
41
36
  static serviceClasses = [];
42
37
 
38
+ /**
39
+ *
40
+ * @param {ServiceOptions} options
41
+ */
43
42
  static start(options = {}) {
44
43
  if (!this.serviceClasses.includes(this.prototype.constructor.name)) {
45
44
  this.serviceClasses.push(this.prototype.constructor.name);
@@ -228,7 +227,7 @@ export class Service {
228
227
  }
229
228
 
230
229
  async #sub(subject, handler) {
231
- const subscription = await connection.subscribe(`gx.sub.${subject}`);
230
+ const subscription = await connection.subscribe(subject);
232
231
  const processor = async () => {
233
232
  for await (const event of subscription) {
234
233
  handler(event.data);
package/src/Types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @typedef {Object} parseMultipartOptions
3
+ * @property {number} maxFileSize
4
+ * @property {number} maxFiles
5
+ * @property {boolean} useMemory
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} ParsedMultiPart
10
+ * @property {string} name- Field name of the part (extracted from Content-Disposition)
11
+ * @property {string} filename - Filename of the part (extracted from Content-Disposition)
12
+ * @property {Object} headers - Headers of the part
13
+ * @property {string} bodyFile - Path to the file when useMemory is false
14
+ * @property {Readable} body - Readable stream of the part
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} ServiceOptions
19
+ * @property {Object} middleware
20
+ * @property {boolean} middleware.json Enable JSON middleware
21
+ * @property {boolean} middleware.raw Enable RAW middleware
22
+ * @property {boolean} middleware.cookies Enable cookies middleware
23
+ * @property {boolean} fullBeacon Enable full beacon
24
+ */
package/src/Util.js CHANGED
@@ -4,9 +4,13 @@ import { readFile } from "fs/promises";
4
4
  import { join } from "path";
5
5
  import { Transform } from "node:stream";
6
6
  import { networkInterfaces } from "os";
7
+ import { Readable } from "stream";
7
8
  import * as http from "http";
8
9
  import * as https from "https";
9
10
  import * as net from "net";
11
+ import { createWriteStream, createReadStream } from "fs";
12
+ import { unlink } from "fs/promises";
13
+ import { tmpdir } from "os";
10
14
 
11
15
  /**
12
16
  * Wait for {delay} ms
@@ -15,6 +19,13 @@ import * as net from "net";
15
19
  */
16
20
  export const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
17
21
 
22
+ /**
23
+ * Wait for next tick
24
+ *
25
+ * @returns
26
+ */
27
+ export const nextTick = () => sleep(0);
28
+
18
29
  /**
19
30
  * Parse nats:// URL
20
31
  * @param {string} url
@@ -228,4 +239,148 @@ export function getNetworkAddresses() {
228
239
  }
229
240
 
230
241
  return list;
242
+ }
243
+
244
+ /**
245
+ *
246
+ *
247
+ * @param {Request} req
248
+ * @param {parseMultipartOptions} options
249
+ * @returns ParsedMultiPart[]
250
+ */
251
+ export async function parseMultipart(req, _options) {
252
+ if (!req.headers["content-type"]?.startsWith("multipart/form-data")) {
253
+ throw new Error("Invalid content type (multipart/form-data expected)");
254
+ }
255
+
256
+ const BUFFER_SIZE = 1024 * 1024;
257
+ const END_OF_HEADERS = Buffer.from("\r\n\r\n");
258
+ const options = {
259
+ useMemory: false,
260
+ ..._options
261
+ };
262
+ const parts = [];
263
+ let stream = req;
264
+
265
+ // if body is present, create a stream from it and use memory
266
+ if (req.body) {
267
+ stream = Readable.from(req.body);
268
+ options.useMemory = true;
269
+ }
270
+
271
+ const boundary = Buffer.from("\r\n--" + req.headers["content-type"].split("boundary=")[1]);
272
+
273
+ await nextTick();
274
+
275
+ let lastChunk = Buffer.from("\r\n");
276
+ let activePart;
277
+ let done = false;
278
+
279
+ while (stream.readable) {
280
+ // next next chunk
281
+ let chunk = stream.read(BUFFER_SIZE);
282
+
283
+ if (!chunk) {
284
+ await nextTick();
285
+ continue;
286
+ }
287
+
288
+ let combined = Buffer.concat([lastChunk, chunk]);
289
+
290
+ while (combined.length >= boundary.length + 2) {
291
+ const boundaryIndex = combined.indexOf(boundary);
292
+ const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
293
+
294
+ if (boundaryIndex > -1) {
295
+ if (boundaryIndex > 0) {
296
+ if (options.useMemory) {
297
+ activePart.body.push(combined.slice(0, boundaryIndex));
298
+ } else {
299
+ activePart.body.write(combined.slice(0, boundaryIndex));
300
+ }
301
+ }
302
+
303
+ if (isLastBoundary) {
304
+ combined = combined.slice(boundaryIndex + boundary.length + 2);
305
+ done = true;
306
+ break;
307
+ }
308
+
309
+ // create new part
310
+ const bodyFile = tempFilename();
311
+ activePart = {
312
+ headers: {},
313
+ bodyFile: options.useMemory ? undefined : bodyFile,
314
+ body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" })
315
+ };
316
+ parts.push(activePart);
317
+
318
+ const endOfHeaders = combined.indexOf(END_OF_HEADERS, boundaryIndex);
319
+ activePart.headers = combined
320
+ .slice(boundaryIndex + boundary.length + 2, endOfHeaders).toString()
321
+ .split("\r\n")
322
+ .reduce((acc, val) => {
323
+ const [header, value] = val.split(": ");
324
+ acc[header.toLowerCase()] = value;
325
+ return acc;
326
+ }, {});
327
+
328
+ combined = combined.slice(endOfHeaders + END_OF_HEADERS.length);
329
+ }
330
+
331
+ lastChunk = combined;
332
+ }
333
+
334
+ // there's no boundary in the chunk, add it to active part
335
+ if (activePart && lastChunk.length > 0 && !done) {
336
+ if (options.useMemory) {
337
+ activePart.body.push(lastChunk);
338
+ } else {
339
+ activePart.body.write(lastChunk);
340
+ }
341
+ lastChunk = Buffer.alloc(0);
342
+ }
343
+ }
344
+
345
+ for (const part of parts) {
346
+ // extract name and filename from content-disposition header
347
+ if (part.headers["content-disposition"]) {
348
+ const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/);
349
+ const [, filename] = part.headers["content-disposition"].match(/filename="([^"]+)"/) || [];
350
+ part.name = name ?? null;
351
+ part.filename = filename ?? null;
352
+ }
353
+
354
+ if (options.useMemory) {
355
+ part.body = Readable.from(Buffer.concat(part.body));
356
+ delete part.bodyFile;
357
+ } else {
358
+ part.body.end();
359
+ part.body = createReadStream(part.bodyFile);
360
+ part.body.on("close", async () => {
361
+ try {
362
+ await unlink(part.bodyFile);
363
+ } catch {
364
+ // ignore errors
365
+ }
366
+ });
367
+ }
368
+ }
369
+
370
+ return parts;
371
+ }
372
+
373
+ export function tempFilename() {
374
+ return join(tmpdir(), `${randomSafeId(12)}.gxtmp`);
375
+ }
376
+
377
+ export function randomSafeId(size = 12) {
378
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
379
+ let result = "";
380
+
381
+ for (let i = 0; i < size; i++) {
382
+ result += charset.charAt(Math.floor(Math.random() * charset.length));
383
+ }
384
+
385
+ return result;
231
386
  }
package/test/gateway.js CHANGED
@@ -1,13 +1,31 @@
1
- import { Gateway, Service } from "../exports.js";
1
+ import { Gateway, Service, streamToBuffer } from "../exports.js";
2
+ import { parseMultipart } from "../src/Util.js";
2
3
 
3
4
  class TestService extends Service {
4
5
 
5
6
  "GET /"(req, res) {
6
7
  res.send("Hello World");
7
8
  }
9
+
10
+ async "POST /upload"(req, res) {
11
+ const parts = await parseMultipart(req, { useMemory: false });
12
+
13
+ for (const part of parts) {
14
+ console.log(part.body);
15
+ }
16
+
17
+ res.send("OK");
18
+ }
19
+
8
20
  }
9
21
 
10
- TestService.start();
22
+ TestService.start({
23
+ middleware: {
24
+ raw: true,
25
+ json: false,
26
+ cookies: false,
27
+ }
28
+ });
11
29
  Gateway.start({
12
30
  beforeRequest: (req, res) => {
13
31
  res.set("X-Test", "Test");