@stemy/backend 6.0.3 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/commands/clear.command.d.ts +6 -0
  2. package/commands/fixtures.command.d.ts +9 -0
  3. package/commands/move-assets.command.d.ts +10 -0
  4. package/common-types.d.ts +22 -12
  5. package/esm2022/commands/clear.command.mjs +15 -0
  6. package/esm2022/commands/fixtures.command.mjs +26 -0
  7. package/esm2022/commands/index.mjs +6 -4
  8. package/esm2022/commands/move-assets.command.mjs +45 -0
  9. package/esm2022/common-types.mjs +3 -3
  10. package/esm2022/public_api.mjs +18 -6
  11. package/esm2022/services/assets.mjs +46 -13
  12. package/esm2022/services/backend-provider.mjs +7 -1
  13. package/esm2022/services/cli-terminal.mjs +56 -0
  14. package/esm2022/services/configuration.mjs +11 -4
  15. package/esm2022/services/drivers/asset-fallback-driver.mjs +35 -0
  16. package/esm2022/services/drivers/asset-grid.driver.mjs +5 -7
  17. package/esm2022/services/drivers/asset-local.driver.mjs +19 -18
  18. package/esm2022/services/drivers/asset-storage-proxy.driver.mjs +48 -0
  19. package/esm2022/services/drivers/fallback-readable.mjs +34 -0
  20. package/esm2022/services/drivers/fallback-streams.mjs +34 -0
  21. package/esm2022/services/entities/asset.mjs +50 -15
  22. package/esm2022/services/entities/temp-asset.mjs +15 -3
  23. package/esm2022/services/terminal-manager.mjs +30 -16
  24. package/esm2022/socket-controllers/socket-terminal.mjs +89 -0
  25. package/esm2022/socket-controllers/terminal.controller.mjs +3 -3
  26. package/fesm2022/stemy-backend.mjs +423 -140
  27. package/fesm2022/stemy-backend.mjs.map +1 -1
  28. package/package.json +3 -2
  29. package/public_api.d.ts +2 -1
  30. package/services/assets.d.ts +13 -5
  31. package/services/cli-terminal.d.ts +18 -0
  32. package/services/configuration.d.ts +2 -0
  33. package/services/drivers/asset-fallback-driver.d.ts +11 -0
  34. package/services/drivers/asset-grid.driver.d.ts +4 -5
  35. package/services/drivers/asset-local.driver.d.ts +6 -6
  36. package/services/drivers/asset-storage-proxy.driver.d.ts +14 -0
  37. package/services/drivers/fallback-readable.d.ts +2 -0
  38. package/services/drivers/fallback-streams.d.ts +2 -0
  39. package/services/entities/asset.d.ts +8 -3
  40. package/services/entities/temp-asset.d.ts +6 -1
  41. package/services/terminal-manager.d.ts +3 -0
  42. package/socket-controllers/socket-terminal.d.ts +19 -0
  43. package/socket-controllers/terminal.controller.d.ts +2 -2
@@ -8,14 +8,14 @@ import { create } from 'fontkit';
8
8
  import sharp_ from 'sharp';
9
9
  import { ObjectId as ObjectId$1 } from 'bson';
10
10
  import axios from 'axios';
11
- import { mkdir, unlink, readFile as readFile$1, writeFile as writeFile$1, lstat, readdir, access, constants, lstatSync, readFileSync, existsSync, mkdirSync, createWriteStream, createReadStream } from 'fs';
11
+ import { mkdir, unlink, readFile as readFile$1, writeFile as writeFile$1, lstat, readdir, access, constants, lstatSync, readFileSync, existsSync, mkdirSync, createWriteStream, writeFileSync, createReadStream } from 'fs';
12
12
  import { gzip, gunzip } from 'zlib';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { exec } from 'child_process';
15
15
  import { createHash } from 'crypto';
16
16
  import { Subscription, Observable, Subject, from, BehaviorSubject } from 'rxjs';
17
17
  import { ObjectId } from 'mongodb';
18
- import mongoose, { Types } from 'mongoose';
18
+ import mongoose from 'mongoose';
19
19
  import { Readable, PassThrough } from 'stream';
20
20
  import fileType from 'file-type/core';
21
21
  import dotenv from 'dotenv';
@@ -31,14 +31,16 @@ import { routingControllersToSpec, OpenAPI, getStatusCode } from 'routing-contro
31
31
  import { defaultMetadataStorage } from 'class-transformer/cjs/storage';
32
32
  import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
33
33
  import { ValidatorConstraint, ValidationTypes, Min, Max, IsOptional, IsBoolean } from 'class-validator';
34
+ import { CommandsAddon, AnsiCodes } from '@stemy/terminal-commands-addon';
35
+ import * as readline from 'readline';
34
36
  import { v4 } from 'uuid';
35
37
  import { createTransport } from 'nodemailer';
36
38
  import * as Handlebars from 'handlebars';
37
- import { CommandsAddon, AnsiCodes } from '@stemy/terminal-commands-addon';
38
39
  import { compare } from 'bcrypt';
39
40
  import moment from 'moment';
40
41
  import { GridFSBucket } from 'mongodb/lib/gridfs';
41
- import { writeFile as writeFile$2, rm } from 'fs/promises';
42
+ import { rm } from 'fs/promises';
43
+ import got from 'got';
42
44
  import { getModelForClass } from '@typegoose/typegoose';
43
45
  import { getValue as getValue$1, setValue } from 'mongoose/lib/utils';
44
46
 
@@ -55,8 +57,8 @@ const SOCKET_CONTROLLERS = Symbol.for("socket-controllers-token");
55
57
  const PARAMETER = Symbol.for("parameter-token");
56
58
  const DI_CONTAINER = Symbol.for("di-container-token");
57
59
  const OPENAPI_VALIDATION = Symbol.for("openapi-validation-token");
58
- const LOCAL_DIR = Symbol.for('asset-local-dir');
59
- const ASSET_DRIVER = Symbol.for('assets-driver');
60
+ const ASSET_LOCAL_DIR = Symbol.for('asset-local-dir');
61
+ const ASSET_DRIVER_FACTORIES = Symbol.for('assets-driver-factories');
60
62
  class Parameter {
61
63
  name;
62
64
  defaultValue;
@@ -864,10 +866,12 @@ async function fileTypeFromBuffer(buffer) {
864
866
  }
865
867
 
866
868
  let Configuration = class Configuration {
869
+ isCli;
867
870
  paramMap;
868
871
  paramValues;
869
872
  constructor(params) {
870
873
  dotenv.config();
874
+ this.isCli = process.env.IS_CLI === 'true';
871
875
  this.paramMap = {};
872
876
  this.paramValues = {};
873
877
  (params || []).forEach(param => this.add(param));
@@ -888,15 +892,15 @@ let Configuration = class Configuration {
888
892
  const value = isFunction(param.resolver)
889
893
  ? param.resolver(envValue, helper)
890
894
  : convertValue(envValue, getType(param.defaultValue));
891
- console.log(colorize(`Processing param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), colorize(envName, ConsoleColor.FgBlue), `"${envValue}"`, value);
895
+ this.log(colorize(`Processing param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), colorize(envName, ConsoleColor.FgBlue), `"${envValue}"`, value);
892
896
  return value;
893
897
  }
894
898
  else if (isFunction(param.resolver)) {
895
899
  const value = param.resolver(param.defaultValue, helper);
896
- console.log(colorize(`Processing default param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), param.defaultValue, value);
900
+ this.log(colorize(`Processing default param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), param.defaultValue, value);
897
901
  return value;
898
902
  }
899
- console.log(colorize(`Using default param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), param.defaultValue);
903
+ this.log(colorize(`Using default param value`, ConsoleColor.FgYellow), colorize(param.name, ConsoleColor.FgGreen), param.defaultValue);
900
904
  return param.defaultValue;
901
905
  }
902
906
  hasParam(name) {
@@ -918,6 +922,11 @@ let Configuration = class Configuration {
918
922
  }
919
923
  return this.paramValues[name];
920
924
  }
925
+ log(...args) {
926
+ if (this.isCli)
927
+ return;
928
+ console.log(...args);
929
+ }
921
930
  };
922
931
  Configuration = __decorate([
923
932
  singleton(),
@@ -989,10 +998,19 @@ class BaseEntity {
989
998
  }
990
999
 
991
1000
  class Asset extends BaseEntity {
992
- driver;
1001
+ drivers;
993
1002
  get filename() {
994
1003
  return this.data.filename;
995
1004
  }
1005
+ get streamId() {
1006
+ return this.data.streamId || this.oid;
1007
+ }
1008
+ get driverId() {
1009
+ return this.data.driverId;
1010
+ }
1011
+ get driver() {
1012
+ return this.drivers.getDriver(this.driverId || this.drivers.missingDriver);
1013
+ }
996
1014
  get contentType() {
997
1015
  return this.data.contentType;
998
1016
  }
@@ -1000,26 +1018,20 @@ class Asset extends BaseEntity {
1000
1018
  return this.data.metadata;
1001
1019
  }
1002
1020
  get stream() {
1003
- return this.driver.openDownloadStream(this.oid);
1021
+ return this.driver.openDownloadStream(this);
1004
1022
  }
1005
- constructor(id, data, collection, driver) {
1023
+ constructor(id, data, collection, drivers) {
1006
1024
  super(id, data, collection);
1007
- this.driver = driver;
1025
+ this.drivers = drivers;
1008
1026
  }
1009
1027
  async unlink() {
1010
1028
  try {
1011
- await this.driver.delete(this.oid);
1012
- await this.collection.deleteOne({ _id: this.oid });
1029
+ await this.driver.delete(this);
1013
1030
  }
1014
1031
  catch (error) {
1015
- let err = error;
1016
- if (error) {
1017
- err = error.message || error || "";
1018
- if (!isString(err) || !err.startsWith("FileNotFound")) {
1019
- throw err;
1020
- }
1021
- }
1032
+ console.log("Failed to unlink", error?.message);
1022
1033
  }
1034
+ await this.collection.deleteOne({ _id: this.oid });
1023
1035
  return this.id;
1024
1036
  }
1025
1037
  async setMeta(metadata) {
@@ -1029,6 +1041,38 @@ class Asset extends BaseEntity {
1029
1041
  getBuffer() {
1030
1042
  return streamToBuffer(this.stream);
1031
1043
  }
1044
+ async move(driverId) {
1045
+ const oldDriver = this.driver;
1046
+ const targetDriver = this.drivers.getDriver(driverId);
1047
+ if (targetDriver === oldDriver)
1048
+ return this;
1049
+ const oldAsset = new Asset(this.oid, this.data, this.collection, this.drivers);
1050
+ const streamId = await this.uploadTo(targetDriver);
1051
+ this.data = {
1052
+ ...this.data,
1053
+ streamId,
1054
+ driverId,
1055
+ };
1056
+ await this.save();
1057
+ await oldDriver.delete(oldAsset);
1058
+ }
1059
+ uploadTo(driver) {
1060
+ return new Promise((resolve, reject) => {
1061
+ const uploaderStream = driver.openUploadStream(this.filename, {
1062
+ chunkSizeBytes: 1048576,
1063
+ contentType: this.contentType,
1064
+ extension: this.metadata.extension,
1065
+ metadata: this.metadata,
1066
+ });
1067
+ this.stream.pipe(uploaderStream)
1068
+ .on("error", error => {
1069
+ reject(error.message || error);
1070
+ })
1071
+ .on("finish", async () => {
1072
+ resolve(uploaderStream.id);
1073
+ });
1074
+ });
1075
+ }
1032
1076
  async download(metadata) {
1033
1077
  metadata = Object.assign(this.metadata, metadata || {});
1034
1078
  metadata.downloadCount = isNaN(metadata.downloadCount) || !metadata.firstDownload
@@ -1052,16 +1096,25 @@ class TempAsset {
1052
1096
  filename;
1053
1097
  contentType;
1054
1098
  metadata;
1055
- id;
1099
+ get id() {
1100
+ return this.oid.toHexString();
1101
+ }
1102
+ get streamId() {
1103
+ return this.oid;
1104
+ }
1105
+ get driverId() {
1106
+ return "temp";
1107
+ }
1056
1108
  get stream() {
1057
1109
  return bufferToStream(this.buffer);
1058
1110
  }
1111
+ oid;
1059
1112
  constructor(buffer, filename, contentType, metadata) {
1060
1113
  this.buffer = buffer;
1061
1114
  this.filename = filename;
1062
1115
  this.contentType = contentType;
1063
1116
  this.metadata = metadata;
1064
- this.id = new ObjectId$1().toHexString();
1117
+ this.oid = new ObjectId$1();
1065
1118
  }
1066
1119
  async unlink() {
1067
1120
  throw new Error(`Temp asset '${this.id}' can not be removed!`);
@@ -1072,6 +1125,9 @@ class TempAsset {
1072
1125
  async getBuffer() {
1073
1126
  return this.buffer;
1074
1127
  }
1128
+ async move() {
1129
+ throw new Error(`Temp asset '${this.id}' can not be moved!`);
1130
+ }
1075
1131
  async download(metadata) {
1076
1132
  return this.stream;
1077
1133
  }
@@ -1101,13 +1157,38 @@ class TempAsset {
1101
1157
  let Assets = class Assets {
1102
1158
  connector;
1103
1159
  assetProcessor;
1104
- driver;
1160
+ config;
1161
+ container;
1162
+ driverFactoryMap;
1105
1163
  collection;
1106
- constructor(connector, assetProcessor, driver) {
1164
+ driverMap;
1165
+ mainDriver;
1166
+ missingDriver;
1167
+ drivers;
1168
+ constructor(connector, assetProcessor, config, container, driverFactoryMap) {
1107
1169
  this.connector = connector;
1108
1170
  this.assetProcessor = assetProcessor;
1109
- this.driver = driver;
1171
+ this.config = config;
1172
+ this.container = container;
1173
+ this.driverFactoryMap = driverFactoryMap;
1110
1174
  this.collection = connector.database?.collection("assets.metadata");
1175
+ this.driverMap = new Map();
1176
+ this.mainDriver = this.config.resolve("assetsMainDriver");
1177
+ this.missingDriver = this.config.resolve("assetsMissingDriver");
1178
+ this.drivers = Object.keys(driverFactoryMap);
1179
+ }
1180
+ getDriver(name) {
1181
+ if (!name) {
1182
+ throw new Error(`No name provider for asset driver!`);
1183
+ }
1184
+ if (!this.driverMap.has(name)) {
1185
+ const factory = this.driverFactoryMap[name];
1186
+ if (!factory) {
1187
+ throw new Error(`No asset driver factory found with name: '${name}'!`);
1188
+ }
1189
+ this.driverMap.set(name, factory(this.container));
1190
+ }
1191
+ return this.driverMap.get(name);
1111
1192
  }
1112
1193
  async write(stream, contentType = null, metadata = null) {
1113
1194
  const uploadStream = copyStream(stream);
@@ -1176,7 +1257,7 @@ let Assets = class Assets {
1176
1257
  }
1177
1258
  async find(where) {
1178
1259
  const data = await this.collection.findOne(where);
1179
- return !data ? null : new Asset(data._id, data, this.collection, this.driver);
1260
+ return !data ? null : new Asset(data._id, data, this.collection, this);
1180
1261
  }
1181
1262
  async findMany(where) {
1182
1263
  const cursor = this.collection.find(where);
@@ -1185,7 +1266,7 @@ let Assets = class Assets {
1185
1266
  for (let item of items) {
1186
1267
  if (!item)
1187
1268
  continue;
1188
- result.push(new Asset(item._id, item, this.collection, this.driver));
1269
+ result.push(new Asset(item._id, item, this.collection, this));
1189
1270
  }
1190
1271
  return result;
1191
1272
  }
@@ -1209,10 +1290,13 @@ let Assets = class Assets {
1209
1290
  metadata.filename = metadata.filename || new ObjectId$1().toHexString();
1210
1291
  metadata.extension = (fileType.ext || "").trim();
1211
1292
  return new Promise(((resolve, reject) => {
1212
- const uploaderStream = this.driver.openUploadStream(metadata.filename, {
1293
+ const driverId = this.mainDriver;
1294
+ const driver = this.getDriver(driverId);
1295
+ const uploaderStream = driver.openUploadStream(metadata.filename, {
1213
1296
  chunkSizeBytes: 1048576,
1297
+ contentType: fileType.mime,
1298
+ extension: fileType.ext,
1214
1299
  metadata,
1215
- contentType: fileType.mime
1216
1300
  });
1217
1301
  stream.pipe(uploaderStream)
1218
1302
  .on("error", error => {
@@ -1220,10 +1304,12 @@ let Assets = class Assets {
1220
1304
  })
1221
1305
  .on("finish", () => {
1222
1306
  const asset = new Asset(uploaderStream.id, {
1307
+ streamId: uploaderStream.id,
1223
1308
  filename: metadata.filename,
1224
1309
  contentType,
1225
- metadata
1226
- }, this.collection, this.driver);
1310
+ metadata,
1311
+ driverId
1312
+ }, this.collection, this);
1227
1313
  asset.save().then(() => {
1228
1314
  resolve(asset);
1229
1315
  }, error => {
@@ -1236,9 +1322,11 @@ let Assets = class Assets {
1236
1322
  Assets = __decorate([
1237
1323
  injectable(),
1238
1324
  scoped(Lifecycle.ContainerScoped),
1239
- __param(2, inject(ASSET_DRIVER)),
1325
+ __param(3, inject(DI_CONTAINER)),
1326
+ __param(4, inject(ASSET_DRIVER_FACTORIES)),
1240
1327
  __metadata("design:paramtypes", [MongoConnector,
1241
- AssetProcessor, Object])
1328
+ AssetProcessor,
1329
+ Configuration, Object, Object])
1242
1330
  ], Assets);
1243
1331
 
1244
1332
  class LazyAsset extends BaseEntity {
@@ -2086,6 +2174,154 @@ Fixtures = __decorate([
2086
2174
  __metadata("design:paramtypes", [Object])
2087
2175
  ], Fixtures);
2088
2176
 
2177
+ class CliTerminal {
2178
+ callbacks = new Set();
2179
+ stdin = process.stdin;
2180
+ constructor() {
2181
+ // Set stdin to raw mode to get character-by-character data
2182
+ if (this.stdin.isTTY) {
2183
+ this.stdin.setRawMode(true);
2184
+ }
2185
+ this.clearScreen();
2186
+ this.stdin.setEncoding('utf8');
2187
+ this.stdin.on('data', this.handleInput);
2188
+ }
2189
+ clearScreen() {
2190
+ readline.cursorTo(process.stdout, 0, 0);
2191
+ readline.clearScreenDown(process.stdout);
2192
+ }
2193
+ handleInput = (data) => {
2194
+ const input = data.toString();
2195
+ // Standard "Ctrl+C" exit handling
2196
+ if (input === '\u0003') {
2197
+ this.clearScreen();
2198
+ this.dispose();
2199
+ }
2200
+ this.callbacks.forEach((cb) => cb(input));
2201
+ };
2202
+ /**
2203
+ * Registers a listener for data input.
2204
+ * Returns an IDisposable to unregister the listener.
2205
+ */
2206
+ onData(cb) {
2207
+ this.callbacks.add(cb);
2208
+ return {
2209
+ dispose: () => this.callbacks.delete(cb),
2210
+ };
2211
+ }
2212
+ write(data) {
2213
+ process.stdout.write(data);
2214
+ }
2215
+ writeln(data) {
2216
+ process.stdout.write(data + '\n');
2217
+ }
2218
+ loadAddon(addon) {
2219
+ addon.activate(this);
2220
+ }
2221
+ dispose() {
2222
+ if (this.stdin.isTTY) {
2223
+ this.stdin.setRawMode(false);
2224
+ }
2225
+ this.stdin.removeListener('data', this.handleInput);
2226
+ this.stdin.pause();
2227
+ this.callbacks.clear();
2228
+ process.exit();
2229
+ }
2230
+ }
2231
+
2232
+ let TerminalManager = class TerminalManager {
2233
+ logger;
2234
+ config;
2235
+ cliTerminal;
2236
+ servicePassword;
2237
+ suggestions;
2238
+ commands;
2239
+ loggedOutCommands;
2240
+ loggedInCommands;
2241
+ constructor(logger, config, commands) {
2242
+ this.logger = logger;
2243
+ this.config = config;
2244
+ this.servicePassword = config.resolve("servicePassword");
2245
+ this.suggestions = {
2246
+ login: async (args) => {
2247
+ if (args.length > 2) {
2248
+ return null;
2249
+ }
2250
+ const input = `${args.at(1).label}`;
2251
+ return (!input) ? [] : [{
2252
+ id: input,
2253
+ label: input,
2254
+ masked: true
2255
+ }];
2256
+ },
2257
+ ...commands.reduce((acc, command) => {
2258
+ command.name = camelCaseToDash(command.name || command.constructor.name || "");
2259
+ if (!command.name || !command.suggest)
2260
+ return acc;
2261
+ acc[command.name] = async (a, t) => command.suggest(a, t);
2262
+ return acc;
2263
+ }, {})
2264
+ };
2265
+ this.commands = commands.reduce((acc, command) => {
2266
+ if (!command.name)
2267
+ return acc;
2268
+ acc[command.name] = async (a, t, p) => command.execute(a, t, p);
2269
+ return acc;
2270
+ }, {});
2271
+ this.loggedOutCommands = ["login", "clear"];
2272
+ this.loggedInCommands = Object.keys(this.commands);
2273
+ this.loggedInCommands.push("logout");
2274
+ console.log(`Current service password is: ${colorize(this.servicePassword, ConsoleColor.FgGreen)}`);
2275
+ }
2276
+ runCli() {
2277
+ this.cliTerminal = new CliTerminal();
2278
+ this.loadAddons(this.cliTerminal);
2279
+ }
2280
+ loadAddons(terminal) {
2281
+ const isCli = terminal === this.cliTerminal;
2282
+ let loggedIn = isCli;
2283
+ const commands = isCli ? {
2284
+ logout: async () => {
2285
+ terminal.dispose();
2286
+ }
2287
+ } : {
2288
+ login: async (args, terminal) => {
2289
+ if (args.at(1).label === this.servicePassword) {
2290
+ loggedIn = true;
2291
+ terminal.writeln("Logged in as admin");
2292
+ }
2293
+ else {
2294
+ throw new Error("Invalid login");
2295
+ }
2296
+ },
2297
+ logout: async (args, terminal) => {
2298
+ loggedIn = false;
2299
+ terminal.writeln("Logged out");
2300
+ }
2301
+ };
2302
+ const addon = new CommandsAddon({
2303
+ commands: {
2304
+ ...commands,
2305
+ ...this.commands
2306
+ },
2307
+ suggestCommands: async () => {
2308
+ if (loggedIn) {
2309
+ return this.loggedInCommands;
2310
+ }
2311
+ return this.loggedOutCommands;
2312
+ },
2313
+ suggestions: this.suggestions
2314
+ });
2315
+ terminal.loadAddon(addon);
2316
+ }
2317
+ };
2318
+ TerminalManager = __decorate([
2319
+ singleton(),
2320
+ __param(2, injectAll(TERMINAL_COMMAND)),
2321
+ __metadata("design:paramtypes", [Logger,
2322
+ Configuration, Array])
2323
+ ], TerminalManager);
2324
+
2089
2325
  const express = express_;
2090
2326
  let BackendProvider = class BackendProvider {
2091
2327
  config;
@@ -2134,6 +2370,11 @@ let BackendProvider = class BackendProvider {
2134
2370
  }
2135
2371
  async quickStart() {
2136
2372
  const port = this.config.resolve("appPort");
2373
+ const isCli = this.config.isCli;
2374
+ if (isCli) {
2375
+ this.container.resolve(TerminalManager).runCli();
2376
+ return `Running CLI mode`;
2377
+ }
2137
2378
  const isWorker = this.config.resolve("isWorker");
2138
2379
  if (isWorker || this.config.resolve("startWorker")) {
2139
2380
  await this.container.resolve(JobManager).startProcessing();
@@ -2717,86 +2958,6 @@ MemoryCache = __decorate([
2717
2958
  __metadata("design:paramtypes", [Cache])
2718
2959
  ], MemoryCache);
2719
2960
 
2720
- let TerminalManager = class TerminalManager {
2721
- logger;
2722
- config;
2723
- servicePassword;
2724
- suggestions;
2725
- commands;
2726
- loggedOutCommands;
2727
- loggedInCommands;
2728
- constructor(logger, config, commands) {
2729
- this.logger = logger;
2730
- this.config = config;
2731
- this.servicePassword = config.resolve("servicePassword");
2732
- this.suggestions = {
2733
- login: async (args) => {
2734
- if (args.length > 2) {
2735
- return null;
2736
- }
2737
- const input = `${args.at(1).label}`;
2738
- return (!input) ? [] : [{
2739
- id: input,
2740
- label: input,
2741
- masked: true
2742
- }];
2743
- },
2744
- ...commands.reduce((acc, command) => {
2745
- command.name = camelCaseToDash(command.name || command.constructor.name || "");
2746
- if (!command.name || !command.suggest)
2747
- return acc;
2748
- acc[command.name] = async (a, t) => command.suggest(a, t);
2749
- return acc;
2750
- }, {})
2751
- };
2752
- this.commands = commands.reduce((acc, command) => {
2753
- if (!command.name)
2754
- return acc;
2755
- acc[command.name] = async (a, t) => command.execute(a, t);
2756
- return acc;
2757
- }, {});
2758
- this.loggedOutCommands = ["login", "clear"];
2759
- this.loggedInCommands = Object.keys(this.commands);
2760
- this.loggedInCommands.push("logout");
2761
- console.log(`Current service password is: ${colorize(this.servicePassword, ConsoleColor.FgGreen)}`);
2762
- }
2763
- loadAddons(terminal) {
2764
- let loggedIn = false;
2765
- const addon = new CommandsAddon({
2766
- commands: {
2767
- login: async (args, terminal) => {
2768
- if (args.at(1).label === this.servicePassword) {
2769
- loggedIn = true;
2770
- terminal.writeln("Logged in as admin");
2771
- }
2772
- else {
2773
- throw new Error("Invalid login");
2774
- }
2775
- },
2776
- logout: async (args, terminal) => {
2777
- loggedIn = false;
2778
- terminal.writeln("Logged out");
2779
- },
2780
- ...this.commands
2781
- },
2782
- suggestCommands: async () => {
2783
- if (loggedIn) {
2784
- return this.loggedInCommands;
2785
- }
2786
- return this.loggedOutCommands;
2787
- },
2788
- suggestions: this.suggestions
2789
- });
2790
- terminal.loadAddon(addon);
2791
- }
2792
- };
2793
- TerminalManager = __decorate([
2794
- singleton(),
2795
- __param(2, injectAll(TERMINAL_COMMAND)),
2796
- __metadata("design:paramtypes", [Logger,
2797
- Configuration, Array])
2798
- ], TerminalManager);
2799
-
2800
2961
  let TokenGenerator = class TokenGenerator {
2801
2962
  chars;
2802
2963
  constructor() {
@@ -3576,7 +3737,7 @@ ProgressController = __decorate([
3576
3737
  __metadata("design:paramtypes", [Progresses, Server])
3577
3738
  ], ProgressController);
3578
3739
 
3579
- class Terminal {
3740
+ class SocketTerminal {
3580
3741
  client;
3581
3742
  addons;
3582
3743
  files$;
@@ -3670,7 +3831,7 @@ let TerminalController = class TerminalController {
3670
3831
  this.terminals = {};
3671
3832
  }
3672
3833
  async terminalInit(client) {
3673
- const terminal = new Terminal(client);
3834
+ const terminal = new SocketTerminal(client);
3674
3835
  this.manager.loadAddons(terminal);
3675
3836
  this.terminals[client.id] = terminal;
3676
3837
  client.on("disconnect", () => terminal.dispose());
@@ -3989,9 +4150,50 @@ FixturesCommand = __decorate([
3989
4150
  __metadata("design:paramtypes", [Fixtures])
3990
4151
  ], FixturesCommand);
3991
4152
 
4153
+ let MoveAssetsCommand = class MoveAssetsCommand {
4154
+ assets;
4155
+ name = "move-assets";
4156
+ constructor(assets) {
4157
+ this.assets = assets;
4158
+ }
4159
+ async execute(args, terminal, progress) {
4160
+ await promiseTimeout(1000);
4161
+ const from = `${args.at(1).id}`;
4162
+ const to = `${args.at(2).id}`;
4163
+ const assets = await this.assets.findMany(from === this.assets.missingDriver ? {
4164
+ $or: [
4165
+ { driverId: from },
4166
+ { driverId: { "$exists": false } }
4167
+ ]
4168
+ } : { driverId: from });
4169
+ progress.setMax(assets.length);
4170
+ for (const asset of assets) {
4171
+ await asset.move(to);
4172
+ progress.advance();
4173
+ }
4174
+ terminal.writeln(colorize(`Assets successfully moved to: ${to}`, ConsoleColor.FgGreen));
4175
+ }
4176
+ async suggest(args) {
4177
+ if (args.length > 3) {
4178
+ return null;
4179
+ }
4180
+ // Replace regex special characters
4181
+ const prev = args.length === 3 ? args.at(1) : null;
4182
+ const id = regexEscape(args.search);
4183
+ const regex = new RegExp(id, "g");
4184
+ return this.assets.drivers.filter(d => d.match(regex) && (!prev || d !== prev.id));
4185
+ }
4186
+ };
4187
+ MoveAssetsCommand = __decorate([
4188
+ injectable(),
4189
+ scoped(Lifecycle.ContainerScoped),
4190
+ __metadata("design:paramtypes", [Assets])
4191
+ ], MoveAssetsCommand);
4192
+
3992
4193
  const commands = [
3993
4194
  ClearCommand,
3994
- FixturesCommand
4195
+ FixturesCommand,
4196
+ MoveAssetsCommand
3995
4197
  ];
3996
4198
 
3997
4199
  let TtlFixture = class TtlFixture {
@@ -4018,20 +4220,18 @@ const fixtures = [
4018
4220
  ];
4019
4221
 
4020
4222
  let AssetGridDriver = class AssetGridDriver {
4021
- metaCollection;
4022
4223
  bucket;
4023
4224
  constructor(connector) {
4024
4225
  this.bucket = new GridFSBucket(connector.database, { bucketName: 'assets' });
4025
- this.metaCollection = "assets.files";
4026
4226
  }
4027
4227
  openUploadStream(filename, opts) {
4028
4228
  return this.bucket.openUploadStream(filename, opts);
4029
4229
  }
4030
- openDownloadStream(id) {
4031
- return this.bucket.openDownloadStream(id);
4230
+ openDownloadStream(asset) {
4231
+ return this.bucket.openDownloadStream(asset.streamId);
4032
4232
  }
4033
- delete(id) {
4034
- return this.bucket.delete(id);
4233
+ delete(asset) {
4234
+ return this.bucket.delete(asset.streamId);
4035
4235
  }
4036
4236
  };
4037
4237
  AssetGridDriver = __decorate([
@@ -4041,38 +4241,112 @@ AssetGridDriver = __decorate([
4041
4241
 
4042
4242
  let AssetLocalDriver = class AssetLocalDriver {
4043
4243
  dir;
4044
- metaCollection;
4045
4244
  constructor(dir) {
4046
4245
  this.dir = dir;
4047
- this.metaCollection = "assets.local";
4048
4246
  }
4049
4247
  openUploadStream(filename, opts) {
4050
- const id = new Types.ObjectId();
4248
+ const id = new ObjectId$1();
4051
4249
  const dir = `${this.dir}/${id.toHexString()}`;
4052
4250
  mkdirSync(dir, { recursive: true });
4053
- const stream = createWriteStream(`${dir}/file.bin`);
4054
- stream.id = id;
4251
+ const stream = createWriteStream(this.getPath(id, filename, opts.extension));
4055
4252
  stream.done = false;
4056
4253
  stream.on('finish', () => {
4057
- writeFile$2(`${dir}/filename.txt`, filename);
4058
- writeFile$2(`${dir}/metadata.json`, JSON.stringify(opts?.metadata || {}));
4254
+ stream.id = id;
4059
4255
  stream.done = true;
4256
+ writeFileSync(`${dir}/metadata.json`, JSON.stringify(opts?.metadata || {}));
4060
4257
  });
4061
4258
  return stream;
4062
4259
  }
4063
- openDownloadStream(id) {
4064
- return createReadStream(`${this.dir}/${id.toHexString()}/file.bin`, { autoClose: true, emitClose: true });
4260
+ openDownloadStream(asset) {
4261
+ return createReadStream(this.getPath(asset.streamId, asset.filename, asset.metadata.extension), { autoClose: true, emitClose: true });
4065
4262
  }
4066
- delete(id) {
4067
- return rm(`${this.dir}/${id.toHexString()}`, { recursive: true, force: true });
4263
+ delete(asset) {
4264
+ return rm(this.getPath(asset.streamId, asset.filename, asset.metadata.extension), { recursive: true, force: true });
4265
+ }
4266
+ getPath(id, name, ext) {
4267
+ return join(this.dir, id.toHexString(), `${name}.${ext}`);
4068
4268
  }
4069
4269
  };
4070
4270
  AssetLocalDriver = __decorate([
4071
4271
  injectable(),
4072
- __param(0, inject(LOCAL_DIR)),
4272
+ __param(0, inject(ASSET_LOCAL_DIR)),
4073
4273
  __metadata("design:paramtypes", [String])
4074
4274
  ], AssetLocalDriver);
4075
4275
 
4276
+ function createFallbackReadable(primary, fallbackFactory) {
4277
+ const proxy = new PassThrough();
4278
+ let hasSwitched = false;
4279
+ const switchToFallback = () => {
4280
+ if (hasSwitched)
4281
+ return;
4282
+ hasSwitched = true;
4283
+ // 1. Cleanup the failing primary
4284
+ primary.unpipe(proxy);
4285
+ primary.destroy();
4286
+ // 2. Initialize and pipe the fallback
4287
+ const fallback = fallbackFactory();
4288
+ // On the fallback, we allow it to end the proxy naturally
4289
+ fallback.pipe(proxy);
4290
+ fallback.on('error', (err) => {
4291
+ proxy.emit('error', err); // If fallback fails too, it's a hard error
4292
+ });
4293
+ };
4294
+ // Pipe primary but prevent it from closing the proxy automatically
4295
+ primary.pipe(proxy, { end: false });
4296
+ primary.on('error', () => {
4297
+ switchToFallback();
4298
+ });
4299
+ // Handle the "Early End" case (e.g., stream closed before data finished)
4300
+ primary.on('end', () => {
4301
+ if (!hasSwitched) {
4302
+ // If we finished successfully without erroring, close the proxy
4303
+ proxy.end();
4304
+ }
4305
+ });
4306
+ return proxy;
4307
+ }
4308
+
4309
+ let AssetStorageProxyDriver = class AssetStorageProxyDriver {
4310
+ config;
4311
+ baseUrl;
4312
+ url;
4313
+ constructor(config) {
4314
+ this.config = config;
4315
+ this.baseUrl = this.config.resolve("storageProxyUri");
4316
+ this.url = this.baseUrl + this.config.resolve("storageProxyBucket");
4317
+ }
4318
+ openUploadStream(_, opts) {
4319
+ const id = new ObjectId$1();
4320
+ const stream = got.stream.put(this.getUrl(id, opts.extension), { headers: {
4321
+ 'Content-Type': opts?.contentType || 'application/octet-stream'
4322
+ } });
4323
+ stream.done = false;
4324
+ stream.on('finish', () => {
4325
+ stream.id = id;
4326
+ stream.done = true;
4327
+ });
4328
+ return stream;
4329
+ }
4330
+ openDownloadStream(asset) {
4331
+ return createFallbackReadable(got.stream.get(this.getUrl(asset.streamId, asset.metadata.extension)), () => got.stream.get(this.getUrl(asset.streamId)));
4332
+ }
4333
+ async delete(asset) {
4334
+ try {
4335
+ await got.delete(this.getUrl(asset.streamId, asset.metadata.extension));
4336
+ }
4337
+ catch (e) {
4338
+ await got.delete(this.getUrl(asset.streamId));
4339
+ }
4340
+ }
4341
+ getUrl(id, ext) {
4342
+ return !ext ? `${this.url}/${id.toHexString()}` : `${this.url}/${id.toHexString()}.${ext}`;
4343
+ }
4344
+ };
4345
+ AssetStorageProxyDriver = __decorate([
4346
+ injectable(),
4347
+ __metadata("design:paramtypes", [Configuration])
4348
+ ], AssetStorageProxyDriver);
4349
+
4076
4350
  class BaseDoc {
4077
4351
  _id;
4078
4352
  /**
@@ -4450,6 +4724,10 @@ function createServices() {
4450
4724
  new Parameter("mongoDb", "node-backend"),
4451
4725
  new Parameter("mongoUser", null),
4452
4726
  new Parameter("mongoPassword", null),
4727
+ new Parameter("storageProxyUri", "http://localhost:4500/files", prepareUrlSlash),
4728
+ new Parameter("storageProxyBucket", "something"),
4729
+ new Parameter("assetsMainDriver", "grid"),
4730
+ new Parameter("assetsMissingDriver", "grid"),
4453
4731
  new Parameter("nodeEnv", "production"),
4454
4732
  new Parameter("appPort", 80),
4455
4733
  new Parameter("zmqPort", 3000),
@@ -4619,11 +4897,16 @@ async function setupBackend(config, providers, parent) {
4619
4897
  diContainer.register(OPENAPI_VALIDATION, {
4620
4898
  useValue: config.customValidation || (() => null)
4621
4899
  });
4622
- diContainer.register(LOCAL_DIR, {
4900
+ diContainer.register(ASSET_LOCAL_DIR, {
4623
4901
  useValue: config.assetLocalDir || "assets_files"
4624
4902
  });
4625
- diContainer.register(ASSET_DRIVER, {
4626
- useClass: config.assetDriver || AssetGridDriver
4903
+ diContainer.register(ASSET_DRIVER_FACTORIES, {
4904
+ useValue: {
4905
+ grid: container => container.resolve(AssetGridDriver),
4906
+ storageProxy: container => container.resolve(AssetStorageProxyDriver),
4907
+ local: container => container.resolve(AssetLocalDriver),
4908
+ ...(config.assetDrivers || {}),
4909
+ }
4627
4910
  });
4628
4911
  diContainers.appContainer = diContainers.appContainer || diContainer;
4629
4912
  // Authentication
@@ -4694,5 +4977,5 @@ async function setupBackend(config, providers, parent) {
4694
4977
  * Generated bundle index. Do not edit.
4695
4978
  */
4696
4979
 
4697
- export { AssetGridDriver, AssetImageParams, AssetLocalDriver, AssetProcessor, AssetResolver, Assets, AuthController, BackendProvider, BaseDoc, Cache, CacheProcessor, Configuration, ConsoleColor, DI_CONTAINER, DocumentArray, EXPRESS, EndpointProvider, ErrorHandlerMiddleware, FIXTURE, Fixtures, Gallery, GalleryCache, GalleryController, HTTP_SERVER, IdGenerator, IsDocumented, IsFile, IsObjectId, JOB, JobManager, JsonResponse, LanguageMiddleware, LazyAssetGenerator, LazyAssets, Logger, MailSender, MemoryCache, MongoConnector, OPENAPI_VALIDATION, OpenApi, PARAMETER, Parameter, PrimitiveArray, Progresses, ResolveEntity, ResponseType, SOCKET_CONTROLLERS, SOCKET_SERVER, TERMINAL_COMMAND, TemplateRenderer, TerminalManager, TokenGenerator, TranslationProvider, Translator, Type, UserManager, assign, broadcast, bufferToStream, camelCaseToDash, colorize, convertValue, copy, copyStream, createIdString, createServices, createTransformer, deleteFile, deleteFromBucket, fileTypeFromBuffer, filter, firstItem, flatten, getConstructorName, getDirName, getExtension, getFileName, getFunctionParams, getType, getValue, groupBy, gunzipPromised, gzipPromised, hydratePopulated, idToString, injectServices, isArray, isBoolean, isBuffer, isConstructor, isDate, isDefined, isFunction, isInterface, isNullOrUndefined, isObject, isObjectId, isPrimitive, isString, isType, jsonHighlight, lastItem, lcFirst, letsLookupStage, lookupStages, matchField, matchFieldStages, matchStage, md5, mkdirRecursive, multiSubscription, observableFromFunction, padLeft, padRight, paginate, paginateAggregations, prepareUrl, prepareUrlEmpty, prepareUrlSlash, projectStage, promiseTimeout, rand, random, readAndDeleteFile, readFile, regexEscape, regroup, replaceSpecialChars, resolveUser, runCommand, service, setupBackend, streamToBuffer, toImage, ucFirst, uniqueItems, unwindStage, valueToPromise, wrapError, writeFile };
4980
+ export { AssetGridDriver, AssetImageParams, AssetLocalDriver, AssetProcessor, AssetResolver, AssetStorageProxyDriver, Assets, AuthController, BackendProvider, BaseDoc, Cache, CacheProcessor, Configuration, ConsoleColor, DI_CONTAINER, DocumentArray, EXPRESS, EndpointProvider, ErrorHandlerMiddleware, FIXTURE, Fixtures, Gallery, GalleryCache, GalleryController, HTTP_SERVER, IdGenerator, IsDocumented, IsFile, IsObjectId, JOB, JobManager, JsonResponse, LanguageMiddleware, LazyAssetGenerator, LazyAssets, Logger, MailSender, MemoryCache, MongoConnector, OPENAPI_VALIDATION, OpenApi, PARAMETER, Parameter, PrimitiveArray, Progresses, ResolveEntity, ResponseType, SOCKET_CONTROLLERS, SOCKET_SERVER, TERMINAL_COMMAND, TemplateRenderer, TerminalManager, TokenGenerator, TranslationProvider, Translator, Type, UserManager, assign, broadcast, bufferToStream, camelCaseToDash, colorize, convertValue, copy, copyStream, createIdString, createServices, createTransformer, deleteFile, deleteFromBucket, fileTypeFromBuffer, filter, firstItem, flatten, getConstructorName, getDirName, getExtension, getFileName, getFunctionParams, getType, getValue, groupBy, gunzipPromised, gzipPromised, hydratePopulated, idToString, injectServices, isArray, isBoolean, isBuffer, isConstructor, isDate, isDefined, isFunction, isInterface, isNullOrUndefined, isObject, isObjectId, isPrimitive, isString, isType, jsonHighlight, lastItem, lcFirst, letsLookupStage, lookupStages, matchField, matchFieldStages, matchStage, md5, mkdirRecursive, multiSubscription, observableFromFunction, padLeft, padRight, paginate, paginateAggregations, prepareUrl, prepareUrlEmpty, prepareUrlSlash, projectStage, promiseTimeout, rand, random, readAndDeleteFile, readFile, regexEscape, regroup, replaceSpecialChars, resolveUser, runCommand, service, setupBackend, streamToBuffer, toImage, ucFirst, uniqueItems, unwindStage, valueToPromise, wrapError, writeFile };
4698
4981
  //# sourceMappingURL=stemy-backend.mjs.map