@fdm-monster/server 2.1.0 → 2.1.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/.yarn/install-state.gz +0 -0
- package/.yarn/releases/{yarn-4.13.0.cjs → yarn-4.14.1.cjs} +288 -288
- package/.yarnrc.yml +5 -1
- package/RELEASE_NOTES.MD +14 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorateMetadata.js +1 -1
- package/dist/consoles/typeorm-create.js.map +1 -1
- package/dist/consoles/typeorm-generate.js.map +1 -1
- package/dist/consoles/typeorm-migrate.js.map +1 -1
- package/dist/constants/authorization.constants.js.map +1 -1
- package/dist/container.js.map +1 -1
- package/dist/controllers/api-key.controller.js +2 -2
- package/dist/controllers/api-key.controller.js.map +1 -1
- package/dist/controllers/auth.controller.js +5 -3
- package/dist/controllers/auth.controller.js.map +1 -1
- package/dist/controllers/batch-call.controller.js +2 -2
- package/dist/controllers/batch-call.controller.js.map +1 -1
- package/dist/controllers/camera-stream.controller.js +2 -2
- package/dist/controllers/camera-stream.controller.js.map +1 -1
- package/dist/controllers/file-storage.controller.js +2 -2
- package/dist/controllers/file-storage.controller.js.map +1 -1
- package/dist/controllers/first-time-setup.controller.js +2 -2
- package/dist/controllers/first-time-setup.controller.js.map +1 -1
- package/dist/controllers/floor.controller.js +2 -2
- package/dist/controllers/floor.controller.js.map +1 -1
- package/dist/controllers/metrics.controller.js +2 -2
- package/dist/controllers/metrics.controller.js.map +1 -1
- package/dist/controllers/print-job.controller.js +2 -2
- package/dist/controllers/print-job.controller.js.map +1 -1
- package/dist/controllers/print-queue.controller.js +2 -2
- package/dist/controllers/print-queue.controller.js.map +1 -1
- package/dist/controllers/printer-files.controller.js +2 -2
- package/dist/controllers/printer-files.controller.js.map +1 -1
- package/dist/controllers/printer-maintenance-log.controller.js +2 -2
- package/dist/controllers/printer-maintenance-log.controller.js.map +1 -1
- package/dist/controllers/printer-settings.controller.js +2 -2
- package/dist/controllers/printer-settings.controller.js.map +1 -1
- package/dist/controllers/printer-tag.controller.js +2 -2
- package/dist/controllers/printer-tag.controller.js.map +1 -1
- package/dist/controllers/printer.controller.js +2 -2
- package/dist/controllers/printer.controller.js.map +1 -1
- package/dist/controllers/server-private.controller.js +2 -2
- package/dist/controllers/server-private.controller.js.map +1 -1
- package/dist/controllers/server-public.controller.js +2 -2
- package/dist/controllers/server-public.controller.js.map +1 -1
- package/dist/controllers/settings.controller.js +2 -2
- package/dist/controllers/settings.controller.js.map +1 -1
- package/dist/controllers/slicer-compat.controller.js +2 -2
- package/dist/controllers/slicer-compat.controller.js.map +1 -1
- package/dist/controllers/user.controller.js +2 -2
- package/dist/controllers/user.controller.js.map +1 -1
- package/dist/entities/api-key.entity.js +2 -2
- package/dist/entities/camera-stream.entity.js +2 -2
- package/dist/entities/floor-position.entity.js +2 -2
- package/dist/entities/floor.entity.js +2 -2
- package/dist/entities/print-job.entity.js +2 -2
- package/dist/entities/printer-maintenance-log.entity.js +2 -2
- package/dist/entities/printer-tag.entity.js +2 -2
- package/dist/entities/printer.entity.js +2 -2
- package/dist/entities/refresh-token.entity.js +2 -2
- package/dist/entities/role.entity.js +2 -2
- package/dist/entities/settings.entity.js +2 -2
- package/dist/entities/tag.entity.js +2 -2
- package/dist/entities/user-role.entity.js +2 -2
- package/dist/entities/user.entity.js +2 -2
- package/dist/exceptions/failed-dependency.exception.js.map +1 -1
- package/dist/exceptions/job.exceptions.js.map +1 -1
- package/dist/exceptions/runtime.exceptions.js.map +1 -1
- package/dist/handlers/event-emitter.js.map +1 -1
- package/dist/handlers/logger-factory.js.map +1 -1
- package/dist/handlers/logger.js.map +1 -1
- package/dist/handlers/logging/file-logging.transport.js +1 -2
- package/dist/handlers/logging/file-logging.transport.js.map +1 -1
- package/dist/handlers/logging/loki-logging.transport.js.map +1 -1
- package/dist/handlers/logging/static.logger.js.map +1 -1
- package/dist/handlers/validators.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/api-key.strategy.js.map +1 -1
- package/dist/middleware/authenticate.js.map +1 -1
- package/dist/middleware/database.js.map +1 -1
- package/dist/middleware/demo.middleware.js.map +1 -1
- package/dist/middleware/exception.filter.js.map +1 -1
- package/dist/middleware/global.middleware.js.map +1 -1
- package/dist/middleware/param-converter.middleware.js.map +1 -1
- package/dist/middleware/passport.js.map +1 -1
- package/dist/middleware/printer-resolver.js.map +1 -1
- package/dist/middleware/printer.js.map +1 -1
- package/dist/middleware/slicer-api-key.middleware.js.map +1 -1
- package/dist/middleware/socketio.middleware.js.map +1 -1
- package/dist/migrations/1706829146617-InitSqlite.js.map +1 -1
- package/dist/migrations/1707494762198-PrinterGroup.js.map +1 -1
- package/dist/migrations/1708465930665-ChangePrintCompletionDeletePrinterCascade.js.map +1 -1
- package/dist/migrations/1713300747465-ChangeRoleNameUnique.js.map +1 -1
- package/dist/migrations/1713897879622-AddPrinterType.js.map +1 -1
- package/dist/migrations/1720338804844-RemovePrinterFile.js.map +1 -1
- package/dist/migrations/1745141688926-AddPrinterUsernamePassword.js.map +1 -1
- package/dist/migrations/1766576698569-DropPermissions.js.map +1 -1
- package/dist/migrations/1767278216516-ChangeCameraPrinterOnDeleteSetNull.js.map +1 -1
- package/dist/migrations/1767279607392-DropCustomGcode.js.map +1 -1
- package/dist/migrations/1767291804417-DropPrintCompletions.js.map +1 -1
- package/dist/migrations/1767352862576-DropSettingsFileClean.js.map +1 -1
- package/dist/migrations/1767355639023-ChangeFloorLevelToOrder.js.map +1 -1
- package/dist/migrations/1767370191762-ChangeFloorNonUniqueOrder.js.map +1 -1
- package/dist/migrations/1767432108916-RenameGroupToTag.js.map +1 -1
- package/dist/migrations/1767451444137-AddPrintJob.js.map +1 -1
- package/dist/migrations/1767909428129-AddPrinterMaintenanceLog.js.map +1 -1
- package/dist/migrations/1778446203015-AddApiKey.js.map +1 -1
- package/dist/plugins/controllers-plugin.js.map +1 -1
- package/dist/server.constants.js +2 -1
- package/dist/server.constants.js.map +1 -1
- package/dist/server.core.js.map +1 -1
- package/dist/server.env.js.map +1 -1
- package/dist/server.host.js.map +1 -1
- package/dist/services/authentication/auth.service.js.map +1 -1
- package/dist/services/authentication/jwt.service.js.map +1 -1
- package/dist/services/bambu/bambu-ftp.adapter.js.map +1 -1
- package/dist/services/bambu/bambu-mqtt.adapter.js.map +1 -1
- package/dist/services/bambu/bambu.client.js.map +1 -1
- package/dist/services/bambu.api.js.map +1 -1
- package/dist/services/core/batch-call.service.js.map +1 -1
- package/dist/services/core/client-bundle.service.js.map +1 -1
- package/dist/services/core/config.service.js +4 -0
- package/dist/services/core/config.service.js.map +1 -1
- package/dist/services/core/cradle.service.js.map +1 -1
- package/dist/services/core/github.service.js.map +1 -1
- package/dist/services/core/http-client.factory.js.map +1 -1
- package/dist/services/core/logs-manager.service.js.map +1 -1
- package/dist/services/core/monsterpi.service.js.map +1 -1
- package/dist/services/core/multer.service.js.map +1 -1
- package/dist/services/core/server-release.service.js.map +1 -1
- package/dist/services/core/yaml.service.js.map +1 -1
- package/dist/services/file-analysis.service.js.map +1 -1
- package/dist/services/file-storage.service.js.map +1 -1
- package/dist/services/moonraker/moonraker-websocket.adapter.js.map +1 -1
- package/dist/services/moonraker/moonraker.client.js.map +1 -1
- package/dist/services/moonraker.api.js.map +1 -1
- package/dist/services/octoprint/octoprint-api.routes.js.map +1 -1
- package/dist/services/octoprint/octoprint-websocket.adapter.js.map +1 -1
- package/dist/services/octoprint/octoprint.client.js.map +1 -1
- package/dist/services/octoprint/utils/api.utils.js.map +1 -1
- package/dist/services/octoprint/utils/file.utils.js.map +1 -1
- package/dist/services/octoprint/utils/octoprint-http-client.builder.js.map +1 -1
- package/dist/services/octoprint.api.js.map +1 -1
- package/dist/services/orm/api-key.service.js.map +1 -1
- package/dist/services/orm/base.service.js.map +1 -1
- package/dist/services/orm/camera-stream.service.js.map +1 -1
- package/dist/services/orm/floor-position.service.js.map +1 -1
- package/dist/services/orm/floor.service.js.map +1 -1
- package/dist/services/orm/permission.service.js.map +1 -1
- package/dist/services/orm/print-job.service.js.map +1 -1
- package/dist/services/orm/printer-maintenance-log.service.js.map +1 -1
- package/dist/services/orm/printer-tag.service.js.map +1 -1
- package/dist/services/orm/printer.service.js.map +1 -1
- package/dist/services/orm/refresh-token.service.js.map +1 -1
- package/dist/services/orm/role.service.js.map +1 -1
- package/dist/services/orm/settings.service.js.map +1 -1
- package/dist/services/orm/user-role.service.js.map +1 -1
- package/dist/services/orm/user.service.js.map +1 -1
- package/dist/services/print-file-downloader.service.js.map +1 -1
- package/dist/services/print-queue.service.js.map +1 -1
- package/dist/services/printer-api.factory.js.map +1 -1
- package/dist/services/printer-api.interface.js.map +1 -1
- package/dist/services/prusa-link/prusa-link-http-polling.adapter.js.map +1 -1
- package/dist/services/prusa-link/prusa-link.api.js.map +1 -1
- package/dist/services/prusa-link/utils/digest-auth.util.js +19 -12
- package/dist/services/prusa-link/utils/digest-auth.util.js.map +1 -1
- package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js +45 -11
- package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js.map +1 -1
- package/dist/services/socket.factory.js.map +1 -1
- package/dist/services/task-manager.service.js.map +1 -1
- package/dist/services/typeorm/typeorm.service.js.map +1 -1
- package/dist/services/validators/printer-service.validation.js.map +1 -1
- package/dist/shared/default-http-client.builder.js.map +1 -1
- package/dist/shared/load-controllers.js.map +1 -1
- package/dist/shared/runtime-settings.migration.js.map +1 -1
- package/dist/shared/websocket-rpc-extended.adapter.js.map +1 -1
- package/dist/shared/websocket.adapter.js.map +1 -1
- package/dist/state/file-upload-tracker.cache.js.map +1 -1
- package/dist/state/floor.store.js.map +1 -1
- package/dist/state/printer-events.cache.js.map +1 -1
- package/dist/state/printer-socket.store.js.map +1 -1
- package/dist/state/printer-thumbnail.cache.js.map +1 -1
- package/dist/state/printer.cache.js.map +1 -1
- package/dist/state/settings.store.js.map +1 -1
- package/dist/state/socket-io.gateway.js.map +1 -1
- package/dist/state/test-printer-socket.store.js.map +1 -1
- package/dist/tasks/boot.task.js.map +1 -1
- package/dist/tasks/client-bundle.task.js.map +1 -1
- package/dist/tasks/print-job-analysis.task.js.map +1 -1
- package/dist/tasks/printer-websocket-restore.task.js.map +1 -1
- package/dist/tasks/printer-websocket.task.js.map +1 -1
- package/dist/tasks/socketio.task.js.map +1 -1
- package/dist/tasks/software-update.task.js.map +1 -1
- package/dist/tasks.js.map +1 -1
- package/dist/utils/array.util.js.map +1 -1
- package/dist/utils/bgcode/bgcode-thumbnail.parser.js.map +1 -1
- package/dist/utils/bgcode/bgcode.utils.js.map +1 -1
- package/dist/utils/bgcode/heatshrink-decoder.js.map +1 -1
- package/dist/utils/bgcode/png-encoder.js.map +1 -1
- package/dist/utils/bgcode/qoi-decoder.js.map +1 -1
- package/dist/utils/cache/key-diff.cache.js.map +1 -1
- package/dist/utils/correlation-token.util.js.map +1 -1
- package/dist/utils/crypto.utils.js.map +1 -1
- package/dist/utils/env.utils.js.map +1 -1
- package/dist/utils/error.utils.js.map +1 -1
- package/dist/utils/fs.utils.js.map +1 -1
- package/dist/utils/gcode.utils.js.map +1 -1
- package/dist/utils/image-dimensions.js.map +1 -1
- package/dist/utils/job-stats.util.js.map +1 -1
- package/dist/utils/normalize-url.js.map +1 -1
- package/dist/utils/parsers/3mf.parser.js.map +1 -1
- package/dist/utils/parsers/bgcode.parser.js.map +1 -1
- package/dist/utils/parsers/gcode.parser.js.map +1 -1
- package/dist/utils/pretty-print.utils.js.map +1 -1
- package/dist/utils/semver.utils.js.map +1 -1
- package/dist/utils/swagger/decorators.js.map +1 -1
- package/dist/utils/swagger/generator.js.map +1 -1
- package/dist/utils/swagger/swagger.js.map +1 -1
- package/dist/utils/thumbnail.util.js.map +1 -1
- package/dist/utils/time.utils.js.map +1 -1
- package/dist/utils/url.utils.js.map +1 -1
- package/package.json +10 -7
- package/packages/consoles/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-diff.cache.js","names":[],"sources":["../../../src/utils/cache/key-diff.cache.ts"],"sourcesContent":["export class KeyDiffCache<T> {\n deletedKeys: number[] = [];\n updatedKeys: number[] = [];\n\n protected keyValueStore = new Map<number, T>();\n\n public async getAllValues(): Promise<T[]> {\n return Array.from(this.keyValueStore.values());\n }\n\n public async getAllKeyValues(): Promise<Record<number, T>> {\n return Object.fromEntries(this.keyValueStore);\n }\n\n public async getValue(key: number): Promise<T | undefined> {\n return this.keyValueStore.get(key);\n }\n\n protected async setKeyValuesBatch(keyValues: Array<{ key: number; value: T }>, markUpdated: boolean = true) {\n for (const { key, value } of keyValues) {\n await this.setKeyValue(key, value, false);\n }\n if (markUpdated) {\n const updatedKeys = keyValues.map(({ key }) => key);\n this.batchMarkUpdated(updatedKeys);\n }\n }\n\n protected async deleteKeysBatch(keys: number[], markDeleted: boolean = true) {\n for (const key of keys) {\n await this.deleteKeyValue(key, false);\n }\n if (markDeleted) {\n this.batchMarkDeleted(keys);\n }\n }\n\n protected async setKeyValue(key: number, value: T, markUpdated: boolean = true) {\n this.keyValueStore.set(key, value);\n if (markUpdated) {\n this.markUpdated(key);\n }\n }\n\n protected async deleteKeyValue(key: number, markDeleted: boolean = true) {\n this.keyValueStore.delete(key);\n if (markDeleted) {\n this.markDeleted(key);\n }\n }\n\n protected batchMarkDeleted(keys: number[]) {\n for (const key of keys) {\n this.markDeleted(key);\n }\n }\n\n protected markUpdated(key: number) {\n const deletedIndex = this.deletedKeys.indexOf(key);\n if (deletedIndex !== -1) {\n this.deletedKeys.splice(deletedIndex, 1);\n }\n if (!this.updatedKeys.includes(key)) {\n this.updatedKeys.push(key);\n }\n }\n\n protected markDeleted(key: number) {\n const updatedIndex = this.updatedKeys.indexOf(key);\n if (updatedIndex !== -1) {\n this.updatedKeys.splice(updatedIndex, 1);\n }\n if (!this.deletedKeys.includes(key)) {\n this.deletedKeys.push(key);\n }\n }\n\n protected resetDiffs() {\n this.deletedKeys = [];\n this.updatedKeys = [];\n }\n\n private batchMarkUpdated(keys: number[]) {\n for (const key of keys) {\n this.markUpdated(key);\n }\n }\n}\n"],"mappings":";AAAA,IAAa,eAAb,MAA6B;CAC3B,cAAwB,EAAE;CAC1B,cAAwB,EAAE;CAE1B,gCAA0B,IAAI,KAAgB;CAE9C,MAAa,eAA6B;
|
|
1
|
+
{"version":3,"file":"key-diff.cache.js","names":[],"sources":["../../../src/utils/cache/key-diff.cache.ts"],"sourcesContent":["export class KeyDiffCache<T> {\n deletedKeys: number[] = [];\n updatedKeys: number[] = [];\n\n protected keyValueStore = new Map<number, T>();\n\n public async getAllValues(): Promise<T[]> {\n return Array.from(this.keyValueStore.values());\n }\n\n public async getAllKeyValues(): Promise<Record<number, T>> {\n return Object.fromEntries(this.keyValueStore);\n }\n\n public async getValue(key: number): Promise<T | undefined> {\n return this.keyValueStore.get(key);\n }\n\n protected async setKeyValuesBatch(keyValues: Array<{ key: number; value: T }>, markUpdated: boolean = true) {\n for (const { key, value } of keyValues) {\n await this.setKeyValue(key, value, false);\n }\n if (markUpdated) {\n const updatedKeys = keyValues.map(({ key }) => key);\n this.batchMarkUpdated(updatedKeys);\n }\n }\n\n protected async deleteKeysBatch(keys: number[], markDeleted: boolean = true) {\n for (const key of keys) {\n await this.deleteKeyValue(key, false);\n }\n if (markDeleted) {\n this.batchMarkDeleted(keys);\n }\n }\n\n protected async setKeyValue(key: number, value: T, markUpdated: boolean = true) {\n this.keyValueStore.set(key, value);\n if (markUpdated) {\n this.markUpdated(key);\n }\n }\n\n protected async deleteKeyValue(key: number, markDeleted: boolean = true) {\n this.keyValueStore.delete(key);\n if (markDeleted) {\n this.markDeleted(key);\n }\n }\n\n protected batchMarkDeleted(keys: number[]) {\n for (const key of keys) {\n this.markDeleted(key);\n }\n }\n\n protected markUpdated(key: number) {\n const deletedIndex = this.deletedKeys.indexOf(key);\n if (deletedIndex !== -1) {\n this.deletedKeys.splice(deletedIndex, 1);\n }\n if (!this.updatedKeys.includes(key)) {\n this.updatedKeys.push(key);\n }\n }\n\n protected markDeleted(key: number) {\n const updatedIndex = this.updatedKeys.indexOf(key);\n if (updatedIndex !== -1) {\n this.updatedKeys.splice(updatedIndex, 1);\n }\n if (!this.deletedKeys.includes(key)) {\n this.deletedKeys.push(key);\n }\n }\n\n protected resetDiffs() {\n this.deletedKeys = [];\n this.updatedKeys = [];\n }\n\n private batchMarkUpdated(keys: number[]) {\n for (const key of keys) {\n this.markUpdated(key);\n }\n }\n}\n"],"mappings":";AAAA,IAAa,eAAb,MAA6B;CAC3B,cAAwB,EAAE;CAC1B,cAAwB,EAAE;CAE1B,gCAA0B,IAAI,KAAgB;CAE9C,MAAa,eAA6B;EACxC,OAAO,MAAM,KAAK,KAAK,cAAc,QAAQ,CAAC;;CAGhD,MAAa,kBAA8C;EACzD,OAAO,OAAO,YAAY,KAAK,cAAc;;CAG/C,MAAa,SAAS,KAAqC;EACzD,OAAO,KAAK,cAAc,IAAI,IAAI;;CAGpC,MAAgB,kBAAkB,WAA6C,cAAuB,MAAM;EAC1G,KAAK,MAAM,EAAE,KAAK,WAAW,WAC3B,MAAM,KAAK,YAAY,KAAK,OAAO,MAAM;EAE3C,IAAI,aAAa;GACf,MAAM,cAAc,UAAU,KAAK,EAAE,UAAU,IAAI;GACnD,KAAK,iBAAiB,YAAY;;;CAItC,MAAgB,gBAAgB,MAAgB,cAAuB,MAAM;EAC3E,KAAK,MAAM,OAAO,MAChB,MAAM,KAAK,eAAe,KAAK,MAAM;EAEvC,IAAI,aACF,KAAK,iBAAiB,KAAK;;CAI/B,MAAgB,YAAY,KAAa,OAAU,cAAuB,MAAM;EAC9E,KAAK,cAAc,IAAI,KAAK,MAAM;EAClC,IAAI,aACF,KAAK,YAAY,IAAI;;CAIzB,MAAgB,eAAe,KAAa,cAAuB,MAAM;EACvE,KAAK,cAAc,OAAO,IAAI;EAC9B,IAAI,aACF,KAAK,YAAY,IAAI;;CAIzB,iBAA2B,MAAgB;EACzC,KAAK,MAAM,OAAO,MAChB,KAAK,YAAY,IAAI;;CAIzB,YAAsB,KAAa;EACjC,MAAM,eAAe,KAAK,YAAY,QAAQ,IAAI;EAClD,IAAI,iBAAiB,IACnB,KAAK,YAAY,OAAO,cAAc,EAAE;EAE1C,IAAI,CAAC,KAAK,YAAY,SAAS,IAAI,EACjC,KAAK,YAAY,KAAK,IAAI;;CAI9B,YAAsB,KAAa;EACjC,MAAM,eAAe,KAAK,YAAY,QAAQ,IAAI;EAClD,IAAI,iBAAiB,IACnB,KAAK,YAAY,OAAO,cAAc,EAAE;EAE1C,IAAI,CAAC,KAAK,YAAY,SAAS,IAAI,EACjC,KAAK,YAAY,KAAK,IAAI;;CAI9B,aAAuB;EACrB,KAAK,cAAc,EAAE;EACrB,KAAK,cAAc,EAAE;;CAGvB,iBAAyB,MAAgB;EACvC,KAAK,MAAM,OAAO,MAChB,KAAK,YAAY,IAAI"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"correlation-token.util.js","names":[],"sources":["../../src/utils/correlation-token.util.ts"],"sourcesContent":["export function generateCorrelationToken() {\n return Math.random().toString(36).slice(2);\n}\n"],"mappings":";AAAA,SAAgB,2BAA2B;
|
|
1
|
+
{"version":3,"file":"correlation-token.util.js","names":[],"sources":["../../src/utils/correlation-token.util.ts"],"sourcesContent":["export function generateCorrelationToken() {\n return Math.random().toString(36).slice(2);\n}\n"],"mappings":";AAAA,SAAgB,2BAA2B;CACzC,OAAO,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.utils.js","names":[],"sources":["../../src/utils/crypto.utils.ts"],"sourcesContent":["import { compareSync, genSaltSync, hashSync } from \"bcryptjs\";\n\nexport function hashPassword(password: string) {\n const salt = genSaltSync(10);\n return hashSync(password, salt);\n}\n\nexport function comparePasswordHash(password: string, passwordHash: string): boolean {\n if (!password?.length) return false;\n return compareSync(password, passwordHash);\n}\n"],"mappings":";;AAEA,SAAgB,aAAa,UAAkB;
|
|
1
|
+
{"version":3,"file":"crypto.utils.js","names":[],"sources":["../../src/utils/crypto.utils.ts"],"sourcesContent":["import { compareSync, genSaltSync, hashSync } from \"bcryptjs\";\n\nexport function hashPassword(password: string) {\n const salt = genSaltSync(10);\n return hashSync(password, salt);\n}\n\nexport function comparePasswordHash(password: string, passwordHash: string): boolean {\n if (!password?.length) return false;\n return compareSync(password, passwordHash);\n}\n"],"mappings":";;AAEA,SAAgB,aAAa,UAAkB;CAE7C,OAAO,SAAS,UADH,YAAY,GACK,CAAC;;AAGjC,SAAgB,oBAAoB,UAAkB,cAA+B;CACnF,IAAI,CAAC,UAAU,QAAQ,OAAO;CAC9B,OAAO,YAAY,UAAU,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.utils.js","names":[],"sources":["../../src/utils/env.utils.ts"],"sourcesContent":["import { AppConstants } from \"@/server.constants\";\nimport dotenv from \"dotenv\";\nimport { join } from \"node:path\";\nimport { superRootPath } from \"@/utils/fs.utils\";\n\nif (process.env.NODE_ENV !== \"test\") {\n dotenv.config({\n path: process.env.ENV_FILE || join(superRootPath(), \".env\"),\n });\n}\n\nexport function getEnvOrDefault<T>(key: string, defaultValue: T) {\n if (!Object.keys(process.env).includes(key) || !process.env[key]?.length) {\n return defaultValue;\n }\n return process.env[key] as T;\n}\n\nexport function isDevelopmentEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultDevelopmentEnv;\n}\n\nexport function isTestEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultTestEnv;\n}\n\nexport function isProductionEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultProductionEnv;\n}\n\nexport function isNode() {\n return \"NODE\" in process.env;\n}\n"],"mappings":";;;;;AAKA,IAAI,QAAQ,IAAI,aAAa,
|
|
1
|
+
{"version":3,"file":"env.utils.js","names":[],"sources":["../../src/utils/env.utils.ts"],"sourcesContent":["import { AppConstants } from \"@/server.constants\";\nimport dotenv from \"dotenv\";\nimport { join } from \"node:path\";\nimport { superRootPath } from \"@/utils/fs.utils\";\n\nif (process.env.NODE_ENV !== \"test\") {\n dotenv.config({\n path: process.env.ENV_FILE || join(superRootPath(), \".env\"),\n });\n}\n\nexport function getEnvOrDefault<T>(key: string, defaultValue: T) {\n if (!Object.keys(process.env).includes(key) || !process.env[key]?.length) {\n return defaultValue;\n }\n return process.env[key] as T;\n}\n\nexport function isDevelopmentEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultDevelopmentEnv;\n}\n\nexport function isTestEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultTestEnv;\n}\n\nexport function isProductionEnvironment() {\n return process.env.NODE_ENV === AppConstants.defaultProductionEnv;\n}\n\nexport function isNode() {\n return \"NODE\" in process.env;\n}\n"],"mappings":";;;;;AAKA,IAAI,QAAQ,IAAI,aAAa,QAC3B,OAAO,OAAO,EACZ,MAAM,QAAQ,IAAI,YAAY,KAAK,eAAe,EAAE,OAAO,EAC5D,CAAC;AAGJ,SAAgB,gBAAmB,KAAa,cAAiB;CAC/D,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,QAChE,OAAO;CAET,OAAO,QAAQ,IAAI;;AAGrB,SAAgB,2BAA2B;CACzC,OAAO,QAAQ,IAAI,aAAa,aAAa;;AAG/C,SAAgB,oBAAoB;CAClC,OAAO,QAAQ,IAAI,aAAa,aAAa;;AAG/C,SAAgB,0BAA0B;CACxC,OAAO,QAAQ,IAAI,aAAa,aAAa;;AAG/C,SAAgB,SAAS;CACvB,OAAO,UAAU,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.utils.js","names":[],"sources":["../../src/utils/error.utils.ts"],"sourcesContent":["export function errorSummary(e: any) {\n return e.message ? `${e.message}\\n ${e.stack}` : `'${e}'`;\n}\n"],"mappings":";AAAA,SAAgB,aAAa,GAAQ;
|
|
1
|
+
{"version":3,"file":"error.utils.js","names":[],"sources":["../../src/utils/error.utils.ts"],"sourcesContent":["export function errorSummary(e: any) {\n return e.message ? `${e.message}\\n ${e.stack}` : `'${e}'`;\n}\n"],"mappings":";AAAA,SAAgB,aAAa,GAAQ;CACnC,OAAO,EAAE,UAAU,GAAG,EAAE,QAAQ,KAAK,EAAE,UAAU,IAAI,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.utils.js","names":[],"sources":["../../src/utils/fs.utils.ts"],"sourcesContent":["import { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getEnvOrDefault } from \"@/utils/env.utils\";\n\n/**\n * Get __dirname equivalent in ESM\n * @param importMetaUrl - Pass import.meta.url from the calling module\n */\nexport function getDirname(importMetaUrl: string): string {\n return dirname(fileURLToPath(importMetaUrl));\n}\n\nexport function getDatabaseFilePath() {\n const dbFile = getEnvOrDefault(AppConstants.DATABASE_FILE, AppConstants.defaultDatabaseFile);\n if (dbFile === \":memory:\") {\n return dbFile;\n }\n\n const dbFolder = getDatabaseFolder();\n return join(dbFolder, dbFile);\n}\n\nexport function getDatabaseFolder() {\n return getEnvOrDefault(AppConstants.DATABASE_PATH, join(superRootPath(), AppConstants.defaultDatabasePath));\n}\n\nexport function getMediaPath() {\n return getEnvOrDefault(AppConstants.MEDIA_PATH, join(superRootPath(), AppConstants.defaultBaseMediaPath));\n}\n\nexport function packageJsonPath() {\n return join(superRootPath(), \"./package.json\");\n}\n\nexport function ensureDirExists(dir: string) {\n if (existsSync(dir)) {\n return;\n }\n\n mkdirSync(dir, { recursive: true });\n}\n\n/**\n * Root where code is hosted, avoid using excessively\n */\nexport function superRootPath() {\n return join(getDirname(import.meta.url), \"../..\");\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,WAAW,eAA+B;
|
|
1
|
+
{"version":3,"file":"fs.utils.js","names":[],"sources":["../../src/utils/fs.utils.ts"],"sourcesContent":["import { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getEnvOrDefault } from \"@/utils/env.utils\";\n\n/**\n * Get __dirname equivalent in ESM\n * @param importMetaUrl - Pass import.meta.url from the calling module\n */\nexport function getDirname(importMetaUrl: string): string {\n return dirname(fileURLToPath(importMetaUrl));\n}\n\nexport function getDatabaseFilePath() {\n const dbFile = getEnvOrDefault(AppConstants.DATABASE_FILE, AppConstants.defaultDatabaseFile);\n if (dbFile === \":memory:\") {\n return dbFile;\n }\n\n const dbFolder = getDatabaseFolder();\n return join(dbFolder, dbFile);\n}\n\nexport function getDatabaseFolder() {\n return getEnvOrDefault(AppConstants.DATABASE_PATH, join(superRootPath(), AppConstants.defaultDatabasePath));\n}\n\nexport function getMediaPath() {\n return getEnvOrDefault(AppConstants.MEDIA_PATH, join(superRootPath(), AppConstants.defaultBaseMediaPath));\n}\n\nexport function packageJsonPath() {\n return join(superRootPath(), \"./package.json\");\n}\n\nexport function ensureDirExists(dir: string) {\n if (existsSync(dir)) {\n return;\n }\n\n mkdirSync(dir, { recursive: true });\n}\n\n/**\n * Root where code is hosted, avoid using excessively\n */\nexport function superRootPath() {\n return join(getDirname(import.meta.url), \"../..\");\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,WAAW,eAA+B;CACxD,OAAO,QAAQ,cAAc,cAAc,CAAC;;AAG9C,SAAgB,sBAAsB;CACpC,MAAM,SAAS,gBAAgB,aAAa,eAAe,aAAa,oBAAoB;CAC5F,IAAI,WAAW,YACb,OAAO;CAIT,OAAO,KADU,mBACG,EAAE,OAAO;;AAG/B,SAAgB,oBAAoB;CAClC,OAAO,gBAAgB,aAAa,eAAe,KAAK,eAAe,EAAE,aAAa,oBAAoB,CAAC;;AAG7G,SAAgB,eAAe;CAC7B,OAAO,gBAAgB,aAAa,YAAY,KAAK,eAAe,EAAE,aAAa,qBAAqB,CAAC;;AAG3G,SAAgB,kBAAkB;CAChC,OAAO,KAAK,eAAe,EAAE,iBAAiB;;AAGhD,SAAgB,gBAAgB,KAAa;CAC3C,IAAI,WAAW,IAAI,EACjB;CAGF,UAAU,KAAK,EAAE,WAAW,MAAM,CAAC;;;;;AAMrC,SAAgB,gBAAgB;CAC9B,OAAO,KAAK,WAAW,OAAO,KAAK,IAAI,EAAE,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gcode.utils.js","names":[],"sources":["../../src/utils/gcode.utils.ts"],"sourcesContent":["import { open } from \"node:fs/promises\";\n\nexport const gcodeScanningChunkSize = 64 * 1024; // 64 KB\n/**\n * Read local GCode metadata or thumbnails\n * @param filePath\n * @param numberOfLines\n */\nexport async function readLastLinesLocal(filePath: string, numberOfLines: number) {\n const file = await open(filePath, \"r\");\n\n try {\n const buffer = Buffer.alloc(gcodeScanningChunkSize);\n const fileSize = await file.stat();\n let lines: string[] = [];\n let position = fileSize.size;\n let iterationsLeft = 100;\n\n while (lines.length <= numberOfLines && position > 0) {\n iterationsLeft--;\n if (iterationsLeft <= 0) {\n throw new Error(\"Too many iterations reached, 'readLastLines' aborted\");\n }\n\n const bytesToRead = Math.min(gcodeScanningChunkSize, position);\n position -= bytesToRead;\n\n await file.read(buffer, 0, bytesToRead, position);\n const chunk = buffer.toString(\"utf-8\", 0, bytesToRead);\n\n // Prepend the chunk's lines\n lines = chunk.split(\"\\n\").concat(lines);\n\n await file.close();\n }\n\n return lines.slice(-numberOfLines);\n } catch (e) {\n await file.close();\n throw e;\n }\n}\n"],"mappings":";;AAEA,MAAa,yBAAyB,KAAK;;;;;;AAM3C,eAAsB,mBAAmB,UAAkB,eAAuB;CAChF,MAAM,OAAO,MAAM,KAAK,UAAU,IAAI;
|
|
1
|
+
{"version":3,"file":"gcode.utils.js","names":[],"sources":["../../src/utils/gcode.utils.ts"],"sourcesContent":["import { open } from \"node:fs/promises\";\n\nexport const gcodeScanningChunkSize = 64 * 1024; // 64 KB\n/**\n * Read local GCode metadata or thumbnails\n * @param filePath\n * @param numberOfLines\n */\nexport async function readLastLinesLocal(filePath: string, numberOfLines: number) {\n const file = await open(filePath, \"r\");\n\n try {\n const buffer = Buffer.alloc(gcodeScanningChunkSize);\n const fileSize = await file.stat();\n let lines: string[] = [];\n let position = fileSize.size;\n let iterationsLeft = 100;\n\n while (lines.length <= numberOfLines && position > 0) {\n iterationsLeft--;\n if (iterationsLeft <= 0) {\n throw new Error(\"Too many iterations reached, 'readLastLines' aborted\");\n }\n\n const bytesToRead = Math.min(gcodeScanningChunkSize, position);\n position -= bytesToRead;\n\n await file.read(buffer, 0, bytesToRead, position);\n const chunk = buffer.toString(\"utf-8\", 0, bytesToRead);\n\n // Prepend the chunk's lines\n lines = chunk.split(\"\\n\").concat(lines);\n\n await file.close();\n }\n\n return lines.slice(-numberOfLines);\n } catch (e) {\n await file.close();\n throw e;\n }\n}\n"],"mappings":";;AAEA,MAAa,yBAAyB,KAAK;;;;;;AAM3C,eAAsB,mBAAmB,UAAkB,eAAuB;CAChF,MAAM,OAAO,MAAM,KAAK,UAAU,IAAI;CAEtC,IAAI;EACF,MAAM,SAAS,OAAO,MAAM,uBAAuB;EACnD,MAAM,WAAW,MAAM,KAAK,MAAM;EAClC,IAAI,QAAkB,EAAE;EACxB,IAAI,WAAW,SAAS;EACxB,IAAI,iBAAiB;EAErB,OAAO,MAAM,UAAU,iBAAiB,WAAW,GAAG;GACpD;GACA,IAAI,kBAAkB,GACpB,MAAM,IAAI,MAAM,uDAAuD;GAGzE,MAAM,cAAc,KAAK,IAAI,wBAAwB,SAAS;GAC9D,YAAY;GAEZ,MAAM,KAAK,KAAK,QAAQ,GAAG,aAAa,SAAS;GAIjD,QAHc,OAAO,SAAS,SAAS,GAAG,YAG7B,CAAC,MAAM,KAAK,CAAC,OAAO,MAAM;GAEvC,MAAM,KAAK,OAAO;;EAGpB,OAAO,MAAM,MAAM,CAAC,cAAc;UAC3B,GAAG;EACV,MAAM,KAAK,OAAO;EAClB,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-dimensions.js","names":[],"sources":["../../src/utils/image-dimensions.ts"],"sourcesContent":["export function getImageDimensions(imageData: Buffer, format: string): { width: number; height: number } {\n if (format === \"PNG\") {\n return getPngDimensions(imageData);\n } else if (format === \"JPG\" || format === \"JPEG\") {\n return getJpgDimensions(imageData);\n }\n return { width: 0, height: 0 };\n}\n\nfunction getPngDimensions(data: Buffer): { width: number; height: number } {\n if (data.length < 24) {\n return { width: 0, height: 0 };\n }\n\n const width = data.readUInt32BE(16);\n const height = data.readUInt32BE(20);\n\n return { width, height };\n}\n\nfunction getJpgDimensions(data: Buffer): { width: number; height: number } {\n let offset = 2;\n\n while (offset < data.length) {\n if (data[offset] !== 0xff) break;\n\n const marker = data[offset + 1];\n offset += 2;\n\n if (marker === 0xc0 || marker === 0xc2) {\n if (offset + 5 < data.length) {\n const height = data.readUInt16BE(offset + 1);\n const width = data.readUInt16BE(offset + 3);\n return { width, height };\n }\n break;\n }\n\n const segmentLength = data.readUInt16BE(offset);\n offset += segmentLength;\n }\n\n return { width: 0, height: 0 };\n}\n"],"mappings":";AAAA,SAAgB,mBAAmB,WAAmB,QAAmD;
|
|
1
|
+
{"version":3,"file":"image-dimensions.js","names":[],"sources":["../../src/utils/image-dimensions.ts"],"sourcesContent":["export function getImageDimensions(imageData: Buffer, format: string): { width: number; height: number } {\n if (format === \"PNG\") {\n return getPngDimensions(imageData);\n } else if (format === \"JPG\" || format === \"JPEG\") {\n return getJpgDimensions(imageData);\n }\n return { width: 0, height: 0 };\n}\n\nfunction getPngDimensions(data: Buffer): { width: number; height: number } {\n if (data.length < 24) {\n return { width: 0, height: 0 };\n }\n\n const width = data.readUInt32BE(16);\n const height = data.readUInt32BE(20);\n\n return { width, height };\n}\n\nfunction getJpgDimensions(data: Buffer): { width: number; height: number } {\n let offset = 2;\n\n while (offset < data.length) {\n if (data[offset] !== 0xff) break;\n\n const marker = data[offset + 1];\n offset += 2;\n\n if (marker === 0xc0 || marker === 0xc2) {\n if (offset + 5 < data.length) {\n const height = data.readUInt16BE(offset + 1);\n const width = data.readUInt16BE(offset + 3);\n return { width, height };\n }\n break;\n }\n\n const segmentLength = data.readUInt16BE(offset);\n offset += segmentLength;\n }\n\n return { width: 0, height: 0 };\n}\n"],"mappings":";AAAA,SAAgB,mBAAmB,WAAmB,QAAmD;CACvG,IAAI,WAAW,OACb,OAAO,iBAAiB,UAAU;MAC7B,IAAI,WAAW,SAAS,WAAW,QACxC,OAAO,iBAAiB,UAAU;CAEpC,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG;;AAGhC,SAAS,iBAAiB,MAAiD;CACzE,IAAI,KAAK,SAAS,IAChB,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG;CAMhC,OAAO;EAAE,OAHK,KAAK,aAAa,GAGlB;EAAE,QAFD,KAAK,aAAa,GAEX;EAAE;;AAG1B,SAAS,iBAAiB,MAAiD;CACzE,IAAI,SAAS;CAEb,OAAO,SAAS,KAAK,QAAQ;EAC3B,IAAI,KAAK,YAAY,KAAM;EAE3B,MAAM,SAAS,KAAK,SAAS;EAC7B,UAAU;EAEV,IAAI,WAAW,OAAQ,WAAW,KAAM;GACtC,IAAI,SAAS,IAAI,KAAK,QAAQ;IAC5B,MAAM,SAAS,KAAK,aAAa,SAAS,EAAE;IAE5C,OAAO;KAAE,OADK,KAAK,aAAa,SAAS,EAC3B;KAAE;KAAQ;;GAE1B;;EAGF,MAAM,gBAAgB,KAAK,aAAa,OAAO;EAC/C,UAAU;;CAGZ,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"job-stats.util.js","names":[],"sources":["../../src/utils/job-stats.util.ts"],"sourcesContent":["import { PrintJob, PrintStatistics } from \"@/entities/print-job.entity\";\n\nexport function calculateJobDuration(startedAt: Date | null, endedAt: Date = new Date()): number | null {\n if (!startedAt) {\n return null;\n }\n return (endedAt.getTime() - startedAt.getTime()) / 1000;\n}\n\nexport interface StatisticsUpdateOptions {\n progress?: number;\n failureReason?: string;\n failureTime?: Date;\n}\n\nexport function initializeOrUpdateStatistics(\n job: PrintJob,\n endedAt: Date = new Date(),\n options: StatisticsUpdateOptions = {},\n): PrintStatistics {\n const actualPrintTimeSeconds = calculateJobDuration(job.startedAt, endedAt);\n\n if (!job.statistics) {\n return {\n startedAt: job.startedAt,\n endedAt,\n actualPrintTimeSeconds,\n progress: options.progress ?? job.progress ?? null,\n failureReason: options.failureReason,\n failureTime: options.failureTime,\n };\n } else {\n job.statistics.endedAt = endedAt;\n job.statistics.actualPrintTimeSeconds = actualPrintTimeSeconds;\n\n if (options.progress !== undefined) {\n job.statistics.progress = options.progress;\n }\n\n if (options.failureReason !== undefined) {\n job.statistics.failureReason = options.failureReason;\n job.statistics.failureTime = options.failureTime ?? endedAt;\n }\n\n return job.statistics;\n }\n}\n\nexport function updateStatisticsForCompletion(job: PrintJob, endedAt: Date = new Date()): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt, { progress: 100 });\n job.endedAt = endedAt;\n job.progress = 100;\n}\n\nexport function updateStatisticsForFailure(job: PrintJob, reason: string, endedAt: Date = new Date()): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt, {\n failureReason: reason,\n failureTime: endedAt,\n });\n job.endedAt = endedAt;\n job.statusReason = reason;\n}\n\nexport function updateStatisticsForCancellation(\n job: PrintJob,\n reason: string = \"Print cancelled by user\",\n endedAt: Date = new Date(),\n): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt);\n job.endedAt = endedAt;\n job.statusReason = reason;\n}\n"],"mappings":";AAEA,SAAgB,qBAAqB,WAAwB,0BAAgB,IAAI,MAAM,EAAiB;
|
|
1
|
+
{"version":3,"file":"job-stats.util.js","names":[],"sources":["../../src/utils/job-stats.util.ts"],"sourcesContent":["import { PrintJob, PrintStatistics } from \"@/entities/print-job.entity\";\n\nexport function calculateJobDuration(startedAt: Date | null, endedAt: Date = new Date()): number | null {\n if (!startedAt) {\n return null;\n }\n return (endedAt.getTime() - startedAt.getTime()) / 1000;\n}\n\nexport interface StatisticsUpdateOptions {\n progress?: number;\n failureReason?: string;\n failureTime?: Date;\n}\n\nexport function initializeOrUpdateStatistics(\n job: PrintJob,\n endedAt: Date = new Date(),\n options: StatisticsUpdateOptions = {},\n): PrintStatistics {\n const actualPrintTimeSeconds = calculateJobDuration(job.startedAt, endedAt);\n\n if (!job.statistics) {\n return {\n startedAt: job.startedAt,\n endedAt,\n actualPrintTimeSeconds,\n progress: options.progress ?? job.progress ?? null,\n failureReason: options.failureReason,\n failureTime: options.failureTime,\n };\n } else {\n job.statistics.endedAt = endedAt;\n job.statistics.actualPrintTimeSeconds = actualPrintTimeSeconds;\n\n if (options.progress !== undefined) {\n job.statistics.progress = options.progress;\n }\n\n if (options.failureReason !== undefined) {\n job.statistics.failureReason = options.failureReason;\n job.statistics.failureTime = options.failureTime ?? endedAt;\n }\n\n return job.statistics;\n }\n}\n\nexport function updateStatisticsForCompletion(job: PrintJob, endedAt: Date = new Date()): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt, { progress: 100 });\n job.endedAt = endedAt;\n job.progress = 100;\n}\n\nexport function updateStatisticsForFailure(job: PrintJob, reason: string, endedAt: Date = new Date()): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt, {\n failureReason: reason,\n failureTime: endedAt,\n });\n job.endedAt = endedAt;\n job.statusReason = reason;\n}\n\nexport function updateStatisticsForCancellation(\n job: PrintJob,\n reason: string = \"Print cancelled by user\",\n endedAt: Date = new Date(),\n): void {\n job.statistics = initializeOrUpdateStatistics(job, endedAt);\n job.endedAt = endedAt;\n job.statusReason = reason;\n}\n"],"mappings":";AAEA,SAAgB,qBAAqB,WAAwB,0BAAgB,IAAI,MAAM,EAAiB;CACtG,IAAI,CAAC,WACH,OAAO;CAET,QAAQ,QAAQ,SAAS,GAAG,UAAU,SAAS,IAAI;;AASrD,SAAgB,6BACd,KACA,0BAAgB,IAAI,MAAM,EAC1B,UAAmC,EAAE,EACpB;CACjB,MAAM,yBAAyB,qBAAqB,IAAI,WAAW,QAAQ;CAE3E,IAAI,CAAC,IAAI,YACP,OAAO;EACL,WAAW,IAAI;EACf;EACA;EACA,UAAU,QAAQ,YAAY,IAAI,YAAY;EAC9C,eAAe,QAAQ;EACvB,aAAa,QAAQ;EACtB;MACI;EACL,IAAI,WAAW,UAAU;EACzB,IAAI,WAAW,yBAAyB;EAExC,IAAI,QAAQ,aAAa,KAAA,GACvB,IAAI,WAAW,WAAW,QAAQ;EAGpC,IAAI,QAAQ,kBAAkB,KAAA,GAAW;GACvC,IAAI,WAAW,gBAAgB,QAAQ;GACvC,IAAI,WAAW,cAAc,QAAQ,eAAe;;EAGtD,OAAO,IAAI;;;AAIf,SAAgB,8BAA8B,KAAe,0BAAgB,IAAI,MAAM,EAAQ;CAC7F,IAAI,aAAa,6BAA6B,KAAK,SAAS,EAAE,UAAU,KAAK,CAAC;CAC9E,IAAI,UAAU;CACd,IAAI,WAAW;;AAGjB,SAAgB,2BAA2B,KAAe,QAAgB,0BAAgB,IAAI,MAAM,EAAQ;CAC1G,IAAI,aAAa,6BAA6B,KAAK,SAAS;EAC1D,eAAe;EACf,aAAa;EACd,CAAC;CACF,IAAI,UAAU;CACd,IAAI,eAAe;;AAGrB,SAAgB,gCACd,KACA,SAAiB,2BACjB,0BAAgB,IAAI,MAAM,EACpB;CACN,IAAI,aAAa,6BAA6B,KAAK,QAAQ;CAC3D,IAAI,UAAU;CACd,IAAI,eAAe"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-url.js","names":[],"sources":["../../src/utils/normalize-url.ts"],"sourcesContent":["// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs\nimport { defaultHttpProtocol } from \"@/utils/url.utils\";\n\nconst DATA_URL_DEFAULT_MIME_TYPE = \"text/plain\";\nconst DATA_URL_DEFAULT_CHARSET = \"us-ascii\";\n\nconst testParameter = (name: string, filters: (RegExp | any)[]) =>\n filters.some((filter) => (filter instanceof RegExp ? filter.test(name) : filter === name));\n\nconst supportedProtocols = new Set([\"https:\", \"http:\", \"file:\"]);\n\nconst hasCustomProtocol = (urlString: string) => {\n try {\n const { protocol } = new URL(urlString);\n\n return protocol.endsWith(\":\") && !protocol.includes(\".\") && !supportedProtocols.has(protocol);\n } catch {\n return false;\n }\n};\n\nconst normalizeDataURL = (urlString: string, { stripHash }: { stripHash?: boolean }) => {\n const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);\n\n if (!match?.groups) {\n throw new Error(`Invalid URL: ${urlString}`);\n }\n\n let { type, data, hash } = match.groups;\n const mediaType = type.split(\";\");\n hash = stripHash ? \"\" : hash;\n\n let isBase64 = false;\n if (mediaType[mediaType.length - 1] === \"base64\") {\n mediaType.pop();\n isBase64 = true;\n }\n\n // Lowercase MIME type\n const mimeType = mediaType.shift()?.toLowerCase() ?? \"\";\n const attributes = mediaType\n .map((attribute) => {\n let [key, value = \"\"] = attribute.split(\"=\").map((string) => string.trim());\n\n // Lowercase `charset`\n if (key === \"charset\") {\n value = value.toLowerCase();\n\n if (value === DATA_URL_DEFAULT_CHARSET) {\n return \"\";\n }\n }\n\n return `${key}${value ? `=${value}` : \"\"}`;\n })\n .filter(Boolean);\n\n const normalizedMediaType = [...attributes];\n\n if (isBase64) {\n normalizedMediaType.push(\"base64\");\n }\n\n if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {\n normalizedMediaType.unshift(mimeType);\n }\n\n return `data:${normalizedMediaType.join(\";\")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : \"\"}`;\n};\n\n/**\n * Modified with extra typing 23/02/2025\n * https://github.com/sindresorhus/normalize-url v8.0.1 patched at 12/01/2024\n * v8.0.0 downloaded at 13/08/2023\n * @param urlString\n * @param options\n * @return {*|string}\n */\nexport function normalizeUrl(\n urlString: string,\n options?: Partial<{\n defaultProtocol: \"http\" | \"https\" | \"ws\" | \"wss\" | string;\n normalizeProtocol: boolean;\n forceHttp: boolean;\n forceHttps: boolean;\n stripAuthentication: boolean;\n stripHash: boolean;\n stripTextFragment: boolean;\n stripProtocol: boolean;\n stripWWW: boolean;\n removeQueryParameters: boolean | RegExp[];\n keepQueryParameters: string[];\n removeTrailingSlash: boolean;\n removeSingleSlash: boolean;\n removeDirectoryIndex: boolean | RegExp[];\n removeExplicitPort: boolean;\n sortQueryParameters: boolean;\n }>,\n): string {\n options = {\n defaultProtocol: \"http\",\n normalizeProtocol: true,\n forceHttp: false,\n forceHttps: false,\n stripAuthentication: true,\n stripHash: false,\n stripTextFragment: true,\n stripWWW: true,\n removeQueryParameters: [/^utm_\\w+/i],\n removeTrailingSlash: true,\n removeSingleSlash: true,\n removeDirectoryIndex: false,\n removeExplicitPort: false,\n sortQueryParameters: true,\n ...options,\n };\n\n // Legacy: Append `:` to the protocol if missing.\n if (typeof options.defaultProtocol === \"string\" && !options.defaultProtocol.endsWith(\":\")) {\n options.defaultProtocol = `${options.defaultProtocol}:`;\n }\n\n urlString = urlString.trim();\n\n // Data URL\n if (/^data:/i.test(urlString)) {\n return normalizeDataURL(urlString, { stripHash: options.stripHash });\n }\n\n if (hasCustomProtocol(urlString)) {\n return urlString;\n }\n\n const hasRelativeProtocol = urlString.startsWith(\"//\");\n const isRelativeUrl = !hasRelativeProtocol && /^\\.*\\//.test(urlString);\n\n // Prepend protocol\n if (!isRelativeUrl) {\n urlString = urlString.replace(/^(?!(?:\\w+:)?\\/\\/)|^\\/\\//, options.defaultProtocol ?? defaultHttpProtocol);\n }\n\n const urlObject = new URL(urlString);\n\n if (options.forceHttp && options.forceHttps) {\n throw new Error(\"The `forceHttp` and `forceHttps` options cannot be used together\");\n }\n\n if (options.forceHttp && urlObject.protocol === \"https:\") {\n urlObject.protocol = \"http:\";\n }\n\n if (options.forceHttps && urlObject.protocol === \"http:\") {\n urlObject.protocol = \"https:\";\n }\n\n // Remove auth\n if (options.stripAuthentication) {\n urlObject.username = \"\";\n urlObject.password = \"\";\n }\n\n // Remove hash\n if (options.stripHash) {\n urlObject.hash = \"\";\n } else if (options.stripTextFragment) {\n urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, \"\");\n }\n\n // Remove duplicate slashes if not preceded by a protocol\n // NOTE: This could be implemented using a single negative lookbehind\n // regex, but we avoid that to maintain compatibility with older js engines\n // which do not have support for that feature.\n if (urlObject.pathname) {\n // TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\\b[a-z][a-z\\d+\\-.]{1,50}:)\\/{2,}/g, '/');` when Safari supports negative lookbehind.\n\n // Split the string by occurrences of this protocol regex, and perform\n // duplicate-slash replacement on the strings between those occurrences\n // (if any).\n const protocolRegex = /\\b[a-z][a-z\\d+\\-.]{1,50}:\\/\\//g;\n\n let lastIndex = 0;\n let result = \"\";\n for (;;) {\n const match = protocolRegex.exec(urlObject.pathname);\n if (!match) {\n break;\n }\n\n const protocol = match[0];\n const protocolAtIndex = match.index;\n const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);\n\n result += intermediate.replace(/\\/{2,}/g, \"/\");\n result += protocol;\n lastIndex = protocolAtIndex + protocol.length;\n }\n\n const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);\n result += remnant.replace(/\\/{2,}/g, \"/\");\n\n urlObject.pathname = result;\n }\n\n // Decode URI octets\n if (urlObject.pathname) {\n try {\n urlObject.pathname = decodeURI(urlObject.pathname);\n } catch {}\n }\n\n // Remove directory index\n if (options.removeDirectoryIndex === true) {\n options.removeDirectoryIndex = [/^index\\.[a-z]+$/];\n }\n\n if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {\n let pathComponents = urlObject.pathname.split(\"/\");\n const lastComponent = pathComponents[pathComponents.length - 1];\n\n if (testParameter(lastComponent, options.removeDirectoryIndex)) {\n pathComponents = pathComponents.slice(0, -1);\n urlObject.pathname = pathComponents.slice(1).join(\"/\") + \"/\";\n }\n }\n\n if (urlObject.hostname) {\n // Remove trailing dot\n urlObject.hostname = urlObject.hostname.replace(/\\.$/, \"\");\n\n // Remove `www.`\n if (options.stripWWW && /^www\\.(?!www\\.)[a-z\\-\\d]{1,63}\\.[a-z.\\-\\d]{2,63}$/.test(urlObject.hostname)) {\n // Each label should be max 63 at length (min: 1).\n // Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names\n // Each TLD should be up to 63 characters long (min: 2).\n // It is technically possible to have a single character TLD, but none currently exist.\n urlObject.hostname = urlObject.hostname.replace(/^www\\./, \"\");\n }\n }\n\n // Remove query unwanted parameters\n if (Array.isArray(options.removeQueryParameters)) {\n // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.\n for (const key of [...urlObject.searchParams.keys()]) {\n if (testParameter(key, options.removeQueryParameters)) {\n urlObject.searchParams.delete(key);\n }\n }\n }\n\n if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {\n urlObject.search = \"\";\n }\n\n // Keep wanted query parameters\n if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {\n // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.\n for (const key of [...urlObject.searchParams.keys()]) {\n if (!testParameter(key, options.keepQueryParameters)) {\n urlObject.searchParams.delete(key);\n }\n }\n }\n\n // Sort query parameters\n if (options.sortQueryParameters) {\n urlObject.searchParams.sort();\n\n // Calling `.sort()` encodes the search parameters, so we need to decode them again.\n try {\n urlObject.search = decodeURIComponent(urlObject.search);\n } catch {}\n }\n\n if (options.removeTrailingSlash) {\n urlObject.pathname = urlObject.pathname.replace(/\\/$/, \"\");\n }\n\n // Remove an explicit port number, excluding a default port number, if applicable\n if (options.removeExplicitPort && urlObject.port) {\n urlObject.port = \"\";\n }\n\n const oldUrlString = urlString;\n\n // Take advantage of many of the Node `url` normalizations\n urlString = urlObject.toString();\n\n if (\n !options.removeSingleSlash &&\n urlObject.pathname === \"/\" &&\n !oldUrlString.endsWith(\"/\") &&\n urlObject.hash === \"\"\n ) {\n urlString = urlString.replace(/\\/$/, \"\");\n }\n\n // Remove ending `/` unless removeSingleSlash is false\n if (\n (options.removeTrailingSlash || urlObject.pathname === \"/\") &&\n urlObject.hash === \"\" &&\n options.removeSingleSlash\n ) {\n urlString = urlString.replace(/\\/$/, \"\");\n }\n\n // Restore relative protocol, if applicable\n if (hasRelativeProtocol && !options.normalizeProtocol) {\n urlString = urlString.replace(/^http:\\/\\//, \"//\");\n }\n\n // Remove http/https\n if (options.stripProtocol) {\n urlString = urlString.replace(/^(?:https?:)?\\/\\//, \"\");\n }\n\n return urlString;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"normalize-url.js","names":[],"sources":["../../src/utils/normalize-url.ts"],"sourcesContent":["// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs\nimport { defaultHttpProtocol } from \"@/utils/url.utils\";\n\nconst DATA_URL_DEFAULT_MIME_TYPE = \"text/plain\";\nconst DATA_URL_DEFAULT_CHARSET = \"us-ascii\";\n\nconst testParameter = (name: string, filters: (RegExp | any)[]) =>\n filters.some((filter) => (filter instanceof RegExp ? filter.test(name) : filter === name));\n\nconst supportedProtocols = new Set([\"https:\", \"http:\", \"file:\"]);\n\nconst hasCustomProtocol = (urlString: string) => {\n try {\n const { protocol } = new URL(urlString);\n\n return protocol.endsWith(\":\") && !protocol.includes(\".\") && !supportedProtocols.has(protocol);\n } catch {\n return false;\n }\n};\n\nconst normalizeDataURL = (urlString: string, { stripHash }: { stripHash?: boolean }) => {\n const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);\n\n if (!match?.groups) {\n throw new Error(`Invalid URL: ${urlString}`);\n }\n\n let { type, data, hash } = match.groups;\n const mediaType = type.split(\";\");\n hash = stripHash ? \"\" : hash;\n\n let isBase64 = false;\n if (mediaType[mediaType.length - 1] === \"base64\") {\n mediaType.pop();\n isBase64 = true;\n }\n\n // Lowercase MIME type\n const mimeType = mediaType.shift()?.toLowerCase() ?? \"\";\n const attributes = mediaType\n .map((attribute) => {\n let [key, value = \"\"] = attribute.split(\"=\").map((string) => string.trim());\n\n // Lowercase `charset`\n if (key === \"charset\") {\n value = value.toLowerCase();\n\n if (value === DATA_URL_DEFAULT_CHARSET) {\n return \"\";\n }\n }\n\n return `${key}${value ? `=${value}` : \"\"}`;\n })\n .filter(Boolean);\n\n const normalizedMediaType = [...attributes];\n\n if (isBase64) {\n normalizedMediaType.push(\"base64\");\n }\n\n if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {\n normalizedMediaType.unshift(mimeType);\n }\n\n return `data:${normalizedMediaType.join(\";\")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : \"\"}`;\n};\n\n/**\n * Modified with extra typing 23/02/2025\n * https://github.com/sindresorhus/normalize-url v8.0.1 patched at 12/01/2024\n * v8.0.0 downloaded at 13/08/2023\n * @param urlString\n * @param options\n * @return {*|string}\n */\nexport function normalizeUrl(\n urlString: string,\n options?: Partial<{\n defaultProtocol: \"http\" | \"https\" | \"ws\" | \"wss\" | string;\n normalizeProtocol: boolean;\n forceHttp: boolean;\n forceHttps: boolean;\n stripAuthentication: boolean;\n stripHash: boolean;\n stripTextFragment: boolean;\n stripProtocol: boolean;\n stripWWW: boolean;\n removeQueryParameters: boolean | RegExp[];\n keepQueryParameters: string[];\n removeTrailingSlash: boolean;\n removeSingleSlash: boolean;\n removeDirectoryIndex: boolean | RegExp[];\n removeExplicitPort: boolean;\n sortQueryParameters: boolean;\n }>,\n): string {\n options = {\n defaultProtocol: \"http\",\n normalizeProtocol: true,\n forceHttp: false,\n forceHttps: false,\n stripAuthentication: true,\n stripHash: false,\n stripTextFragment: true,\n stripWWW: true,\n removeQueryParameters: [/^utm_\\w+/i],\n removeTrailingSlash: true,\n removeSingleSlash: true,\n removeDirectoryIndex: false,\n removeExplicitPort: false,\n sortQueryParameters: true,\n ...options,\n };\n\n // Legacy: Append `:` to the protocol if missing.\n if (typeof options.defaultProtocol === \"string\" && !options.defaultProtocol.endsWith(\":\")) {\n options.defaultProtocol = `${options.defaultProtocol}:`;\n }\n\n urlString = urlString.trim();\n\n // Data URL\n if (/^data:/i.test(urlString)) {\n return normalizeDataURL(urlString, { stripHash: options.stripHash });\n }\n\n if (hasCustomProtocol(urlString)) {\n return urlString;\n }\n\n const hasRelativeProtocol = urlString.startsWith(\"//\");\n const isRelativeUrl = !hasRelativeProtocol && /^\\.*\\//.test(urlString);\n\n // Prepend protocol\n if (!isRelativeUrl) {\n urlString = urlString.replace(/^(?!(?:\\w+:)?\\/\\/)|^\\/\\//, options.defaultProtocol ?? defaultHttpProtocol);\n }\n\n const urlObject = new URL(urlString);\n\n if (options.forceHttp && options.forceHttps) {\n throw new Error(\"The `forceHttp` and `forceHttps` options cannot be used together\");\n }\n\n if (options.forceHttp && urlObject.protocol === \"https:\") {\n urlObject.protocol = \"http:\";\n }\n\n if (options.forceHttps && urlObject.protocol === \"http:\") {\n urlObject.protocol = \"https:\";\n }\n\n // Remove auth\n if (options.stripAuthentication) {\n urlObject.username = \"\";\n urlObject.password = \"\";\n }\n\n // Remove hash\n if (options.stripHash) {\n urlObject.hash = \"\";\n } else if (options.stripTextFragment) {\n urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, \"\");\n }\n\n // Remove duplicate slashes if not preceded by a protocol\n // NOTE: This could be implemented using a single negative lookbehind\n // regex, but we avoid that to maintain compatibility with older js engines\n // which do not have support for that feature.\n if (urlObject.pathname) {\n // TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\\b[a-z][a-z\\d+\\-.]{1,50}:)\\/{2,}/g, '/');` when Safari supports negative lookbehind.\n\n // Split the string by occurrences of this protocol regex, and perform\n // duplicate-slash replacement on the strings between those occurrences\n // (if any).\n const protocolRegex = /\\b[a-z][a-z\\d+\\-.]{1,50}:\\/\\//g;\n\n let lastIndex = 0;\n let result = \"\";\n for (;;) {\n const match = protocolRegex.exec(urlObject.pathname);\n if (!match) {\n break;\n }\n\n const protocol = match[0];\n const protocolAtIndex = match.index;\n const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);\n\n result += intermediate.replace(/\\/{2,}/g, \"/\");\n result += protocol;\n lastIndex = protocolAtIndex + protocol.length;\n }\n\n const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);\n result += remnant.replace(/\\/{2,}/g, \"/\");\n\n urlObject.pathname = result;\n }\n\n // Decode URI octets\n if (urlObject.pathname) {\n try {\n urlObject.pathname = decodeURI(urlObject.pathname);\n } catch {}\n }\n\n // Remove directory index\n if (options.removeDirectoryIndex === true) {\n options.removeDirectoryIndex = [/^index\\.[a-z]+$/];\n }\n\n if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {\n let pathComponents = urlObject.pathname.split(\"/\");\n const lastComponent = pathComponents[pathComponents.length - 1];\n\n if (testParameter(lastComponent, options.removeDirectoryIndex)) {\n pathComponents = pathComponents.slice(0, -1);\n urlObject.pathname = pathComponents.slice(1).join(\"/\") + \"/\";\n }\n }\n\n if (urlObject.hostname) {\n // Remove trailing dot\n urlObject.hostname = urlObject.hostname.replace(/\\.$/, \"\");\n\n // Remove `www.`\n if (options.stripWWW && /^www\\.(?!www\\.)[a-z\\-\\d]{1,63}\\.[a-z.\\-\\d]{2,63}$/.test(urlObject.hostname)) {\n // Each label should be max 63 at length (min: 1).\n // Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names\n // Each TLD should be up to 63 characters long (min: 2).\n // It is technically possible to have a single character TLD, but none currently exist.\n urlObject.hostname = urlObject.hostname.replace(/^www\\./, \"\");\n }\n }\n\n // Remove query unwanted parameters\n if (Array.isArray(options.removeQueryParameters)) {\n // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.\n for (const key of [...urlObject.searchParams.keys()]) {\n if (testParameter(key, options.removeQueryParameters)) {\n urlObject.searchParams.delete(key);\n }\n }\n }\n\n if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {\n urlObject.search = \"\";\n }\n\n // Keep wanted query parameters\n if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {\n // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.\n for (const key of [...urlObject.searchParams.keys()]) {\n if (!testParameter(key, options.keepQueryParameters)) {\n urlObject.searchParams.delete(key);\n }\n }\n }\n\n // Sort query parameters\n if (options.sortQueryParameters) {\n urlObject.searchParams.sort();\n\n // Calling `.sort()` encodes the search parameters, so we need to decode them again.\n try {\n urlObject.search = decodeURIComponent(urlObject.search);\n } catch {}\n }\n\n if (options.removeTrailingSlash) {\n urlObject.pathname = urlObject.pathname.replace(/\\/$/, \"\");\n }\n\n // Remove an explicit port number, excluding a default port number, if applicable\n if (options.removeExplicitPort && urlObject.port) {\n urlObject.port = \"\";\n }\n\n const oldUrlString = urlString;\n\n // Take advantage of many of the Node `url` normalizations\n urlString = urlObject.toString();\n\n if (\n !options.removeSingleSlash &&\n urlObject.pathname === \"/\" &&\n !oldUrlString.endsWith(\"/\") &&\n urlObject.hash === \"\"\n ) {\n urlString = urlString.replace(/\\/$/, \"\");\n }\n\n // Remove ending `/` unless removeSingleSlash is false\n if (\n (options.removeTrailingSlash || urlObject.pathname === \"/\") &&\n urlObject.hash === \"\" &&\n options.removeSingleSlash\n ) {\n urlString = urlString.replace(/\\/$/, \"\");\n }\n\n // Restore relative protocol, if applicable\n if (hasRelativeProtocol && !options.normalizeProtocol) {\n urlString = urlString.replace(/^http:\\/\\//, \"//\");\n }\n\n // Remove http/https\n if (options.stripProtocol) {\n urlString = urlString.replace(/^(?:https?:)?\\/\\//, \"\");\n }\n\n return urlString;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"3mf.parser.js","names":["fs","path"],"sources":["../../../src/utils/parsers/3mf.parser.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport AdmZip from \"adm-zip\";\nimport { ThreeMFMetadata } from \"@/entities/print-job.entity\";\nimport { ParsedThumbnail } from \"./parser.types\";\nimport { getImageDimensions } from \"../image-dimensions\";\n\ninterface ThreeMFParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n plates?: ThreeMFMetadata[\"plates\"];\n };\n normalized: ThreeMFMetadata;\n plates?: ThreeMFMetadata[\"plates\"];\n}\n\n/**\n * 3MF parser for extracting metadata from .3mf files\n * Supports both single and multi-plate 3MF files (Bambu Lab format)\n */\nexport class ThreeMFParser {\n async parse(filePath: string): Promise<ThreeMFParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = path.basename(filePath);\n\n const zip = new AdmZip(filePath);\n const zipEntries = zip.getEntries();\n\n // Check for metadata.json first (some slicers may use this)\n const metadataJsonEntry = zipEntries.find((e) => e.entryName === \"metadata.json\");\n let metadata: Record<string, any> = {};\n\n if (metadataJsonEntry) {\n // Parse JSON metadata\n const jsonContent = metadataJsonEntry.getData().toString(\"utf8\");\n const jsonData = JSON.parse(jsonContent);\n metadata = this.normalizeJsonMetadata(jsonData);\n } else {\n // Extract metadata from 3D model XML\n const modelEntry = zipEntries.find(\n (e) => e.entryName === \"3D/3dmodel.model\" || e.entryName === \"Metadata/model_settings.config\",\n );\n metadata = modelEntry ? this.extractMetadataFromXML(modelEntry.getData().toString(\"utf8\")) : {};\n }\n\n // Check for multi-plate structure (Bambu Lab)\n const plates = this.extractPlates(zipEntries);\n const isMultiPlate = plates.length > 1;\n\n // Extract thumbnails\n const thumbnails = this.extractThumbnails(zipEntries);\n\n // For single-plate files, use plate data for top-level metadata\n let topLevelPrintTime = this.parseTime(metadata.printTime);\n let topLevelFilamentWeight = this.parseFloat(metadata.totalFilamentWeight || metadata.filamentWeight);\n let topLevelLayers = this.parseInt(metadata.layerCount);\n let topLevelFilamentUsedMm = this.parseFloat(metadata.filamentUsed);\n let topLevelFilamentUsedCm3 = this.parseFloat(metadata.filamentVolume);\n let topLevelFilamentDensity = this.parseFloat(metadata.filamentDensity);\n let topLevelMaxZ = this.parseFloat(metadata.maxZ);\n let topLevelSlicerVersion = metadata.slicerVersion || metadata.generator;\n\n // Additional fields from plates\n let topLevelNozzleDiameter = this.parseFloat(metadata.nozzleDiameter);\n let topLevelLayerHeight = this.parseFloat(metadata.layerHeight || metadata.layer_height);\n let topLevelFirstLayerHeight = this.parseFloat(metadata.firstLayerHeight || metadata.first_layer_height);\n let topLevelBedTemp = this.parseFloat(metadata.bedTemp || metadata.bed_temperature);\n let topLevelNozzleTemp = this.parseFloat(metadata.nozzleTemp || metadata.nozzle_temperature);\n let topLevelFillDensity = metadata.infillDensity || metadata.infill_density || metadata.fill_density || null;\n let topLevelFilamentType = metadata.filamentType || metadata.filament_type || null;\n let topLevelPrinterModel = metadata.printerModel || metadata.printer_model || null;\n let topLevelFilamentDiameter = this.parseFloat(metadata.filamentDiameter || metadata.filament_diameter) || 1.75;\n\n if (plates.length >= 1 && plates[0]) {\n if (plates.length === 1) {\n // Single plate - promote ALL plate data to top level\n const plate = plates[0] as any;\n topLevelPrintTime = plate.gcodePrintTimeSeconds ?? topLevelPrintTime;\n topLevelFilamentWeight = plate.filamentUsedGrams ?? topLevelFilamentWeight;\n topLevelLayers = plate.totalLayers ?? topLevelLayers;\n topLevelFilamentUsedMm = plate.filamentUsedMm ?? topLevelFilamentUsedMm;\n topLevelFilamentUsedCm3 = plate.filamentUsedCm3 ?? topLevelFilamentUsedCm3;\n topLevelFilamentDensity = plate.filamentDensityGramsCm3 ?? topLevelFilamentDensity;\n topLevelMaxZ = plate.maxLayerZ ?? topLevelMaxZ;\n topLevelSlicerVersion = plate.slicerVersion ?? topLevelSlicerVersion;\n topLevelNozzleDiameter = plate.nozzleDiameterMm ?? topLevelNozzleDiameter;\n topLevelLayerHeight = plate.layerHeight ?? topLevelLayerHeight;\n topLevelFirstLayerHeight = plate.firstLayerHeight ?? topLevelFirstLayerHeight;\n topLevelBedTemp = plate.bedTemperature ?? topLevelBedTemp;\n topLevelNozzleTemp = plate.nozzleTemperature ?? topLevelNozzleTemp;\n topLevelFillDensity = plate.fillDensity ?? topLevelFillDensity;\n topLevelFilamentType = plate.filamentType ?? topLevelFilamentType;\n topLevelPrinterModel = plate.printerModel ?? topLevelPrinterModel;\n topLevelFilamentDiameter = plate.filamentDiameterMm ?? topLevelFilamentDiameter;\n } else {\n // Multi-plate - aggregate data from all plates\n topLevelPrintTime = plates.reduce((sum, p: any) => sum + (p.gcodePrintTimeSeconds || 0), 0);\n topLevelFilamentWeight = plates.reduce((sum, p: any) => sum + (p.filamentUsedGrams || 0), 0);\n topLevelFilamentUsedMm = plates.reduce((sum, p: any) => sum + (p.filamentUsedMm || 0), 0);\n topLevelFilamentUsedCm3 = plates.reduce((sum, p: any) => sum + (p.filamentUsedCm3 || 0), 0);\n topLevelLayers = Math.max(...plates.map((p: any) => p.totalLayers || 0));\n topLevelMaxZ = Math.max(...plates.map((p: any) => p.maxLayerZ || 0));\n\n // Use first plate for shared settings (all plates have same printer/material settings)\n const firstPlate = plates[0] as any;\n topLevelFilamentDensity = firstPlate.filamentDensityGramsCm3 ?? topLevelFilamentDensity;\n topLevelSlicerVersion = firstPlate.slicerVersion ?? topLevelSlicerVersion;\n topLevelNozzleDiameter = firstPlate.nozzleDiameterMm ?? topLevelNozzleDiameter;\n topLevelLayerHeight = firstPlate.layerHeight ?? topLevelLayerHeight;\n topLevelFirstLayerHeight = firstPlate.firstLayerHeight ?? topLevelFirstLayerHeight;\n topLevelBedTemp = firstPlate.bedTemperature ?? topLevelBedTemp;\n topLevelNozzleTemp = firstPlate.nozzleTemperature ?? topLevelNozzleTemp;\n topLevelFillDensity = firstPlate.fillDensity ?? topLevelFillDensity;\n topLevelFilamentType = firstPlate.filamentType ?? topLevelFilamentType;\n topLevelPrinterModel = firstPlate.printerModel ?? topLevelPrinterModel;\n topLevelFilamentDiameter = firstPlate.filamentDiameterMm ?? topLevelFilamentDiameter;\n }\n }\n\n const normalized: ThreeMFMetadata = {\n fileName,\n fileFormat: \"3mf\",\n fileSize: stats.size,\n isMultiPlate,\n totalPlates: plates.length || 1,\n gcodePrintTimeSeconds: topLevelPrintTime,\n nozzleDiameterMm: topLevelNozzleDiameter,\n filamentDiameterMm: topLevelFilamentDiameter,\n filamentDensityGramsCm3: topLevelFilamentDensity,\n filamentUsedMm: topLevelFilamentUsedMm,\n filamentUsedCm3: topLevelFilamentUsedCm3,\n filamentUsedGrams: topLevelFilamentWeight,\n totalFilamentUsedGrams: topLevelFilamentWeight,\n layerHeight: topLevelLayerHeight,\n firstLayerHeight: topLevelFirstLayerHeight,\n bedTemperature: topLevelBedTemp,\n nozzleTemperature: topLevelNozzleTemp,\n fillDensity: topLevelFillDensity,\n filamentType: topLevelFilamentType,\n printerModel: topLevelPrinterModel,\n slicerVersion: topLevelSlicerVersion,\n maxLayerZ: topLevelMaxZ,\n totalLayers: topLevelLayers,\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n plates: plates.length > 0 ? plates : undefined,\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n plates: plates.length > 0 ? plates : undefined,\n },\n normalized,\n plates: plates.length > 0 ? plates : undefined,\n };\n }\n\n private normalizeJsonMetadata(jsonData: any): Record<string, any> {\n // Normalize JSON metadata keys to match expected format\n const metadata: Record<string, any> = {};\n\n // Map common JSON metadata keys to our internal format\n if (jsonData.nozzleDiameter !== undefined) metadata.nozzleDiameter = String(jsonData.nozzleDiameter);\n if (jsonData.estimatedPrintTimeSec !== undefined) metadata.printTime = String(jsonData.estimatedPrintTimeSec);\n if (jsonData.filamentDiameter !== undefined) metadata.filamentDiameter = String(jsonData.filamentDiameter);\n if (jsonData.filamentDensity !== undefined) metadata.filamentDensity = String(jsonData.filamentDensity);\n if (jsonData.filamentUsedGrams !== undefined) metadata.filamentWeight = String(jsonData.filamentUsedGrams);\n if (jsonData.layerHeight !== undefined) metadata.layerHeight = String(jsonData.layerHeight);\n if (jsonData.firstLayerHeight !== undefined) metadata.firstLayerHeight = String(jsonData.firstLayerHeight);\n if (jsonData.bedTemp !== undefined) metadata.bedTemp = String(jsonData.bedTemp);\n if (jsonData.nozzleTemp !== undefined) metadata.nozzleTemp = String(jsonData.nozzleTemp);\n if (jsonData.fillDensity !== undefined) metadata.infillDensity = String(jsonData.fillDensity);\n if (jsonData.filamentType !== undefined) metadata.filamentType = jsonData.filamentType;\n if (jsonData.printerModel !== undefined) metadata.printerModel = jsonData.printerModel;\n\n return metadata;\n }\n\n private extractMetadataFromXML(xml: string): Record<string, string> {\n const metadata: Record<string, string> = {};\n\n // Extract simple key-value pairs from XML\n const patterns = [\n /<printtime>([^<]+)<\\/printtime>/i,\n /<layerheight>([^<]+)<\\/layerheight>/i,\n /<filamentused>([^<]+)<\\/filamentused>/i,\n /<filamenttype>([^<]+)<\\/filamenttype>/i,\n /<nozzlediameter>([^<]+)<\\/nozzlediameter>/i,\n /<bedtemperature>([^<]+)<\\/bedtemperature>/i,\n /<nozzletemperature>([^<]+)<\\/nozzletemperature>/i,\n ];\n\n for (const pattern of patterns) {\n const match = xml.match(pattern);\n if (match) {\n const key = pattern.source.match(/<([^>]+)>/)?.[1] || \"\";\n metadata[key] = match[1];\n }\n }\n\n // Extract generator/slicer info\n const generatorMatch = xml.match(/generator=\"([^\"]+)\"/);\n if (generatorMatch) {\n metadata.generator = generatorMatch[1];\n metadata.slicerVersion = generatorMatch[1];\n }\n\n return metadata;\n }\n\n private extractPlates(zipEntries: AdmZip.IZipEntry[]): NonNullable<ThreeMFMetadata[\"plates\"]> {\n const plates: NonNullable<ThreeMFMetadata[\"plates\"]> = [];\n\n // Look for Bambu Lab plate structure (but not .gcode.md5 files)\n const plateEntries = zipEntries.filter(\n (e) => e.entryName.match(/Metadata\\/plate_\\d+\\.gcode$/) && !e.entryName.endsWith(\".md5\"),\n );\n\n if (plateEntries.length === 0) {\n // Single plate file or non-Bambu format\n return [];\n }\n\n for (const entry of plateEntries) {\n const plateMatch = entry.entryName.match(/plate_(\\d+)\\.gcode/);\n if (!plateMatch) continue;\n\n // Bambu uses 1-indexed plate numbers in filenames (plate_1.gcode = plate 1)\n const plateNumber = Number.parseInt(plateMatch[1]);\n // Read more bytes to include CONFIG_BLOCK (contains layer_height, temps, etc.)\n const gcodeContent = entry.getData().toString(\"utf8\", 0, Math.min(50000, entry.getData().length));\n\n // Parse basic metadata from G-code header\n const metadata = this.parseGCodeHeader(gcodeContent);\n\n // Find thumbnails for this plate\n const plateThumbs = zipEntries.filter(\n (e) =>\n e.entryName.includes(`plate_${plateMatch[1]}`) &&\n (e.entryName.endsWith(\".png\") || e.entryName.endsWith(\".jpg\")),\n );\n\n const plateThumbnails: ParsedThumbnail[] = plateThumbs.map((t) => {\n const format = t.entryName.endsWith(\".png\") ? \"PNG\" : \"JPG\";\n const imageData = t.getData();\n\n const sizeMatch = t.entryName.match(/(\\d+)x(\\d+)/);\n let width = sizeMatch ? Number.parseInt(sizeMatch[1]) : 0;\n let height = sizeMatch ? Number.parseInt(sizeMatch[2]) : 0;\n\n if (width === 0 || height === 0) {\n const dimensions = getImageDimensions(imageData, format);\n width = dimensions.width;\n height = dimensions.height;\n }\n\n return {\n width,\n height,\n format,\n data: imageData.toString(\"base64\"),\n };\n });\n const printTime = this.parseTime(\n metadata.model_printing_time || metadata.total_estimated_time || metadata.print_time,\n );\n const filamentWeight = this.parseFloat(\n metadata.total_filament_weight_g || metadata.filament_weight || metadata.total_filament_weight,\n );\n const layerCount = this.parseInt(metadata.total_layer_number || metadata.layer_count || metadata.total_layers);\n\n // Extract all available Bambu metadata from G-code header + config\n const plateMetadata = {\n plateNumber,\n gcodePrintTimeSeconds: printTime,\n filamentUsedGrams: filamentWeight,\n totalLayers: layerCount,\n filamentUsedMm: this.parseFloat(\n metadata.total_filament_length_mm || metadata.filament_length_mm || metadata.filament_used_mm,\n ),\n filamentUsedCm3: this.parseFloat(metadata.total_filament_volume_cm3 || metadata.filament_volume_cm3),\n filamentDensityGramsCm3: this.parseFloat(metadata.filament_density),\n filamentDiameterMm: this.parseFloat(metadata.filament_diameter) || 1.75,\n maxLayerZ: this.parseFloat(metadata.max_z_height || metadata.max_layer_z),\n slicerVersion: metadata.bambustudio || metadata.slicer_version || metadata.slicer || null,\n nozzleDiameterMm: this.parseFloat(metadata.nozzle_diameter),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(\n metadata.first_layer_height || metadata.initial_layer_height || metadata.initial_layer_print_height,\n ),\n bedTemperature: this.parseFloat(\n metadata.bed_temperature_actual ||\n metadata.bed_temperature ||\n metadata.bed_temp ||\n metadata.bed_temperature_initial_layer,\n ),\n nozzleTemperature: this.parseFloat(\n metadata.nozzle_temperature || metadata.nozzle_temp || metadata.nozzle_temperature_initial_layer,\n ),\n fillDensity: metadata.sparse_infill_density || metadata.infill_density || metadata.fill_density || null,\n filamentType: metadata.filament_type || null,\n printerModel: metadata.printer_model || null,\n objects: [],\n thumbnails:\n plateThumbnails.length > 0\n ? plateThumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n };\n\n plates.push(plateMetadata);\n }\n\n return plates.sort((a, b) => a.plateNumber - b.plateNumber);\n }\n\n private parseGCodeHeader(gcode: string): Record<string, string> {\n const metadata: Record<string, string> = {};\n const lines = gcode.split(\"\\n\").slice(0, 1000); // Extended to include CONFIG_BLOCK and early G-code\n\n for (const line of lines) {\n // Parse comment lines\n if (line.startsWith(\";\")) {\n // Skip block markers\n if (line.includes(\"_BLOCK_START\") || line.includes(\"_BLOCK_END\")) continue;\n\n // Special case: BambuStudio version (no key:value pattern)\n // ; BambuStudio 02.04.00.70\n const bambuMatch = line.match(/;\\s*BambuStudio\\s+([\\d.]+)/i);\n if (bambuMatch) {\n metadata.bambustudio = `BambuStudio ${bambuMatch[1]}`;\n continue;\n }\n\n // Match patterns like:\n // ; key: value\n // ; key = value\n // ; key : value\n const match = line.match(/;\\s*([^=:]+?)\\s*[:=]\\s*(.+)/);\n if (match) {\n let key = match[1].trim().toLowerCase().replace(/\\s+/g, \"_\");\n let value = match[2].trim();\n\n // Remove bracketed units from key: \"total_filament_weight_[g]\" -> \"total_filament_weight_g\"\n // Also normalize special chars: [cm^3] -> cm3\n key = key.replace(/\\[([^\\]]+)\\]/g, (_, unit) => unit.replace(/\\^/g, \"\"));\n\n // Split value on semicolon and take first part (handles \"11m 15s; total estimated time: 18m 15s\")\n value = value.split(\";\")[0].trim();\n\n // Remove remaining bracketed units and percentages from value\n value = value.replace(/\\s*\\[.*?\\]\\s*/g, \"\"); // Remove [units]\n\n // Store the value (don't overwrite if already set from header)\n if (!metadata[key]) {\n metadata[key] = value;\n }\n }\n } else {\n // Parse actual G-code commands for temperatures\n // M140 S65 - Set bed temperature\n // M190 S65 - Wait for bed temperature\n if (!metadata.bed_temperature_actual) {\n const bedTempMatch = line.match(/^M1(40|90)\\s+S(\\d+)/);\n if (bedTempMatch && Number.parseInt(bedTempMatch[2]) > 0) {\n metadata.bed_temperature_actual = bedTempMatch[2];\n }\n }\n }\n }\n\n return metadata;\n }\n\n private extractThumbnails(zipEntries: AdmZip.IZipEntry[]): ParsedThumbnail[] {\n const thumbnails: ParsedThumbnail[] = [];\n\n const thumbEntries = zipEntries.filter(\n (e) => e.entryName.match(/Metadata\\/.*\\.(png|jpg|jpeg)/i) || e.entryName.match(/Thumbnails\\/.*/i),\n );\n\n for (const entry of thumbEntries) {\n const format = entry.entryName.match(/\\.(png|jpg|jpeg)$/i)?.[1].toUpperCase() || \"PNG\";\n const imageData = entry.getData();\n\n const sizeMatch = entry.entryName.match(/(\\d+)x(\\d+)/);\n let width = sizeMatch ? Number.parseInt(sizeMatch[1]) : 0;\n let height = sizeMatch ? Number.parseInt(sizeMatch[2]) : 0;\n\n if (width === 0 || height === 0) {\n const dimensions = getImageDimensions(imageData, format);\n width = dimensions.width;\n height = dimensions.height;\n }\n\n const base64Data = imageData.toString(\"base64\");\n\n thumbnails.push({\n width,\n height,\n format,\n data: base64Data,\n });\n }\n\n return thumbnails;\n }\n\n private parseFloat(value: string | undefined): number | null {\n if (!value) return null;\n const num = Number.parseFloat(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseInt(value: string | undefined): number | null {\n if (!value) return null;\n const num = Number.parseInt(value, 10);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n // Try parsing as a duration string first (e.g., \"11m 15s\" or \"1h 30m\")\n const match = value.match(/(?:(\\d+)h)?(?:\\s*(\\d+)m)?(?:\\s*(\\d+)s)?/);\n if (match && (match[1] || match[2] || match[3])) {\n const hours = Number.parseInt(match[1] || \"0\");\n const minutes = Number.parseInt(match[2] || \"0\");\n const secs = Number.parseInt(match[3] || \"0\");\n return hours * 3600 + minutes * 60 + secs;\n }\n\n // Fallback to parsing as plain seconds\n const seconds = Number.parseFloat(value);\n if (!Number.isNaN(seconds)) return seconds;\n\n return null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"3mf.parser.js","names":["fs","path"],"sources":["../../../src/utils/parsers/3mf.parser.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport AdmZip from \"adm-zip\";\nimport { ThreeMFMetadata } from \"@/entities/print-job.entity\";\nimport { ParsedThumbnail } from \"./parser.types\";\nimport { getImageDimensions } from \"../image-dimensions\";\n\ninterface ThreeMFParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n plates?: ThreeMFMetadata[\"plates\"];\n };\n normalized: ThreeMFMetadata;\n plates?: ThreeMFMetadata[\"plates\"];\n}\n\n/**\n * 3MF parser for extracting metadata from .3mf files\n * Supports both single and multi-plate 3MF files (Bambu Lab format)\n */\nexport class ThreeMFParser {\n async parse(filePath: string): Promise<ThreeMFParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = path.basename(filePath);\n\n const zip = new AdmZip(filePath);\n const zipEntries = zip.getEntries();\n\n // Check for metadata.json first (some slicers may use this)\n const metadataJsonEntry = zipEntries.find((e) => e.entryName === \"metadata.json\");\n let metadata: Record<string, any> = {};\n\n if (metadataJsonEntry) {\n // Parse JSON metadata\n const jsonContent = metadataJsonEntry.getData().toString(\"utf8\");\n const jsonData = JSON.parse(jsonContent);\n metadata = this.normalizeJsonMetadata(jsonData);\n } else {\n // Extract metadata from 3D model XML\n const modelEntry = zipEntries.find(\n (e) => e.entryName === \"3D/3dmodel.model\" || e.entryName === \"Metadata/model_settings.config\",\n );\n metadata = modelEntry ? this.extractMetadataFromXML(modelEntry.getData().toString(\"utf8\")) : {};\n }\n\n // Check for multi-plate structure (Bambu Lab)\n const plates = this.extractPlates(zipEntries);\n const isMultiPlate = plates.length > 1;\n\n // Extract thumbnails\n const thumbnails = this.extractThumbnails(zipEntries);\n\n // For single-plate files, use plate data for top-level metadata\n let topLevelPrintTime = this.parseTime(metadata.printTime);\n let topLevelFilamentWeight = this.parseFloat(metadata.totalFilamentWeight || metadata.filamentWeight);\n let topLevelLayers = this.parseInt(metadata.layerCount);\n let topLevelFilamentUsedMm = this.parseFloat(metadata.filamentUsed);\n let topLevelFilamentUsedCm3 = this.parseFloat(metadata.filamentVolume);\n let topLevelFilamentDensity = this.parseFloat(metadata.filamentDensity);\n let topLevelMaxZ = this.parseFloat(metadata.maxZ);\n let topLevelSlicerVersion = metadata.slicerVersion || metadata.generator;\n\n // Additional fields from plates\n let topLevelNozzleDiameter = this.parseFloat(metadata.nozzleDiameter);\n let topLevelLayerHeight = this.parseFloat(metadata.layerHeight || metadata.layer_height);\n let topLevelFirstLayerHeight = this.parseFloat(metadata.firstLayerHeight || metadata.first_layer_height);\n let topLevelBedTemp = this.parseFloat(metadata.bedTemp || metadata.bed_temperature);\n let topLevelNozzleTemp = this.parseFloat(metadata.nozzleTemp || metadata.nozzle_temperature);\n let topLevelFillDensity = metadata.infillDensity || metadata.infill_density || metadata.fill_density || null;\n let topLevelFilamentType = metadata.filamentType || metadata.filament_type || null;\n let topLevelPrinterModel = metadata.printerModel || metadata.printer_model || null;\n let topLevelFilamentDiameter = this.parseFloat(metadata.filamentDiameter || metadata.filament_diameter) || 1.75;\n\n if (plates.length >= 1 && plates[0]) {\n if (plates.length === 1) {\n // Single plate - promote ALL plate data to top level\n const plate = plates[0] as any;\n topLevelPrintTime = plate.gcodePrintTimeSeconds ?? topLevelPrintTime;\n topLevelFilamentWeight = plate.filamentUsedGrams ?? topLevelFilamentWeight;\n topLevelLayers = plate.totalLayers ?? topLevelLayers;\n topLevelFilamentUsedMm = plate.filamentUsedMm ?? topLevelFilamentUsedMm;\n topLevelFilamentUsedCm3 = plate.filamentUsedCm3 ?? topLevelFilamentUsedCm3;\n topLevelFilamentDensity = plate.filamentDensityGramsCm3 ?? topLevelFilamentDensity;\n topLevelMaxZ = plate.maxLayerZ ?? topLevelMaxZ;\n topLevelSlicerVersion = plate.slicerVersion ?? topLevelSlicerVersion;\n topLevelNozzleDiameter = plate.nozzleDiameterMm ?? topLevelNozzleDiameter;\n topLevelLayerHeight = plate.layerHeight ?? topLevelLayerHeight;\n topLevelFirstLayerHeight = plate.firstLayerHeight ?? topLevelFirstLayerHeight;\n topLevelBedTemp = plate.bedTemperature ?? topLevelBedTemp;\n topLevelNozzleTemp = plate.nozzleTemperature ?? topLevelNozzleTemp;\n topLevelFillDensity = plate.fillDensity ?? topLevelFillDensity;\n topLevelFilamentType = plate.filamentType ?? topLevelFilamentType;\n topLevelPrinterModel = plate.printerModel ?? topLevelPrinterModel;\n topLevelFilamentDiameter = plate.filamentDiameterMm ?? topLevelFilamentDiameter;\n } else {\n // Multi-plate - aggregate data from all plates\n topLevelPrintTime = plates.reduce((sum, p: any) => sum + (p.gcodePrintTimeSeconds || 0), 0);\n topLevelFilamentWeight = plates.reduce((sum, p: any) => sum + (p.filamentUsedGrams || 0), 0);\n topLevelFilamentUsedMm = plates.reduce((sum, p: any) => sum + (p.filamentUsedMm || 0), 0);\n topLevelFilamentUsedCm3 = plates.reduce((sum, p: any) => sum + (p.filamentUsedCm3 || 0), 0);\n topLevelLayers = Math.max(...plates.map((p: any) => p.totalLayers || 0));\n topLevelMaxZ = Math.max(...plates.map((p: any) => p.maxLayerZ || 0));\n\n // Use first plate for shared settings (all plates have same printer/material settings)\n const firstPlate = plates[0] as any;\n topLevelFilamentDensity = firstPlate.filamentDensityGramsCm3 ?? topLevelFilamentDensity;\n topLevelSlicerVersion = firstPlate.slicerVersion ?? topLevelSlicerVersion;\n topLevelNozzleDiameter = firstPlate.nozzleDiameterMm ?? topLevelNozzleDiameter;\n topLevelLayerHeight = firstPlate.layerHeight ?? topLevelLayerHeight;\n topLevelFirstLayerHeight = firstPlate.firstLayerHeight ?? topLevelFirstLayerHeight;\n topLevelBedTemp = firstPlate.bedTemperature ?? topLevelBedTemp;\n topLevelNozzleTemp = firstPlate.nozzleTemperature ?? topLevelNozzleTemp;\n topLevelFillDensity = firstPlate.fillDensity ?? topLevelFillDensity;\n topLevelFilamentType = firstPlate.filamentType ?? topLevelFilamentType;\n topLevelPrinterModel = firstPlate.printerModel ?? topLevelPrinterModel;\n topLevelFilamentDiameter = firstPlate.filamentDiameterMm ?? topLevelFilamentDiameter;\n }\n }\n\n const normalized: ThreeMFMetadata = {\n fileName,\n fileFormat: \"3mf\",\n fileSize: stats.size,\n isMultiPlate,\n totalPlates: plates.length || 1,\n gcodePrintTimeSeconds: topLevelPrintTime,\n nozzleDiameterMm: topLevelNozzleDiameter,\n filamentDiameterMm: topLevelFilamentDiameter,\n filamentDensityGramsCm3: topLevelFilamentDensity,\n filamentUsedMm: topLevelFilamentUsedMm,\n filamentUsedCm3: topLevelFilamentUsedCm3,\n filamentUsedGrams: topLevelFilamentWeight,\n totalFilamentUsedGrams: topLevelFilamentWeight,\n layerHeight: topLevelLayerHeight,\n firstLayerHeight: topLevelFirstLayerHeight,\n bedTemperature: topLevelBedTemp,\n nozzleTemperature: topLevelNozzleTemp,\n fillDensity: topLevelFillDensity,\n filamentType: topLevelFilamentType,\n printerModel: topLevelPrinterModel,\n slicerVersion: topLevelSlicerVersion,\n maxLayerZ: topLevelMaxZ,\n totalLayers: topLevelLayers,\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n plates: plates.length > 0 ? plates : undefined,\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n plates: plates.length > 0 ? plates : undefined,\n },\n normalized,\n plates: plates.length > 0 ? plates : undefined,\n };\n }\n\n private normalizeJsonMetadata(jsonData: any): Record<string, any> {\n // Normalize JSON metadata keys to match expected format\n const metadata: Record<string, any> = {};\n\n // Map common JSON metadata keys to our internal format\n if (jsonData.nozzleDiameter !== undefined) metadata.nozzleDiameter = String(jsonData.nozzleDiameter);\n if (jsonData.estimatedPrintTimeSec !== undefined) metadata.printTime = String(jsonData.estimatedPrintTimeSec);\n if (jsonData.filamentDiameter !== undefined) metadata.filamentDiameter = String(jsonData.filamentDiameter);\n if (jsonData.filamentDensity !== undefined) metadata.filamentDensity = String(jsonData.filamentDensity);\n if (jsonData.filamentUsedGrams !== undefined) metadata.filamentWeight = String(jsonData.filamentUsedGrams);\n if (jsonData.layerHeight !== undefined) metadata.layerHeight = String(jsonData.layerHeight);\n if (jsonData.firstLayerHeight !== undefined) metadata.firstLayerHeight = String(jsonData.firstLayerHeight);\n if (jsonData.bedTemp !== undefined) metadata.bedTemp = String(jsonData.bedTemp);\n if (jsonData.nozzleTemp !== undefined) metadata.nozzleTemp = String(jsonData.nozzleTemp);\n if (jsonData.fillDensity !== undefined) metadata.infillDensity = String(jsonData.fillDensity);\n if (jsonData.filamentType !== undefined) metadata.filamentType = jsonData.filamentType;\n if (jsonData.printerModel !== undefined) metadata.printerModel = jsonData.printerModel;\n\n return metadata;\n }\n\n private extractMetadataFromXML(xml: string): Record<string, string> {\n const metadata: Record<string, string> = {};\n\n // Extract simple key-value pairs from XML\n const patterns = [\n /<printtime>([^<]+)<\\/printtime>/i,\n /<layerheight>([^<]+)<\\/layerheight>/i,\n /<filamentused>([^<]+)<\\/filamentused>/i,\n /<filamenttype>([^<]+)<\\/filamenttype>/i,\n /<nozzlediameter>([^<]+)<\\/nozzlediameter>/i,\n /<bedtemperature>([^<]+)<\\/bedtemperature>/i,\n /<nozzletemperature>([^<]+)<\\/nozzletemperature>/i,\n ];\n\n for (const pattern of patterns) {\n const match = xml.match(pattern);\n if (match) {\n const key = pattern.source.match(/<([^>]+)>/)?.[1] || \"\";\n metadata[key] = match[1];\n }\n }\n\n // Extract generator/slicer info\n const generatorMatch = xml.match(/generator=\"([^\"]+)\"/);\n if (generatorMatch) {\n metadata.generator = generatorMatch[1];\n metadata.slicerVersion = generatorMatch[1];\n }\n\n return metadata;\n }\n\n private extractPlates(zipEntries: AdmZip.IZipEntry[]): NonNullable<ThreeMFMetadata[\"plates\"]> {\n const plates: NonNullable<ThreeMFMetadata[\"plates\"]> = [];\n\n // Look for Bambu Lab plate structure (but not .gcode.md5 files)\n const plateEntries = zipEntries.filter(\n (e) => e.entryName.match(/Metadata\\/plate_\\d+\\.gcode$/) && !e.entryName.endsWith(\".md5\"),\n );\n\n if (plateEntries.length === 0) {\n // Single plate file or non-Bambu format\n return [];\n }\n\n for (const entry of plateEntries) {\n const plateMatch = entry.entryName.match(/plate_(\\d+)\\.gcode/);\n if (!plateMatch) continue;\n\n // Bambu uses 1-indexed plate numbers in filenames (plate_1.gcode = plate 1)\n const plateNumber = Number.parseInt(plateMatch[1]);\n // Read more bytes to include CONFIG_BLOCK (contains layer_height, temps, etc.)\n const gcodeContent = entry.getData().toString(\"utf8\", 0, Math.min(50000, entry.getData().length));\n\n // Parse basic metadata from G-code header\n const metadata = this.parseGCodeHeader(gcodeContent);\n\n // Find thumbnails for this plate\n const plateThumbs = zipEntries.filter(\n (e) =>\n e.entryName.includes(`plate_${plateMatch[1]}`) &&\n (e.entryName.endsWith(\".png\") || e.entryName.endsWith(\".jpg\")),\n );\n\n const plateThumbnails: ParsedThumbnail[] = plateThumbs.map((t) => {\n const format = t.entryName.endsWith(\".png\") ? \"PNG\" : \"JPG\";\n const imageData = t.getData();\n\n const sizeMatch = t.entryName.match(/(\\d+)x(\\d+)/);\n let width = sizeMatch ? Number.parseInt(sizeMatch[1]) : 0;\n let height = sizeMatch ? Number.parseInt(sizeMatch[2]) : 0;\n\n if (width === 0 || height === 0) {\n const dimensions = getImageDimensions(imageData, format);\n width = dimensions.width;\n height = dimensions.height;\n }\n\n return {\n width,\n height,\n format,\n data: imageData.toString(\"base64\"),\n };\n });\n const printTime = this.parseTime(\n metadata.model_printing_time || metadata.total_estimated_time || metadata.print_time,\n );\n const filamentWeight = this.parseFloat(\n metadata.total_filament_weight_g || metadata.filament_weight || metadata.total_filament_weight,\n );\n const layerCount = this.parseInt(metadata.total_layer_number || metadata.layer_count || metadata.total_layers);\n\n // Extract all available Bambu metadata from G-code header + config\n const plateMetadata = {\n plateNumber,\n gcodePrintTimeSeconds: printTime,\n filamentUsedGrams: filamentWeight,\n totalLayers: layerCount,\n filamentUsedMm: this.parseFloat(\n metadata.total_filament_length_mm || metadata.filament_length_mm || metadata.filament_used_mm,\n ),\n filamentUsedCm3: this.parseFloat(metadata.total_filament_volume_cm3 || metadata.filament_volume_cm3),\n filamentDensityGramsCm3: this.parseFloat(metadata.filament_density),\n filamentDiameterMm: this.parseFloat(metadata.filament_diameter) || 1.75,\n maxLayerZ: this.parseFloat(metadata.max_z_height || metadata.max_layer_z),\n slicerVersion: metadata.bambustudio || metadata.slicer_version || metadata.slicer || null,\n nozzleDiameterMm: this.parseFloat(metadata.nozzle_diameter),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(\n metadata.first_layer_height || metadata.initial_layer_height || metadata.initial_layer_print_height,\n ),\n bedTemperature: this.parseFloat(\n metadata.bed_temperature_actual ||\n metadata.bed_temperature ||\n metadata.bed_temp ||\n metadata.bed_temperature_initial_layer,\n ),\n nozzleTemperature: this.parseFloat(\n metadata.nozzle_temperature || metadata.nozzle_temp || metadata.nozzle_temperature_initial_layer,\n ),\n fillDensity: metadata.sparse_infill_density || metadata.infill_density || metadata.fill_density || null,\n filamentType: metadata.filament_type || null,\n printerModel: metadata.printer_model || null,\n objects: [],\n thumbnails:\n plateThumbnails.length > 0\n ? plateThumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n };\n\n plates.push(plateMetadata);\n }\n\n return plates.sort((a, b) => a.plateNumber - b.plateNumber);\n }\n\n private parseGCodeHeader(gcode: string): Record<string, string> {\n const metadata: Record<string, string> = {};\n const lines = gcode.split(\"\\n\").slice(0, 1000); // Extended to include CONFIG_BLOCK and early G-code\n\n for (const line of lines) {\n // Parse comment lines\n if (line.startsWith(\";\")) {\n // Skip block markers\n if (line.includes(\"_BLOCK_START\") || line.includes(\"_BLOCK_END\")) continue;\n\n // Special case: BambuStudio version (no key:value pattern)\n // ; BambuStudio 02.04.00.70\n const bambuMatch = line.match(/;\\s*BambuStudio\\s+([\\d.]+)/i);\n if (bambuMatch) {\n metadata.bambustudio = `BambuStudio ${bambuMatch[1]}`;\n continue;\n }\n\n // Match patterns like:\n // ; key: value\n // ; key = value\n // ; key : value\n const match = line.match(/;\\s*([^=:]+?)\\s*[:=]\\s*(.+)/);\n if (match) {\n let key = match[1].trim().toLowerCase().replace(/\\s+/g, \"_\");\n let value = match[2].trim();\n\n // Remove bracketed units from key: \"total_filament_weight_[g]\" -> \"total_filament_weight_g\"\n // Also normalize special chars: [cm^3] -> cm3\n key = key.replace(/\\[([^\\]]+)\\]/g, (_, unit) => unit.replace(/\\^/g, \"\"));\n\n // Split value on semicolon and take first part (handles \"11m 15s; total estimated time: 18m 15s\")\n value = value.split(\";\")[0].trim();\n\n // Remove remaining bracketed units and percentages from value\n value = value.replace(/\\s*\\[.*?\\]\\s*/g, \"\"); // Remove [units]\n\n // Store the value (don't overwrite if already set from header)\n if (!metadata[key]) {\n metadata[key] = value;\n }\n }\n } else {\n // Parse actual G-code commands for temperatures\n // M140 S65 - Set bed temperature\n // M190 S65 - Wait for bed temperature\n if (!metadata.bed_temperature_actual) {\n const bedTempMatch = line.match(/^M1(40|90)\\s+S(\\d+)/);\n if (bedTempMatch && Number.parseInt(bedTempMatch[2]) > 0) {\n metadata.bed_temperature_actual = bedTempMatch[2];\n }\n }\n }\n }\n\n return metadata;\n }\n\n private extractThumbnails(zipEntries: AdmZip.IZipEntry[]): ParsedThumbnail[] {\n const thumbnails: ParsedThumbnail[] = [];\n\n const thumbEntries = zipEntries.filter(\n (e) => e.entryName.match(/Metadata\\/.*\\.(png|jpg|jpeg)/i) || e.entryName.match(/Thumbnails\\/.*/i),\n );\n\n for (const entry of thumbEntries) {\n const format = entry.entryName.match(/\\.(png|jpg|jpeg)$/i)?.[1].toUpperCase() || \"PNG\";\n const imageData = entry.getData();\n\n const sizeMatch = entry.entryName.match(/(\\d+)x(\\d+)/);\n let width = sizeMatch ? Number.parseInt(sizeMatch[1]) : 0;\n let height = sizeMatch ? Number.parseInt(sizeMatch[2]) : 0;\n\n if (width === 0 || height === 0) {\n const dimensions = getImageDimensions(imageData, format);\n width = dimensions.width;\n height = dimensions.height;\n }\n\n const base64Data = imageData.toString(\"base64\");\n\n thumbnails.push({\n width,\n height,\n format,\n data: base64Data,\n });\n }\n\n return thumbnails;\n }\n\n private parseFloat(value: string | undefined): number | null {\n if (!value) return null;\n const num = Number.parseFloat(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseInt(value: string | undefined): number | null {\n if (!value) return null;\n const num = Number.parseInt(value, 10);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n // Try parsing as a duration string first (e.g., \"11m 15s\" or \"1h 30m\")\n const match = value.match(/(?:(\\d+)h)?(?:\\s*(\\d+)m)?(?:\\s*(\\d+)s)?/);\n if (match && (match[1] || match[2] || match[3])) {\n const hours = Number.parseInt(match[1] || \"0\");\n const minutes = Number.parseInt(match[2] || \"0\");\n const secs = Number.parseInt(match[3] || \"0\");\n return hours * 3600 + minutes * 60 + secs;\n }\n\n // Fallback to parsing as plain seconds\n const seconds = Number.parseFloat(value);\n if (!Number.isNaN(seconds)) return seconds;\n\n return null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bgcode.parser.js","names":[],"sources":["../../../src/utils/parsers/bgcode.parser.ts"],"sourcesContent":["import path from \"node:path\";\nimport { BGCodeMetadata } from \"@/entities/print-job.entity\";\nimport fs, { open } from \"node:fs/promises\";\nimport {\n parseFileHeader,\n parseBlockHeaders,\n getBlockData,\n decompressBlock,\n extractMetadataFromBlocks,\n} from \"../bgcode/bgcode.utils\";\nimport {\n BgCodeBlockTypes,\n type BgCodeBlockHeader,\n type BgCodeThumbnailParameters,\n BgCodeBlockTypeName,\n BgCodeCompressionName,\n} from \"../bgcode/bgcode.types\";\nimport { processThumbnail } from \"../bgcode/bgcode-thumbnail.parser\";\nimport { ParsedThumbnail } from \"./parser.types\";\n\ninterface BGCodeParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n blocks?: Array<{\n type: string;\n compressedSize: number;\n uncompressedSize: number;\n compression: string;\n }>;\n };\n normalized: BGCodeMetadata;\n}\n\n/**\n * BGCode parser for extracting metadata from .bgcode files\n * BGCode is a binary G-code format used by Prusa printers\n */\nexport class BGCodeParser {\n async parse(filePath: string): Promise<BGCodeParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = path.basename(filePath);\n\n const fileHandle = await open(filePath, \"r\");\n\n try {\n const { version, checksumType } = await parseFileHeader(fileHandle);\n if (version !== 1) {\n throw new Error(`Unsupported BGCode version: ${version}`);\n }\n\n const blockHeaders = await parseBlockHeaders(fileHandle, stats.size, checksumType, true);\n\n const metadata = await extractMetadataFromBlocks(fileHandle, blockHeaders);\n const thumbnails = await this.extractThumbnailsFromBlocks(fileHandle, blockHeaders);\n\n const isMmu =\n this.isMmuData(metadata.nozzle_diameter) ||\n this.isMmuData(metadata.temperature) ||\n this.isMmuData(metadata.filament_used_mm) ||\n this.isMmuData(metadata.bed_temperature) ||\n this.isMmuData(metadata.filament_type, \";\");\n\n const normalized: BGCodeMetadata = {\n fileName,\n fileFormat: \"bgcode\",\n fileSize: stats.size,\n producer: metadata.producer,\n producedOn: metadata.produced_on,\n checksumType: checksumType === 1 ? \"CRC32\" : \"None\",\n isMmu: isMmu || undefined,\n gcodePrintTimeSeconds: this.parseTime(metadata.estimated_printing_time_normal_mode || metadata.print_time),\n gcodePrintTimeSecondsSilent: this.parseTime(metadata.estimated_printing_time_silent_mode),\n nozzleDiameterMm: isMmu\n ? this.parseNumberArray(metadata.nozzle_diameter)\n : this.parseFirstValue(metadata.nozzle_diameter),\n filamentDiameterMm: isMmu\n ? this.parseNumberArray(metadata.filament_diameter)\n : this.parseFirstValue(metadata.filament_diameter) || 1.75,\n filamentDensityGramsCm3: isMmu\n ? this.parseNumberArray(metadata.filament_density)\n : this.parseFirstValue(metadata.filament_density),\n filamentUsedMm: isMmu\n ? this.parseNumberArray(metadata.filament_used_mm)\n : this.parseFirstValue(metadata.filament_used_mm),\n filamentUsedCm3: isMmu\n ? this.parseNumberArray(metadata.filament_used_cm3)\n : this.parseFirstValue(metadata.filament_used_cm3),\n filamentUsedGrams: isMmu\n ? this.parseNumberArray(metadata.filament_used_g)\n : this.parseFirstValue(metadata.filament_used_g),\n totalFilamentUsedGrams: isMmu\n ? this.sumNumberArray(this.parseNumberArray(metadata.filament_used_g))\n : this.parseFirstValue(metadata.filament_used_g),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(metadata.first_layer_height || metadata.initial_layer_height),\n bedTemperature: isMmu\n ? this.parseNumberArray(metadata.bed_temperature)\n : this.parseFirstValue(metadata.bed_temperature),\n nozzleTemperature: isMmu\n ? this.parseNumberArray(metadata.temperature)\n : this.parseFirstValue(metadata.temperature),\n fillDensity: metadata.fill_density || null,\n filamentType: isMmu\n ? this.parseStringArray(metadata.filament_type, \";\")\n : this.parseFirstCsvValue(metadata.filament_type),\n printerModel: metadata.printer_model || null,\n slicerVersion: metadata.producer || null,\n maxLayerZ: this.parseFloat(metadata.max_layer_z),\n totalLayers: this.parseInt(metadata.total_layers || metadata.layer_count),\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n blocks: blockHeaders.map((b) => ({\n type: BgCodeBlockTypeName[b.type] || `Unknown(${b.type})`,\n compressedSize: b.compressedSize,\n uncompressedSize: b.uncompressedSize,\n compression: BgCodeCompressionName[b.compression] || `Unknown(${b.compression})`,\n })),\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n blocks: blockHeaders.map((b) => ({\n type: BgCodeBlockTypeName[b.type] || `Unknown(${b.type})`,\n compressedSize: b.compressedSize,\n uncompressedSize: b.uncompressedSize,\n compression: BgCodeCompressionName[b.compression] || `Unknown(${b.compression})`,\n })),\n },\n normalized,\n };\n } finally {\n await fileHandle.close();\n }\n }\n\n private async extractThumbnailsFromBlocks(\n fileHandle: any,\n blockHeaders: BgCodeBlockHeader[],\n ): Promise<ParsedThumbnail[]> {\n const thumbnails: ParsedThumbnail[] = [];\n\n const thumbnailBlocks = blockHeaders.filter((b) => b.type === BgCodeBlockTypes.Thumbnail);\n\n for (const header of thumbnailBlocks) {\n const parameters = header.parameters as BgCodeThumbnailParameters;\n\n const blockData = await getBlockData(fileHandle, header);\n const imageData = decompressBlock(header.compression, blockData);\n\n const processed = processThumbnail(imageData, parameters);\n\n thumbnails.push({\n width: parameters.width,\n height: parameters.height,\n format: processed.extension,\n data: processed.data.toString(\"base64\"),\n });\n }\n\n return thumbnails;\n }\n\n private parseNumber(value: string | undefined, parser: (val: string) => number): number | null {\n if (!value) return null;\n const num = parser(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFloat(value: string | undefined): number | null {\n return this.parseNumber(value, Number.parseFloat);\n }\n\n private parseInt(value: string | undefined): number | null {\n return this.parseNumber(value, (val) => Number.parseInt(val, 10));\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n let totalSeconds = 0;\n const hours = new RegExp(/(\\d+)h/).exec(value);\n const minutes = new RegExp(/(\\d+)m/).exec(value);\n const seconds = new RegExp(/(\\d+)s/).exec(value);\n\n if (hours) totalSeconds += Number.parseInt(hours[1]) * 3600;\n if (minutes) totalSeconds += Number.parseInt(minutes[1]) * 60;\n if (seconds) totalSeconds += Number.parseInt(seconds[1]);\n\n if (hours || minutes || seconds) return totalSeconds;\n\n const num = Number.parseFloat(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFirstValue(value: string | undefined): number | null {\n if (!value) return null;\n\n const firstValue = value.split(\",\")[0].trim();\n const num = Number.parseFloat(firstValue);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFirstCsvValue(value: string | undefined): string | null {\n if (!value) return null;\n return value.trim();\n }\n\n private parseNumberArray(value: string | undefined): number[] | null {\n if (!value) return null;\n const values = value\n .split(\",\")\n .map((v) => Number.parseFloat(v.trim()))\n .filter((n) => !Number.isNaN(n));\n return values.length > 0 ? values : null;\n }\n\n private parseStringArray(value: string | undefined, separator: string = \";\"): string[] | null {\n if (!value) return null;\n const values = value\n .split(separator)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n return values.length > 0 ? values : null;\n }\n\n private isMmuData(value: string | undefined, separator: string = \",\"): boolean {\n if (!value) return false;\n const parts = value\n .split(separator)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n return parts.length > 1;\n }\n\n private sumNumberArray(values: number[] | null): number | null {\n if (!values || values.length === 0) return null;\n return values.reduce((sum, val) => sum + val, 0);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bgcode.parser.js","names":[],"sources":["../../../src/utils/parsers/bgcode.parser.ts"],"sourcesContent":["import path from \"node:path\";\nimport { BGCodeMetadata } from \"@/entities/print-job.entity\";\nimport fs, { open } from \"node:fs/promises\";\nimport {\n parseFileHeader,\n parseBlockHeaders,\n getBlockData,\n decompressBlock,\n extractMetadataFromBlocks,\n} from \"../bgcode/bgcode.utils\";\nimport {\n BgCodeBlockTypes,\n type BgCodeBlockHeader,\n type BgCodeThumbnailParameters,\n BgCodeBlockTypeName,\n BgCodeCompressionName,\n} from \"../bgcode/bgcode.types\";\nimport { processThumbnail } from \"../bgcode/bgcode-thumbnail.parser\";\nimport { ParsedThumbnail } from \"./parser.types\";\n\ninterface BGCodeParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n blocks?: Array<{\n type: string;\n compressedSize: number;\n uncompressedSize: number;\n compression: string;\n }>;\n };\n normalized: BGCodeMetadata;\n}\n\n/**\n * BGCode parser for extracting metadata from .bgcode files\n * BGCode is a binary G-code format used by Prusa printers\n */\nexport class BGCodeParser {\n async parse(filePath: string): Promise<BGCodeParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = path.basename(filePath);\n\n const fileHandle = await open(filePath, \"r\");\n\n try {\n const { version, checksumType } = await parseFileHeader(fileHandle);\n if (version !== 1) {\n throw new Error(`Unsupported BGCode version: ${version}`);\n }\n\n const blockHeaders = await parseBlockHeaders(fileHandle, stats.size, checksumType, true);\n\n const metadata = await extractMetadataFromBlocks(fileHandle, blockHeaders);\n const thumbnails = await this.extractThumbnailsFromBlocks(fileHandle, blockHeaders);\n\n const isMmu =\n this.isMmuData(metadata.nozzle_diameter) ||\n this.isMmuData(metadata.temperature) ||\n this.isMmuData(metadata.filament_used_mm) ||\n this.isMmuData(metadata.bed_temperature) ||\n this.isMmuData(metadata.filament_type, \";\");\n\n const normalized: BGCodeMetadata = {\n fileName,\n fileFormat: \"bgcode\",\n fileSize: stats.size,\n producer: metadata.producer,\n producedOn: metadata.produced_on,\n checksumType: checksumType === 1 ? \"CRC32\" : \"None\",\n isMmu: isMmu || undefined,\n gcodePrintTimeSeconds: this.parseTime(metadata.estimated_printing_time_normal_mode || metadata.print_time),\n gcodePrintTimeSecondsSilent: this.parseTime(metadata.estimated_printing_time_silent_mode),\n nozzleDiameterMm: isMmu\n ? this.parseNumberArray(metadata.nozzle_diameter)\n : this.parseFirstValue(metadata.nozzle_diameter),\n filamentDiameterMm: isMmu\n ? this.parseNumberArray(metadata.filament_diameter)\n : this.parseFirstValue(metadata.filament_diameter) || 1.75,\n filamentDensityGramsCm3: isMmu\n ? this.parseNumberArray(metadata.filament_density)\n : this.parseFirstValue(metadata.filament_density),\n filamentUsedMm: isMmu\n ? this.parseNumberArray(metadata.filament_used_mm)\n : this.parseFirstValue(metadata.filament_used_mm),\n filamentUsedCm3: isMmu\n ? this.parseNumberArray(metadata.filament_used_cm3)\n : this.parseFirstValue(metadata.filament_used_cm3),\n filamentUsedGrams: isMmu\n ? this.parseNumberArray(metadata.filament_used_g)\n : this.parseFirstValue(metadata.filament_used_g),\n totalFilamentUsedGrams: isMmu\n ? this.sumNumberArray(this.parseNumberArray(metadata.filament_used_g))\n : this.parseFirstValue(metadata.filament_used_g),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(metadata.first_layer_height || metadata.initial_layer_height),\n bedTemperature: isMmu\n ? this.parseNumberArray(metadata.bed_temperature)\n : this.parseFirstValue(metadata.bed_temperature),\n nozzleTemperature: isMmu\n ? this.parseNumberArray(metadata.temperature)\n : this.parseFirstValue(metadata.temperature),\n fillDensity: metadata.fill_density || null,\n filamentType: isMmu\n ? this.parseStringArray(metadata.filament_type, \";\")\n : this.parseFirstCsvValue(metadata.filament_type),\n printerModel: metadata.printer_model || null,\n slicerVersion: metadata.producer || null,\n maxLayerZ: this.parseFloat(metadata.max_layer_z),\n totalLayers: this.parseInt(metadata.total_layers || metadata.layer_count),\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n blocks: blockHeaders.map((b) => ({\n type: BgCodeBlockTypeName[b.type] || `Unknown(${b.type})`,\n compressedSize: b.compressedSize,\n uncompressedSize: b.uncompressedSize,\n compression: BgCodeCompressionName[b.compression] || `Unknown(${b.compression})`,\n })),\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n blocks: blockHeaders.map((b) => ({\n type: BgCodeBlockTypeName[b.type] || `Unknown(${b.type})`,\n compressedSize: b.compressedSize,\n uncompressedSize: b.uncompressedSize,\n compression: BgCodeCompressionName[b.compression] || `Unknown(${b.compression})`,\n })),\n },\n normalized,\n };\n } finally {\n await fileHandle.close();\n }\n }\n\n private async extractThumbnailsFromBlocks(\n fileHandle: any,\n blockHeaders: BgCodeBlockHeader[],\n ): Promise<ParsedThumbnail[]> {\n const thumbnails: ParsedThumbnail[] = [];\n\n const thumbnailBlocks = blockHeaders.filter((b) => b.type === BgCodeBlockTypes.Thumbnail);\n\n for (const header of thumbnailBlocks) {\n const parameters = header.parameters as BgCodeThumbnailParameters;\n\n const blockData = await getBlockData(fileHandle, header);\n const imageData = decompressBlock(header.compression, blockData);\n\n const processed = processThumbnail(imageData, parameters);\n\n thumbnails.push({\n width: parameters.width,\n height: parameters.height,\n format: processed.extension,\n data: processed.data.toString(\"base64\"),\n });\n }\n\n return thumbnails;\n }\n\n private parseNumber(value: string | undefined, parser: (val: string) => number): number | null {\n if (!value) return null;\n const num = parser(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFloat(value: string | undefined): number | null {\n return this.parseNumber(value, Number.parseFloat);\n }\n\n private parseInt(value: string | undefined): number | null {\n return this.parseNumber(value, (val) => Number.parseInt(val, 10));\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n let totalSeconds = 0;\n const hours = new RegExp(/(\\d+)h/).exec(value);\n const minutes = new RegExp(/(\\d+)m/).exec(value);\n const seconds = new RegExp(/(\\d+)s/).exec(value);\n\n if (hours) totalSeconds += Number.parseInt(hours[1]) * 3600;\n if (minutes) totalSeconds += Number.parseInt(minutes[1]) * 60;\n if (seconds) totalSeconds += Number.parseInt(seconds[1]);\n\n if (hours || minutes || seconds) return totalSeconds;\n\n const num = Number.parseFloat(value);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFirstValue(value: string | undefined): number | null {\n if (!value) return null;\n\n const firstValue = value.split(\",\")[0].trim();\n const num = Number.parseFloat(firstValue);\n return Number.isNaN(num) ? null : num;\n }\n\n private parseFirstCsvValue(value: string | undefined): string | null {\n if (!value) return null;\n return value.trim();\n }\n\n private parseNumberArray(value: string | undefined): number[] | null {\n if (!value) return null;\n const values = value\n .split(\",\")\n .map((v) => Number.parseFloat(v.trim()))\n .filter((n) => !Number.isNaN(n));\n return values.length > 0 ? values : null;\n }\n\n private parseStringArray(value: string | undefined, separator: string = \";\"): string[] | null {\n if (!value) return null;\n const values = value\n .split(separator)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n return values.length > 0 ? values : null;\n }\n\n private isMmuData(value: string | undefined, separator: string = \",\"): boolean {\n if (!value) return false;\n const parts = value\n .split(separator)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n return parts.length > 1;\n }\n\n private sumNumberArray(values: number[] | null): number | null {\n if (!values || values.length === 0) return null;\n return values.reduce((sum, val) => sum + val, 0);\n }\n}\n"],"mappings":";;;;;;;;;;AAqCA,IAAa,eAAb,MAA0B;CACxB,MAAM,MAAM,UAA8C;EACxD,MAAM,QAAQ,MAAM,GAAG,KAAK,SAAS;EACrC,MAAM,WAAW,KAAK,SAAS,SAAS;EAExC,MAAM,aAAa,MAAM,KAAK,UAAU,IAAI;EAE5C,IAAI;GACF,MAAM,EAAE,SAAS,iBAAiB,MAAM,gBAAgB,WAAW;GACnE,IAAI,YAAY,GACd,MAAM,IAAI,MAAM,+BAA+B,UAAU;GAG3D,MAAM,eAAe,MAAM,kBAAkB,YAAY,MAAM,MAAM,cAAc,KAAK;GAExF,MAAM,WAAW,MAAM,0BAA0B,YAAY,aAAa;GAC1E,MAAM,aAAa,MAAM,KAAK,4BAA4B,YAAY,aAAa;GAEnF,MAAM,QACJ,KAAK,UAAU,SAAS,gBAAgB,IACxC,KAAK,UAAU,SAAS,YAAY,IACpC,KAAK,UAAU,SAAS,iBAAiB,IACzC,KAAK,UAAU,SAAS,gBAAgB,IACxC,KAAK,UAAU,SAAS,eAAe,IAAI;GAE7C,MAAM,aAA6B;IACjC;IACA,YAAY;IACZ,UAAU,MAAM;IAChB,UAAU,SAAS;IACnB,YAAY,SAAS;IACrB,cAAc,iBAAiB,IAAI,UAAU;IAC7C,OAAO,SAAS,KAAA;IAChB,uBAAuB,KAAK,UAAU,SAAS,uCAAuC,SAAS,WAAW;IAC1G,6BAA6B,KAAK,UAAU,SAAS,oCAAoC;IACzF,kBAAkB,QACd,KAAK,iBAAiB,SAAS,gBAAgB,GAC/C,KAAK,gBAAgB,SAAS,gBAAgB;IAClD,oBAAoB,QAChB,KAAK,iBAAiB,SAAS,kBAAkB,GACjD,KAAK,gBAAgB,SAAS,kBAAkB,IAAI;IACxD,yBAAyB,QACrB,KAAK,iBAAiB,SAAS,iBAAiB,GAChD,KAAK,gBAAgB,SAAS,iBAAiB;IACnD,gBAAgB,QACZ,KAAK,iBAAiB,SAAS,iBAAiB,GAChD,KAAK,gBAAgB,SAAS,iBAAiB;IACnD,iBAAiB,QACb,KAAK,iBAAiB,SAAS,kBAAkB,GACjD,KAAK,gBAAgB,SAAS,kBAAkB;IACpD,mBAAmB,QACf,KAAK,iBAAiB,SAAS,gBAAgB,GAC/C,KAAK,gBAAgB,SAAS,gBAAgB;IAClD,wBAAwB,QACpB,KAAK,eAAe,KAAK,iBAAiB,SAAS,gBAAgB,CAAC,GACpE,KAAK,gBAAgB,SAAS,gBAAgB;IAClD,aAAa,KAAK,WAAW,SAAS,aAAa;IACnD,kBAAkB,KAAK,WAAW,SAAS,sBAAsB,SAAS,qBAAqB;IAC/F,gBAAgB,QACZ,KAAK,iBAAiB,SAAS,gBAAgB,GAC/C,KAAK,gBAAgB,SAAS,gBAAgB;IAClD,mBAAmB,QACf,KAAK,iBAAiB,SAAS,YAAY,GAC3C,KAAK,gBAAgB,SAAS,YAAY;IAC9C,aAAa,SAAS,gBAAgB;IACtC,cAAc,QACV,KAAK,iBAAiB,SAAS,eAAe,IAAI,GAClD,KAAK,mBAAmB,SAAS,cAAc;IACnD,cAAc,SAAS,iBAAiB;IACxC,eAAe,SAAS,YAAY;IACpC,WAAW,KAAK,WAAW,SAAS,YAAY;IAChD,aAAa,KAAK,SAAS,SAAS,gBAAgB,SAAS,YAAY;IACzE,YACE,WAAW,SAAS,IAChB,WAAW,KAAK,OAAO;KACrB,OAAO,EAAE;KACT,QAAQ,EAAE;KACV,QAAQ,EAAE;KACV,YAAY,EAAE,MAAM,UAAU;KAC/B,EAAE,GACH,KAAA;IACN,QAAQ,aAAa,KAAK,OAAO;KAC/B,MAAM,oBAAoB,EAAE,SAAS,WAAW,EAAE,KAAK;KACvD,gBAAgB,EAAE;KAClB,kBAAkB,EAAE;KACpB,aAAa,sBAAsB,EAAE,gBAAgB,WAAW,EAAE,YAAY;KAC/E,EAAE;IACJ;GAED,OAAO;IACL,KAAK;KACH,aAAa;KACb,QAAQ,aAAa,KAAK,OAAO;MAC/B,MAAM,oBAAoB,EAAE,SAAS,WAAW,EAAE,KAAK;MACvD,gBAAgB,EAAE;MAClB,kBAAkB,EAAE;MACpB,aAAa,sBAAsB,EAAE,gBAAgB,WAAW,EAAE,YAAY;MAC/E,EAAE;KACJ;IACD;IACD;YACO;GACR,MAAM,WAAW,OAAO;;;CAI5B,MAAc,4BACZ,YACA,cAC4B;EAC5B,MAAM,aAAgC,EAAE;EAExC,MAAM,kBAAkB,aAAa,QAAQ,MAAM,EAAE,SAAS,iBAAiB,UAAU;EAEzF,KAAK,MAAM,UAAU,iBAAiB;GACpC,MAAM,aAAa,OAAO;GAE1B,MAAM,YAAY,MAAM,aAAa,YAAY,OAAO;GAGxD,MAAM,YAAY,iBAFA,gBAAgB,OAAO,aAAa,UAEV,EAAE,WAAW;GAEzD,WAAW,KAAK;IACd,OAAO,WAAW;IAClB,QAAQ,WAAW;IACnB,QAAQ,UAAU;IAClB,MAAM,UAAU,KAAK,SAAS,SAAS;IACxC,CAAC;;EAGJ,OAAO;;CAGT,YAAoB,OAA2B,QAAgD;EAC7F,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,MAAM,OAAO,MAAM;EACzB,OAAO,OAAO,MAAM,IAAI,GAAG,OAAO;;CAGpC,WAAmB,OAA0C;EAC3D,OAAO,KAAK,YAAY,OAAO,OAAO,WAAW;;CAGnD,SAAiB,OAA0C;EACzD,OAAO,KAAK,YAAY,QAAQ,QAAQ,OAAO,SAAS,KAAK,GAAG,CAAC;;CAGnE,UAAkB,OAA0C;EAC1D,IAAI,CAAC,OAAO,OAAO;EAEnB,IAAI,eAAe;EACnB,MAAM,yBAAQ,IAAI,OAAO,SAAS,EAAC,KAAK,MAAM;EAC9C,MAAM,2BAAU,IAAI,OAAO,SAAS,EAAC,KAAK,MAAM;EAChD,MAAM,2BAAU,IAAI,OAAO,SAAS,EAAC,KAAK,MAAM;EAEhD,IAAI,OAAO,gBAAgB,OAAO,SAAS,MAAM,GAAG,GAAG;EACvD,IAAI,SAAS,gBAAgB,OAAO,SAAS,QAAQ,GAAG,GAAG;EAC3D,IAAI,SAAS,gBAAgB,OAAO,SAAS,QAAQ,GAAG;EAExD,IAAI,SAAS,WAAW,SAAS,OAAO;EAExC,MAAM,MAAM,OAAO,WAAW,MAAM;EACpC,OAAO,OAAO,MAAM,IAAI,GAAG,OAAO;;CAGpC,gBAAwB,OAA0C;EAChE,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,aAAa,MAAM,MAAM,IAAI,CAAC,GAAG,MAAM;EAC7C,MAAM,MAAM,OAAO,WAAW,WAAW;EACzC,OAAO,OAAO,MAAM,IAAI,GAAG,OAAO;;CAGpC,mBAA2B,OAA0C;EACnE,IAAI,CAAC,OAAO,OAAO;EACnB,OAAO,MAAM,MAAM;;CAGrB,iBAAyB,OAA4C;EACnE,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,SAAS,MACZ,MAAM,IAAI,CACV,KAAK,MAAM,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CACvC,QAAQ,MAAM,CAAC,OAAO,MAAM,EAAE,CAAC;EAClC,OAAO,OAAO,SAAS,IAAI,SAAS;;CAGtC,iBAAyB,OAA2B,YAAoB,KAAsB;EAC5F,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,SAAS,MACZ,MAAM,UAAU,CAChB,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,EAAE;EAC9B,OAAO,OAAO,SAAS,IAAI,SAAS;;CAGtC,UAAkB,OAA2B,YAAoB,KAAc;EAC7E,IAAI,CAAC,OAAO,OAAO;EAKnB,OAJc,MACX,MAAM,UAAU,CAChB,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,EAChB,CAAC,SAAS;;CAGxB,eAAuB,QAAwC;EAC7D,IAAI,CAAC,UAAU,OAAO,WAAW,GAAG,OAAO;EAC3C,OAAO,OAAO,QAAQ,KAAK,QAAQ,MAAM,KAAK,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gcode.parser.js","names":["fs"],"sources":["../../../src/utils/parsers/gcode.parser.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as readline from \"node:readline\";\nimport { createReadStream } from \"node:fs\";\nimport { GCodeMetadata } from \"@/entities/print-job.entity\";\nimport { convertQoiToPng } from \"../bgcode/bgcode-thumbnail.parser\";\nimport { ParsedThumbnail } from \"./parser.types\";\n\ninterface GCodeParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n metadata: Record<string, string>;\n };\n normalized: GCodeMetadata;\n}\n\n/**\n * G-code parser for extracting metadata from .gcode files\n * Reads first and last N lines to extract slicer metadata\n */\nexport class GCodeParser {\n private readonly maxHeaderLinesToRead = 500; // Read from start\n private readonly maxFooterLinesToRead = 500; // Read from end\n\n async parse(filePath: string): Promise<GCodeParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = filePath.split(/[/\\\\]/).pop() || filePath;\n\n const metadata = await this.extractMetadata(filePath);\n const thumbnails = await this.extractThumbnails(filePath);\n\n const normalized: GCodeMetadata = {\n fileName,\n fileFormat: \"gcode\",\n fileSize: stats.size,\n gcodePrintTimeSeconds: this.parseTime(\n metadata.estimated_printing_time_normal_mode || metadata.estimated_printing_time || metadata.print_time,\n ),\n nozzleDiameterMm: this.parseFloat(metadata.nozzle_diameter),\n filamentDiameterMm: this.parseFloat(metadata.filament_diameter),\n filamentDensityGramsCm3: this.parseFloat(metadata.filament_density),\n filamentUsedMm: this.parseFloat(metadata.filament_used_mm),\n filamentUsedCm3: this.parseFloat(metadata.filament_used_cm3),\n filamentUsedGrams: this.parseFloat(metadata.filament_used_g),\n totalFilamentUsedGrams: this.parseFloat(metadata.total_filament_used_g || metadata.filament_used_g),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(metadata.first_layer_height || metadata.initial_layer_height),\n bedTemperature: this.parseFloat(metadata.bed_temperature || metadata.first_layer_bed_temperature),\n nozzleTemperature: this.parseFloat(metadata.temperature || metadata.first_layer_temperature),\n fillDensity: metadata.fill_density || null,\n filamentType: metadata.filament_type || null,\n printerModel: metadata.printer_model || metadata.printer_name || null,\n slicerVersion: metadata.generated_by || metadata.slicer_version || null,\n maxLayerZ: this.parseFloat(metadata.max_layer_z),\n totalLayers: this.parseInt(metadata.total_layers) || this.parseInt(metadata.layer_count),\n generatedBy: metadata.generated_by,\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n metadata,\n },\n normalized,\n };\n }\n\n private async extractMetadata(filePath: string): Promise<Record<string, string>> {\n const metadata: Record<string, string> = {};\n\n // Read from start of file (header often has thumbnails and basic info)\n await this.extractMetadataFromStart(filePath, metadata);\n\n // Read from end of file (footer often has summary metadata - filament, time, etc.)\n await this.extractMetadataFromEnd(filePath, metadata);\n\n return metadata;\n }\n\n private async extractMetadataFromStart(filePath: string, metadata: Record<string, string>): Promise<void> {\n let linesRead = 0;\n\n const fileStream = createReadStream(filePath);\n const rl = readline.createInterface({\n input: fileStream,\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n if (linesRead >= this.maxHeaderLinesToRead) break;\n linesRead++;\n\n this.parseMetadataLine(line, metadata);\n }\n\n rl.close();\n fileStream.close();\n }\n\n private async extractMetadataFromEnd(filePath: string, metadata: Record<string, string>): Promise<void> {\n // Read last N lines efficiently\n const stats = await fs.stat(filePath);\n const fileSize = stats.size;\n\n // Estimate bytes to read (assume ~50 bytes per line avg)\n const estimatedBytes = this.maxFooterLinesToRead * 50;\n const startPosition = Math.max(0, fileSize - estimatedBytes);\n\n const fileHandle = await fs.open(filePath, \"r\");\n try {\n const buffer = Buffer.alloc(estimatedBytes);\n const { bytesRead } = await fileHandle.read(buffer, 0, estimatedBytes, startPosition);\n const text = buffer.toString(\"utf8\", 0, bytesRead);\n const lines = text.split(\"\\n\");\n\n // Process footer lines (summary metadata often here)\n for (const line of lines) {\n this.parseMetadataLine(line, metadata);\n }\n } finally {\n await fileHandle.close();\n }\n }\n\n private parseMetadataLine(line: string, metadata: Record<string, string>): void {\n // Skip non-comment lines\n if (!line.startsWith(\";\")) return;\n\n // Special case: \"; generated by PrusaSlicer X.X.X on ...\"\n const generatedByMatch = line.match(/^;\\s*generated by\\s+([^\\s]+)/i);\n if (generatedByMatch && !metadata.generated_by) {\n metadata.generated_by = generatedByMatch[1];\n return;\n }\n\n // Parse PrusaSlicer/SuperSlicer format: \"; key = value\"\n const prusaMatch = line.match(/^;\\s*([^=]+?)\\s*=\\s*(.+)$/);\n if (prusaMatch) {\n let key = prusaMatch[1].trim().toLowerCase().replace(/\\s+/g, \"_\");\n let value = prusaMatch[2].trim();\n\n // Normalize bracketed units: \"filament used [mm]\" -> \"filament_used_mm\"\n key = key.replace(/\\[([^\\]]+)\\]/g, (_, unit) => \"_\" + unit.replace(/\\^/g, \"\"));\n // Normalize parentheses: \"estimated printing time (normal mode)\" -> \"estimated_printing_time_normal_mode\"\n key = key.replace(/\\(([^)]+)\\)/g, (_, content) => \"_\" + content.replace(/\\s+/g, \"_\"));\n // Normalize multiple underscores\n key = key.replace(/_+/g, \"_\");\n\n // Don't overwrite if already set (header takes precedence)\n if (!metadata[key]) {\n metadata[key] = value.trim();\n }\n return;\n }\n\n // Parse Cura format: \";KEY:value\"\n const curaMatch = line.match(/^;([A-Z_]+):(.+)$/);\n if (curaMatch) {\n const [, key, value] = curaMatch;\n const normalizedKey = key.toLowerCase();\n if (!metadata[normalizedKey]) {\n metadata[normalizedKey] = value.trim();\n }\n return;\n }\n\n // Parse Simplify3D format: \"; key: value\"\n const s3dMatch = line.match(/^;\\s*([^:]+?):\\s*(.+)$/);\n if (s3dMatch) {\n const [, key, value] = s3dMatch;\n const normalizedKey = key.trim().toLowerCase().replace(/\\s+/g, \"_\");\n if (!metadata[normalizedKey]) {\n metadata[normalizedKey] = value.trim();\n }\n }\n }\n\n private async extractThumbnails(filePath: string): Promise<ParsedThumbnail[]> {\n const thumbnails: ParsedThumbnail[] = [];\n let linesRead = 0;\n let inThumbnail = false;\n let thumbnailData: string[] = [];\n let currentWidth = 0;\n let currentHeight = 0;\n let currentFormat = \"PNG\";\n\n const fileStream = createReadStream(filePath);\n const rl = readline.createInterface({\n input: fileStream,\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n if (linesRead >= this.maxHeaderLinesToRead && !inThumbnail) break;\n linesRead++;\n\n // PrusaSlicer thumbnail format\n // Format 1: ; thumbnail begin 313x173 57100 (width x height dataLength)\n // Format 2: ; thumbnail begin 313x173 PNG (width x height format)\n const thumbnailStart = line.match(/;\\s*thumbnail begin (\\d+)x(\\d+)\\s*(\\w+)?/i);\n if (thumbnailStart) {\n inThumbnail = true;\n currentWidth = parseInt(thumbnailStart[1]);\n currentHeight = parseInt(thumbnailStart[2]);\n\n // Third parameter could be format (PNG/JPG/QOI) or data length (number)\n const thirdParam = thumbnailStart[3];\n if (thirdParam && /^(PNG|JPG|JPEG|QOI)$/i.test(thirdParam)) {\n currentFormat = thirdParam.toUpperCase();\n } else {\n // If it's a number or not specified, default to PNG\n currentFormat = \"PNG\";\n }\n\n thumbnailData = [];\n continue;\n }\n\n if (inThumbnail) {\n if (line.match(/;\\s*thumbnail end/i)) {\n let base64Data = thumbnailData.join(\"\");\n let format = currentFormat.toUpperCase();\n\n if (format === \"QOI\") {\n try {\n const qoiBuffer = Buffer.from(base64Data, \"base64\");\n const pngBuffer = convertQoiToPng(qoiBuffer);\n base64Data = pngBuffer.toString(\"base64\");\n format = \"PNG\";\n } catch {\n // Keep original QOI if conversion fails\n }\n }\n\n thumbnails.push({\n width: currentWidth,\n height: currentHeight,\n format,\n data: base64Data,\n });\n inThumbnail = false;\n thumbnailData = [];\n } else if (line.startsWith(\";\")) {\n const data = line.substring(1).trim();\n if (data) {\n thumbnailData.push(data);\n }\n }\n }\n }\n\n rl.close();\n fileStream.close();\n\n return thumbnails;\n }\n\n private parseFloat(value: string | undefined): number | null {\n if (!value) return null;\n const num = parseFloat(value);\n return isNaN(num) ? null : num;\n }\n\n private parseInt(value: string | undefined): number | null {\n if (!value) return null;\n const num = parseInt(value, 10);\n return isNaN(num) ? null : num;\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n // Try parsing as duration string FIRST (e.g., \"1h 31m 17s\" or \"19m 58s\")\n const match = value.match(/(?:(\\d+)h)?(?:\\s*(\\d+)m)?(?:\\s*(\\d+)s)?/);\n if (match && (match[1] || match[2] || match[3])) {\n const hours = parseInt(match[1] || \"0\");\n const minutes = parseInt(match[2] || \"0\");\n const secs = parseInt(match[3] || \"0\");\n return hours * 3600 + minutes * 60 + secs;\n }\n\n // Fallback to parsing as plain seconds\n const seconds = parseFloat(value);\n if (!isNaN(seconds)) return seconds;\n\n return null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gcode.parser.js","names":["fs"],"sources":["../../../src/utils/parsers/gcode.parser.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as readline from \"node:readline\";\nimport { createReadStream } from \"node:fs\";\nimport { GCodeMetadata } from \"@/entities/print-job.entity\";\nimport { convertQoiToPng } from \"../bgcode/bgcode-thumbnail.parser\";\nimport { ParsedThumbnail } from \"./parser.types\";\n\ninterface GCodeParseResult {\n raw: {\n _thumbnails?: ParsedThumbnail[];\n metadata: Record<string, string>;\n };\n normalized: GCodeMetadata;\n}\n\n/**\n * G-code parser for extracting metadata from .gcode files\n * Reads first and last N lines to extract slicer metadata\n */\nexport class GCodeParser {\n private readonly maxHeaderLinesToRead = 500; // Read from start\n private readonly maxFooterLinesToRead = 500; // Read from end\n\n async parse(filePath: string): Promise<GCodeParseResult> {\n const stats = await fs.stat(filePath);\n const fileName = filePath.split(/[/\\\\]/).pop() || filePath;\n\n const metadata = await this.extractMetadata(filePath);\n const thumbnails = await this.extractThumbnails(filePath);\n\n const normalized: GCodeMetadata = {\n fileName,\n fileFormat: \"gcode\",\n fileSize: stats.size,\n gcodePrintTimeSeconds: this.parseTime(\n metadata.estimated_printing_time_normal_mode || metadata.estimated_printing_time || metadata.print_time,\n ),\n nozzleDiameterMm: this.parseFloat(metadata.nozzle_diameter),\n filamentDiameterMm: this.parseFloat(metadata.filament_diameter),\n filamentDensityGramsCm3: this.parseFloat(metadata.filament_density),\n filamentUsedMm: this.parseFloat(metadata.filament_used_mm),\n filamentUsedCm3: this.parseFloat(metadata.filament_used_cm3),\n filamentUsedGrams: this.parseFloat(metadata.filament_used_g),\n totalFilamentUsedGrams: this.parseFloat(metadata.total_filament_used_g || metadata.filament_used_g),\n layerHeight: this.parseFloat(metadata.layer_height),\n firstLayerHeight: this.parseFloat(metadata.first_layer_height || metadata.initial_layer_height),\n bedTemperature: this.parseFloat(metadata.bed_temperature || metadata.first_layer_bed_temperature),\n nozzleTemperature: this.parseFloat(metadata.temperature || metadata.first_layer_temperature),\n fillDensity: metadata.fill_density || null,\n filamentType: metadata.filament_type || null,\n printerModel: metadata.printer_model || metadata.printer_name || null,\n slicerVersion: metadata.generated_by || metadata.slicer_version || null,\n maxLayerZ: this.parseFloat(metadata.max_layer_z),\n totalLayers: this.parseInt(metadata.total_layers) || this.parseInt(metadata.layer_count),\n generatedBy: metadata.generated_by,\n thumbnails:\n thumbnails.length > 0\n ? thumbnails.map((t) => ({\n width: t.width,\n height: t.height,\n format: t.format,\n dataLength: t.data?.length || 0,\n }))\n : undefined,\n };\n\n return {\n raw: {\n _thumbnails: thumbnails,\n metadata,\n },\n normalized,\n };\n }\n\n private async extractMetadata(filePath: string): Promise<Record<string, string>> {\n const metadata: Record<string, string> = {};\n\n // Read from start of file (header often has thumbnails and basic info)\n await this.extractMetadataFromStart(filePath, metadata);\n\n // Read from end of file (footer often has summary metadata - filament, time, etc.)\n await this.extractMetadataFromEnd(filePath, metadata);\n\n return metadata;\n }\n\n private async extractMetadataFromStart(filePath: string, metadata: Record<string, string>): Promise<void> {\n let linesRead = 0;\n\n const fileStream = createReadStream(filePath);\n const rl = readline.createInterface({\n input: fileStream,\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n if (linesRead >= this.maxHeaderLinesToRead) break;\n linesRead++;\n\n this.parseMetadataLine(line, metadata);\n }\n\n rl.close();\n fileStream.close();\n }\n\n private async extractMetadataFromEnd(filePath: string, metadata: Record<string, string>): Promise<void> {\n // Read last N lines efficiently\n const stats = await fs.stat(filePath);\n const fileSize = stats.size;\n\n // Estimate bytes to read (assume ~50 bytes per line avg)\n const estimatedBytes = this.maxFooterLinesToRead * 50;\n const startPosition = Math.max(0, fileSize - estimatedBytes);\n\n const fileHandle = await fs.open(filePath, \"r\");\n try {\n const buffer = Buffer.alloc(estimatedBytes);\n const { bytesRead } = await fileHandle.read(buffer, 0, estimatedBytes, startPosition);\n const text = buffer.toString(\"utf8\", 0, bytesRead);\n const lines = text.split(\"\\n\");\n\n // Process footer lines (summary metadata often here)\n for (const line of lines) {\n this.parseMetadataLine(line, metadata);\n }\n } finally {\n await fileHandle.close();\n }\n }\n\n private parseMetadataLine(line: string, metadata: Record<string, string>): void {\n // Skip non-comment lines\n if (!line.startsWith(\";\")) return;\n\n // Special case: \"; generated by PrusaSlicer X.X.X on ...\"\n const generatedByMatch = line.match(/^;\\s*generated by\\s+([^\\s]+)/i);\n if (generatedByMatch && !metadata.generated_by) {\n metadata.generated_by = generatedByMatch[1];\n return;\n }\n\n // Parse PrusaSlicer/SuperSlicer format: \"; key = value\"\n const prusaMatch = line.match(/^;\\s*([^=]+?)\\s*=\\s*(.+)$/);\n if (prusaMatch) {\n let key = prusaMatch[1].trim().toLowerCase().replace(/\\s+/g, \"_\");\n let value = prusaMatch[2].trim();\n\n // Normalize bracketed units: \"filament used [mm]\" -> \"filament_used_mm\"\n key = key.replace(/\\[([^\\]]+)\\]/g, (_, unit) => \"_\" + unit.replace(/\\^/g, \"\"));\n // Normalize parentheses: \"estimated printing time (normal mode)\" -> \"estimated_printing_time_normal_mode\"\n key = key.replace(/\\(([^)]+)\\)/g, (_, content) => \"_\" + content.replace(/\\s+/g, \"_\"));\n // Normalize multiple underscores\n key = key.replace(/_+/g, \"_\");\n\n // Don't overwrite if already set (header takes precedence)\n if (!metadata[key]) {\n metadata[key] = value.trim();\n }\n return;\n }\n\n // Parse Cura format: \";KEY:value\"\n const curaMatch = line.match(/^;([A-Z_]+):(.+)$/);\n if (curaMatch) {\n const [, key, value] = curaMatch;\n const normalizedKey = key.toLowerCase();\n if (!metadata[normalizedKey]) {\n metadata[normalizedKey] = value.trim();\n }\n return;\n }\n\n // Parse Simplify3D format: \"; key: value\"\n const s3dMatch = line.match(/^;\\s*([^:]+?):\\s*(.+)$/);\n if (s3dMatch) {\n const [, key, value] = s3dMatch;\n const normalizedKey = key.trim().toLowerCase().replace(/\\s+/g, \"_\");\n if (!metadata[normalizedKey]) {\n metadata[normalizedKey] = value.trim();\n }\n }\n }\n\n private async extractThumbnails(filePath: string): Promise<ParsedThumbnail[]> {\n const thumbnails: ParsedThumbnail[] = [];\n let linesRead = 0;\n let inThumbnail = false;\n let thumbnailData: string[] = [];\n let currentWidth = 0;\n let currentHeight = 0;\n let currentFormat = \"PNG\";\n\n const fileStream = createReadStream(filePath);\n const rl = readline.createInterface({\n input: fileStream,\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n if (linesRead >= this.maxHeaderLinesToRead && !inThumbnail) break;\n linesRead++;\n\n // PrusaSlicer thumbnail format\n // Format 1: ; thumbnail begin 313x173 57100 (width x height dataLength)\n // Format 2: ; thumbnail begin 313x173 PNG (width x height format)\n const thumbnailStart = line.match(/;\\s*thumbnail begin (\\d+)x(\\d+)\\s*(\\w+)?/i);\n if (thumbnailStart) {\n inThumbnail = true;\n currentWidth = parseInt(thumbnailStart[1]);\n currentHeight = parseInt(thumbnailStart[2]);\n\n // Third parameter could be format (PNG/JPG/QOI) or data length (number)\n const thirdParam = thumbnailStart[3];\n if (thirdParam && /^(PNG|JPG|JPEG|QOI)$/i.test(thirdParam)) {\n currentFormat = thirdParam.toUpperCase();\n } else {\n // If it's a number or not specified, default to PNG\n currentFormat = \"PNG\";\n }\n\n thumbnailData = [];\n continue;\n }\n\n if (inThumbnail) {\n if (line.match(/;\\s*thumbnail end/i)) {\n let base64Data = thumbnailData.join(\"\");\n let format = currentFormat.toUpperCase();\n\n if (format === \"QOI\") {\n try {\n const qoiBuffer = Buffer.from(base64Data, \"base64\");\n const pngBuffer = convertQoiToPng(qoiBuffer);\n base64Data = pngBuffer.toString(\"base64\");\n format = \"PNG\";\n } catch {\n // Keep original QOI if conversion fails\n }\n }\n\n thumbnails.push({\n width: currentWidth,\n height: currentHeight,\n format,\n data: base64Data,\n });\n inThumbnail = false;\n thumbnailData = [];\n } else if (line.startsWith(\";\")) {\n const data = line.substring(1).trim();\n if (data) {\n thumbnailData.push(data);\n }\n }\n }\n }\n\n rl.close();\n fileStream.close();\n\n return thumbnails;\n }\n\n private parseFloat(value: string | undefined): number | null {\n if (!value) return null;\n const num = parseFloat(value);\n return isNaN(num) ? null : num;\n }\n\n private parseInt(value: string | undefined): number | null {\n if (!value) return null;\n const num = parseInt(value, 10);\n return isNaN(num) ? null : num;\n }\n\n private parseTime(value: string | undefined): number | null {\n if (!value) return null;\n\n // Try parsing as duration string FIRST (e.g., \"1h 31m 17s\" or \"19m 58s\")\n const match = value.match(/(?:(\\d+)h)?(?:\\s*(\\d+)m)?(?:\\s*(\\d+)s)?/);\n if (match && (match[1] || match[2] || match[3])) {\n const hours = parseInt(match[1] || \"0\");\n const minutes = parseInt(match[2] || \"0\");\n const secs = parseInt(match[3] || \"0\");\n return hours * 3600 + minutes * 60 + secs;\n }\n\n // Fallback to parsing as plain seconds\n const seconds = parseFloat(value);\n if (!isNaN(seconds)) return seconds;\n\n return null;\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAa,cAAb,MAAyB;CACvB,uBAAwC;CACxC,uBAAwC;CAExC,MAAM,MAAM,UAA6C;EACvD,MAAM,QAAQ,MAAMA,KAAG,KAAK,SAAS;EACrC,MAAM,WAAW,SAAS,MAAM,QAAQ,CAAC,KAAK,IAAI;EAElD,MAAM,WAAW,MAAM,KAAK,gBAAgB,SAAS;EACrD,MAAM,aAAa,MAAM,KAAK,kBAAkB,SAAS;EAEzD,MAAM,aAA4B;GAChC;GACA,YAAY;GACZ,UAAU,MAAM;GAChB,uBAAuB,KAAK,UAC1B,SAAS,uCAAuC,SAAS,2BAA2B,SAAS,WAC9F;GACD,kBAAkB,KAAK,WAAW,SAAS,gBAAgB;GAC3D,oBAAoB,KAAK,WAAW,SAAS,kBAAkB;GAC/D,yBAAyB,KAAK,WAAW,SAAS,iBAAiB;GACnE,gBAAgB,KAAK,WAAW,SAAS,iBAAiB;GAC1D,iBAAiB,KAAK,WAAW,SAAS,kBAAkB;GAC5D,mBAAmB,KAAK,WAAW,SAAS,gBAAgB;GAC5D,wBAAwB,KAAK,WAAW,SAAS,yBAAyB,SAAS,gBAAgB;GACnG,aAAa,KAAK,WAAW,SAAS,aAAa;GACnD,kBAAkB,KAAK,WAAW,SAAS,sBAAsB,SAAS,qBAAqB;GAC/F,gBAAgB,KAAK,WAAW,SAAS,mBAAmB,SAAS,4BAA4B;GACjG,mBAAmB,KAAK,WAAW,SAAS,eAAe,SAAS,wBAAwB;GAC5F,aAAa,SAAS,gBAAgB;GACtC,cAAc,SAAS,iBAAiB;GACxC,cAAc,SAAS,iBAAiB,SAAS,gBAAgB;GACjE,eAAe,SAAS,gBAAgB,SAAS,kBAAkB;GACnE,WAAW,KAAK,WAAW,SAAS,YAAY;GAChD,aAAa,KAAK,SAAS,SAAS,aAAa,IAAI,KAAK,SAAS,SAAS,YAAY;GACxF,aAAa,SAAS;GACtB,YACE,WAAW,SAAS,IAChB,WAAW,KAAK,OAAO;IACrB,OAAO,EAAE;IACT,QAAQ,EAAE;IACV,QAAQ,EAAE;IACV,YAAY,EAAE,MAAM,UAAU;IAC/B,EAAE,GACH,KAAA;GACP;EAED,OAAO;GACL,KAAK;IACH,aAAa;IACb;IACD;GACD;GACD;;CAGH,MAAc,gBAAgB,UAAmD;EAC/E,MAAM,WAAmC,EAAE;EAG3C,MAAM,KAAK,yBAAyB,UAAU,SAAS;EAGvD,MAAM,KAAK,uBAAuB,UAAU,SAAS;EAErD,OAAO;;CAGT,MAAc,yBAAyB,UAAkB,UAAiD;EACxG,IAAI,YAAY;EAEhB,MAAM,aAAa,iBAAiB,SAAS;EAC7C,MAAM,KAAK,SAAS,gBAAgB;GAClC,OAAO;GACP,WAAW;GACZ,CAAC;EAEF,WAAW,MAAM,QAAQ,IAAI;GAC3B,IAAI,aAAa,KAAK,sBAAsB;GAC5C;GAEA,KAAK,kBAAkB,MAAM,SAAS;;EAGxC,GAAG,OAAO;EACV,WAAW,OAAO;;CAGpB,MAAc,uBAAuB,UAAkB,UAAiD;EAGtG,MAAM,YAAW,MADGA,KAAG,KAAK,SAAS,EACd;EAGvB,MAAM,iBAAiB,KAAK,uBAAuB;EACnD,MAAM,gBAAgB,KAAK,IAAI,GAAG,WAAW,eAAe;EAE5D,MAAM,aAAa,MAAMA,KAAG,KAAK,UAAU,IAAI;EAC/C,IAAI;GACF,MAAM,SAAS,OAAO,MAAM,eAAe;GAC3C,MAAM,EAAE,cAAc,MAAM,WAAW,KAAK,QAAQ,GAAG,gBAAgB,cAAc;GAErF,MAAM,QADO,OAAO,SAAS,QAAQ,GAAG,UACtB,CAAC,MAAM,KAAK;GAG9B,KAAK,MAAM,QAAQ,OACjB,KAAK,kBAAkB,MAAM,SAAS;YAEhC;GACR,MAAM,WAAW,OAAO;;;CAI5B,kBAA0B,MAAc,UAAwC;EAE9E,IAAI,CAAC,KAAK,WAAW,IAAI,EAAE;EAG3B,MAAM,mBAAmB,KAAK,MAAM,gCAAgC;EACpE,IAAI,oBAAoB,CAAC,SAAS,cAAc;GAC9C,SAAS,eAAe,iBAAiB;GACzC;;EAIF,MAAM,aAAa,KAAK,MAAM,4BAA4B;EAC1D,IAAI,YAAY;GACd,IAAI,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,QAAQ,QAAQ,IAAI;GACjE,IAAI,QAAQ,WAAW,GAAG,MAAM;GAGhC,MAAM,IAAI,QAAQ,kBAAkB,GAAG,SAAS,MAAM,KAAK,QAAQ,OAAO,GAAG,CAAC;GAE9E,MAAM,IAAI,QAAQ,iBAAiB,GAAG,YAAY,MAAM,QAAQ,QAAQ,QAAQ,IAAI,CAAC;GAErF,MAAM,IAAI,QAAQ,OAAO,IAAI;GAG7B,IAAI,CAAC,SAAS,MACZ,SAAS,OAAO,MAAM,MAAM;GAE9B;;EAIF,MAAM,YAAY,KAAK,MAAM,oBAAoB;EACjD,IAAI,WAAW;GACb,MAAM,GAAG,KAAK,SAAS;GACvB,MAAM,gBAAgB,IAAI,aAAa;GACvC,IAAI,CAAC,SAAS,gBACZ,SAAS,iBAAiB,MAAM,MAAM;GAExC;;EAIF,MAAM,WAAW,KAAK,MAAM,yBAAyB;EACrD,IAAI,UAAU;GACZ,MAAM,GAAG,KAAK,SAAS;GACvB,MAAM,gBAAgB,IAAI,MAAM,CAAC,aAAa,CAAC,QAAQ,QAAQ,IAAI;GACnE,IAAI,CAAC,SAAS,gBACZ,SAAS,iBAAiB,MAAM,MAAM;;;CAK5C,MAAc,kBAAkB,UAA8C;EAC5E,MAAM,aAAgC,EAAE;EACxC,IAAI,YAAY;EAChB,IAAI,cAAc;EAClB,IAAI,gBAA0B,EAAE;EAChC,IAAI,eAAe;EACnB,IAAI,gBAAgB;EACpB,IAAI,gBAAgB;EAEpB,MAAM,aAAa,iBAAiB,SAAS;EAC7C,MAAM,KAAK,SAAS,gBAAgB;GAClC,OAAO;GACP,WAAW;GACZ,CAAC;EAEF,WAAW,MAAM,QAAQ,IAAI;GAC3B,IAAI,aAAa,KAAK,wBAAwB,CAAC,aAAa;GAC5D;GAKA,MAAM,iBAAiB,KAAK,MAAM,4CAA4C;GAC9E,IAAI,gBAAgB;IAClB,cAAc;IACd,eAAe,SAAS,eAAe,GAAG;IAC1C,gBAAgB,SAAS,eAAe,GAAG;IAG3C,MAAM,aAAa,eAAe;IAClC,IAAI,cAAc,wBAAwB,KAAK,WAAW,EACxD,gBAAgB,WAAW,aAAa;SAGxC,gBAAgB;IAGlB,gBAAgB,EAAE;IAClB;;GAGF,IAAI;QACE,KAAK,MAAM,qBAAqB,EAAE;KACpC,IAAI,aAAa,cAAc,KAAK,GAAG;KACvC,IAAI,SAAS,cAAc,aAAa;KAExC,IAAI,WAAW,OACb,IAAI;MAGF,aADkB,gBADA,OAAO,KAAK,YAAY,SACC,CACrB,CAAC,SAAS,SAAS;MACzC,SAAS;aACH;KAKV,WAAW,KAAK;MACd,OAAO;MACP,QAAQ;MACR;MACA,MAAM;MACP,CAAC;KACF,cAAc;KACd,gBAAgB,EAAE;WACb,IAAI,KAAK,WAAW,IAAI,EAAE;KAC/B,MAAM,OAAO,KAAK,UAAU,EAAE,CAAC,MAAM;KACrC,IAAI,MACF,cAAc,KAAK,KAAK;;;;EAMhC,GAAG,OAAO;EACV,WAAW,OAAO;EAElB,OAAO;;CAGT,WAAmB,OAA0C;EAC3D,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,MAAM,WAAW,MAAM;EAC7B,OAAO,MAAM,IAAI,GAAG,OAAO;;CAG7B,SAAiB,OAA0C;EACzD,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,MAAM,SAAS,OAAO,GAAG;EAC/B,OAAO,MAAM,IAAI,GAAG,OAAO;;CAG7B,UAAkB,OAA0C;EAC1D,IAAI,CAAC,OAAO,OAAO;EAGnB,MAAM,QAAQ,MAAM,MAAM,0CAA0C;EACpE,IAAI,UAAU,MAAM,MAAM,MAAM,MAAM,MAAM,KAAK;GAC/C,MAAM,QAAQ,SAAS,MAAM,MAAM,IAAI;GACvC,MAAM,UAAU,SAAS,MAAM,MAAM,IAAI;GACzC,MAAM,OAAO,SAAS,MAAM,MAAM,IAAI;GACtC,OAAO,QAAQ,OAAO,UAAU,KAAK;;EAIvC,MAAM,UAAU,WAAW,MAAM;EACjC,IAAI,CAAC,MAAM,QAAQ,EAAE,OAAO;EAE5B,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pretty-print.utils.js","names":[],"sources":["../../src/utils/pretty-print.utils.ts"],"sourcesContent":["export function PP(input: any) {\n return JSON.stringify(input, null, 2);\n}\n\nexport function PL(input: any) {\n console.log(PP(input));\n}\n"],"mappings":";AAAA,SAAgB,GAAG,OAAY;
|
|
1
|
+
{"version":3,"file":"pretty-print.utils.js","names":[],"sources":["../../src/utils/pretty-print.utils.ts"],"sourcesContent":["export function PP(input: any) {\n return JSON.stringify(input, null, 2);\n}\n\nexport function PL(input: any) {\n console.log(PP(input));\n}\n"],"mappings":";AAAA,SAAgB,GAAG,OAAY;CAC7B,OAAO,KAAK,UAAU,OAAO,MAAM,EAAE;;AAGvC,SAAgB,GAAG,OAAY;CAC7B,QAAQ,IAAI,GAAG,MAAM,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semver.utils.js","names":[],"sources":["../../src/utils/semver.utils.ts"],"sourcesContent":["import { compare } from \"semver\";\n\nexport function checkVersionSatisfiesMinimum(newVersion: string, minimumVersion: string) {\n const comparison = compare(newVersion, minimumVersion);\n // -1 => newVersion less than\n // 0 => newVersion equal to\n // 1 => newVersion greater than\n return comparison !== -1;\n}\n\nexport function getMaximumOfVersionsSafe(v1: string, v2?: string) {\n if (!v2) return v1;\n const comparison = compare(v1, v2);\n if (comparison === 0) return v1;\n if (comparison === -1) return v2;\n return v1;\n}\n"],"mappings":";;AAEA,SAAgB,6BAA6B,YAAoB,gBAAwB;
|
|
1
|
+
{"version":3,"file":"semver.utils.js","names":[],"sources":["../../src/utils/semver.utils.ts"],"sourcesContent":["import { compare } from \"semver\";\n\nexport function checkVersionSatisfiesMinimum(newVersion: string, minimumVersion: string) {\n const comparison = compare(newVersion, minimumVersion);\n // -1 => newVersion less than\n // 0 => newVersion equal to\n // 1 => newVersion greater than\n return comparison !== -1;\n}\n\nexport function getMaximumOfVersionsSafe(v1: string, v2?: string) {\n if (!v2) return v1;\n const comparison = compare(v1, v2);\n if (comparison === 0) return v1;\n if (comparison === -1) return v2;\n return v1;\n}\n"],"mappings":";;AAEA,SAAgB,6BAA6B,YAAoB,gBAAwB;CAKvF,OAJmB,QAAQ,YAAY,eAItB,KAAK;;AAGxB,SAAgB,yBAAyB,IAAY,IAAa;CAChE,IAAI,CAAC,IAAI,OAAO;CAChB,MAAM,aAAa,QAAQ,IAAI,GAAG;CAClC,IAAI,eAAe,GAAG,OAAO;CAC7B,IAAI,eAAe,IAAI,OAAO;CAC9B,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.js","names":[],"sources":["../../../src/utils/swagger/decorators.ts"],"sourcesContent":["// src/decorators/api.decorator.ts\nimport \"reflect-metadata\";\n\nexport const API_METADATA_KEY = \"Router Config\";\n\nexport interface ApiPropertyOptions {\n description?: string;\n required?: boolean;\n type?: any;\n isArray?: boolean;\n example?: any;\n}\n\nexport interface ApiOperationOptions {\n summary?: string;\n description?: string;\n responses?: Record<string, any>;\n}\n\nexport function ApiProperty(options: ApiPropertyOptions = {}) {\n return function (target: any, propertyKey: string) {\n const metadata = Reflect.getMetadata(API_METADATA_KEY, target.constructor) || {};\n metadata[propertyKey] = options;\n Reflect.defineMetadata(API_METADATA_KEY, metadata, target.constructor);\n };\n}\n\nexport function ApiOperation(options: ApiOperationOptions = {}) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const metadata = Reflect.getMetadata(API_METADATA_KEY, target.constructor) || {};\n metadata[`${propertyKey}:operation`] = options;\n Reflect.defineMetadata(API_METADATA_KEY, metadata, target.constructor);\n };\n}\n"],"mappings":";;AAGA,MAAa,mBAAmB;AAgBhC,SAAgB,YAAY,UAA8B,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"decorators.js","names":[],"sources":["../../../src/utils/swagger/decorators.ts"],"sourcesContent":["// src/decorators/api.decorator.ts\nimport \"reflect-metadata\";\n\nexport const API_METADATA_KEY = \"Router Config\";\n\nexport interface ApiPropertyOptions {\n description?: string;\n required?: boolean;\n type?: any;\n isArray?: boolean;\n example?: any;\n}\n\nexport interface ApiOperationOptions {\n summary?: string;\n description?: string;\n responses?: Record<string, any>;\n}\n\nexport function ApiProperty(options: ApiPropertyOptions = {}) {\n return function (target: any, propertyKey: string) {\n const metadata = Reflect.getMetadata(API_METADATA_KEY, target.constructor) || {};\n metadata[propertyKey] = options;\n Reflect.defineMetadata(API_METADATA_KEY, metadata, target.constructor);\n };\n}\n\nexport function ApiOperation(options: ApiOperationOptions = {}) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const metadata = Reflect.getMetadata(API_METADATA_KEY, target.constructor) || {};\n metadata[`${propertyKey}:operation`] = options;\n Reflect.defineMetadata(API_METADATA_KEY, metadata, target.constructor);\n };\n}\n"],"mappings":";;AAGA,MAAa,mBAAmB;AAgBhC,SAAgB,YAAY,UAA8B,EAAE,EAAE;CAC5D,OAAO,SAAU,QAAa,aAAqB;EACjD,MAAM,WAAW,QAAQ,YAAA,iBAA8B,OAAO,YAAY,IAAI,EAAE;EAChF,SAAS,eAAe;EACxB,QAAQ,eAAe,kBAAkB,UAAU,OAAO,YAAY;;;AAI1E,SAAgB,aAAa,UAA+B,EAAE,EAAE;CAC9D,OAAO,SAAU,QAAa,aAAqB,YAAgC;EACjF,MAAM,WAAW,QAAQ,YAAA,iBAA8B,OAAO,YAAY,IAAI,EAAE;EAChF,SAAS,GAAG,YAAY,eAAe;EACvC,QAAQ,eAAe,kBAAkB,UAAU,OAAO,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","names":[],"sources":["../../../src/utils/swagger/generator.ts"],"sourcesContent":["import { OpenAPIObject, PathItemObject, SchemaObject } from \"openapi3-ts/oas31\";\nimport { API_METADATA_KEY } from \"@/utils/swagger/decorators\";\nimport { findControllers, FindControllersResult } from \"awilix-express\";\nimport { MethodName, type IRouteConfig } from \"awilix-router-core/lib/state-util\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { getDirname } from \"@/utils/fs.utils\";\n\nexport class SwaggerGenerator {\n private readonly logger: LoggerService;\n\n constructor(logger: LoggerService) {\n this.logger = logger;\n }\n private readonly openApiDoc: OpenAPIObject = {\n openapi: \"3.1.0\",\n info: {\n title: \"FDM Monster API\",\n version: process.env.npm_package_version || \"2.0.0\",\n description:\n \"FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.\",\n license: {\n name: \"AGPL-3.0-or-later\",\n url: \"https://www.gnu.org/licenses/agpl-3.0.en.html\",\n },\n contact: {\n name: \"FDM Monster GitHub\",\n url: \"https://github.com/fdm-monster/fdm-monster\",\n },\n },\n servers: [\n {\n url: \"/api\",\n description: \"API Server\",\n },\n ],\n paths: {},\n components: {\n schemas: {},\n securitySchemes: {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description: \"Enter your JWT token\",\n },\n },\n },\n security: [\n {\n bearerAuth: [],\n },\n ],\n };\n\n public async generate(): Promise<OpenAPIObject> {\n try {\n const routePath = \"../../controllers\";\n const discoveredControllers = await findControllers(`${routePath}/*.controller.js`, {\n cwd: getDirname(import.meta.url),\n ignore: [\"**/*.map\", \"**/*.d.ts\"],\n absolute: true,\n esModules: true,\n });\n for (const registration of discoveredControllers) {\n await this.processController(registration);\n }\n this.logger.log(`Generated OpenAPI spec with ${Object.keys(this.openApiDoc.paths || {}).length} paths`);\n } catch (error) {\n this.logger.error(\"Failed to generate swagger specification\", error);\n }\n\n return this.openApiDoc;\n }\n\n private async processController(prototype: Awaited<FindControllersResult>[number]) {\n for (const [methodName, methodConfig] of prototype.state.methods) {\n if (methodConfig.paths.length > 0) {\n await this.processMethod(prototype, prototype.state.root, methodName, methodConfig);\n }\n }\n }\n\n private async processMethod(\n controller: Awaited<FindControllersResult>[number],\n root: IRouteConfig,\n methodName: MethodName,\n methodConfig: IRouteConfig,\n ) {\n if (!methodName) return;\n\n const method = methodName.toString();\n const name = method.toLowerCase();\n for (let methodPath of methodConfig.paths) {\n methodPath = root.paths[0] + methodPath;\n // Convert Express path format (:id) to OpenAPI format ({id})\n methodPath = methodPath.replaceAll(/:([a-zA-Z0-9_]+)/g, \"{$1}\");\n\n const metadata = Reflect.getMetadata(API_METADATA_KEY, controller.target);\n for (const verb of methodConfig.verbs) {\n const key = `${name}:operation`;\n const description = metadata?.hasOwnProperty(key) ? metadata[key] : null;\n\n const httpMethod = verb.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"delete\"\n | \"patch\"\n | \"options\"\n | \"head\"\n | \"trace\";\n\n const operationObject = {\n tags: [controller.target.name],\n summary: description?.summary ?? method,\n description: description?.description ?? \"\",\n responses: description?.responses ?? {\n \"200\": {\n description: \"Successful response\",\n },\n },\n } as any;\n\n // Extract path parameters from the path (e.g., {id}, {userId}, etc.)\n operationObject.parameters = this.extractPathParameters(methodPath);\n\n // Process method parameters and response type\n const paramTypes = Reflect.getMetadata(\"design:paramtypes\", controller.target, name);\n const returnType = Reflect.getMetadata(\"design:returntype\", controller.target, name);\n\n if (paramTypes) {\n const additionalParams = this.processParameterTypes(paramTypes);\n operationObject.parameters = [...operationObject.parameters, ...additionalParams];\n }\n\n if (returnType && operationObject.responses?.[\"200\"]) {\n operationObject.responses[\"200\"].content = {\n \"application/json\": {\n schema: this.processReturnType(returnType),\n },\n };\n }\n\n const operation: PathItemObject = {\n [httpMethod]: operationObject,\n };\n\n this.openApiDoc.paths ??= {};\n\n this.openApiDoc.paths[methodPath] = {\n ...this.openApiDoc.paths[methodPath],\n ...operation,\n };\n }\n }\n }\n\n private extractPathParameters(path: string): any[] {\n const paramRegex = /\\{([a-zA-Z0-9_]+)\\}/g;\n const params: any[] = [];\n let match;\n\n while ((match = paramRegex.exec(path)) !== null) {\n const paramName = match[1];\n params.push({\n name: paramName,\n in: \"path\",\n required: true,\n schema: {\n type: \"string\",\n },\n description: `The ${paramName} parameter`,\n });\n }\n\n return params;\n }\n\n private processParameterTypes(types: any[]): any[] {\n return types.map((type) => this.createParameterDefinition(type));\n }\n\n private processReturnType(type: any): SchemaObject {\n return this.createSchemaDefinition(type);\n }\n\n private createSchemaDefinition(type: any): SchemaObject {\n // Implementation for creating OpenAPI schema from TypeScript type\n // This is a simplified version - you'll want to expand this\n return {\n type: \"object\",\n properties: this.getTypeProperties(type),\n };\n }\n\n private createParameterDefinition(type: any): any {\n // Implementation for creating parameter definitions\n return {\n in: \"body\",\n schema: this.createSchemaDefinition(type),\n };\n }\n\n private getTypeProperties(type: any): Record<string, any> {\n const properties: Record<string, any> = {};\n const metadata = Reflect.getMetadata(API_METADATA_KEY, type) || {};\n\n Object.keys(metadata).forEach((key) => {\n properties[key] = {\n type: this.getPropertyType(metadata[key].type),\n description: metadata[key].description,\n example: metadata[key].example,\n };\n });\n\n return properties;\n }\n\n private getPropertyType(type: any): string {\n const typeMap: Record<string, string> = {\n String: \"string\",\n Number: \"number\",\n Boolean: \"boolean\",\n Object: \"object\",\n Array: \"array\",\n };\n\n return typeMap[type?.name] || \"string\";\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generator.js","names":[],"sources":["../../../src/utils/swagger/generator.ts"],"sourcesContent":["import { OpenAPIObject, PathItemObject, SchemaObject } from \"openapi3-ts/oas31\";\nimport { API_METADATA_KEY } from \"@/utils/swagger/decorators\";\nimport { findControllers, FindControllersResult } from \"awilix-express\";\nimport { MethodName, type IRouteConfig } from \"awilix-router-core/lib/state-util\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { getDirname } from \"@/utils/fs.utils\";\n\nexport class SwaggerGenerator {\n private readonly logger: LoggerService;\n\n constructor(logger: LoggerService) {\n this.logger = logger;\n }\n private readonly openApiDoc: OpenAPIObject = {\n openapi: \"3.1.0\",\n info: {\n title: \"FDM Monster API\",\n version: process.env.npm_package_version || \"2.0.0\",\n description:\n \"FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.\",\n license: {\n name: \"AGPL-3.0-or-later\",\n url: \"https://www.gnu.org/licenses/agpl-3.0.en.html\",\n },\n contact: {\n name: \"FDM Monster GitHub\",\n url: \"https://github.com/fdm-monster/fdm-monster\",\n },\n },\n servers: [\n {\n url: \"/api\",\n description: \"API Server\",\n },\n ],\n paths: {},\n components: {\n schemas: {},\n securitySchemes: {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description: \"Enter your JWT token\",\n },\n },\n },\n security: [\n {\n bearerAuth: [],\n },\n ],\n };\n\n public async generate(): Promise<OpenAPIObject> {\n try {\n const routePath = \"../../controllers\";\n const discoveredControllers = await findControllers(`${routePath}/*.controller.js`, {\n cwd: getDirname(import.meta.url),\n ignore: [\"**/*.map\", \"**/*.d.ts\"],\n absolute: true,\n esModules: true,\n });\n for (const registration of discoveredControllers) {\n await this.processController(registration);\n }\n this.logger.log(`Generated OpenAPI spec with ${Object.keys(this.openApiDoc.paths || {}).length} paths`);\n } catch (error) {\n this.logger.error(\"Failed to generate swagger specification\", error);\n }\n\n return this.openApiDoc;\n }\n\n private async processController(prototype: Awaited<FindControllersResult>[number]) {\n for (const [methodName, methodConfig] of prototype.state.methods) {\n if (methodConfig.paths.length > 0) {\n await this.processMethod(prototype, prototype.state.root, methodName, methodConfig);\n }\n }\n }\n\n private async processMethod(\n controller: Awaited<FindControllersResult>[number],\n root: IRouteConfig,\n methodName: MethodName,\n methodConfig: IRouteConfig,\n ) {\n if (!methodName) return;\n\n const method = methodName.toString();\n const name = method.toLowerCase();\n for (let methodPath of methodConfig.paths) {\n methodPath = root.paths[0] + methodPath;\n // Convert Express path format (:id) to OpenAPI format ({id})\n methodPath = methodPath.replaceAll(/:([a-zA-Z0-9_]+)/g, \"{$1}\");\n\n const metadata = Reflect.getMetadata(API_METADATA_KEY, controller.target);\n for (const verb of methodConfig.verbs) {\n const key = `${name}:operation`;\n const description = metadata?.hasOwnProperty(key) ? metadata[key] : null;\n\n const httpMethod = verb.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"delete\"\n | \"patch\"\n | \"options\"\n | \"head\"\n | \"trace\";\n\n const operationObject = {\n tags: [controller.target.name],\n summary: description?.summary ?? method,\n description: description?.description ?? \"\",\n responses: description?.responses ?? {\n \"200\": {\n description: \"Successful response\",\n },\n },\n } as any;\n\n // Extract path parameters from the path (e.g., {id}, {userId}, etc.)\n operationObject.parameters = this.extractPathParameters(methodPath);\n\n // Process method parameters and response type\n const paramTypes = Reflect.getMetadata(\"design:paramtypes\", controller.target, name);\n const returnType = Reflect.getMetadata(\"design:returntype\", controller.target, name);\n\n if (paramTypes) {\n const additionalParams = this.processParameterTypes(paramTypes);\n operationObject.parameters = [...operationObject.parameters, ...additionalParams];\n }\n\n if (returnType && operationObject.responses?.[\"200\"]) {\n operationObject.responses[\"200\"].content = {\n \"application/json\": {\n schema: this.processReturnType(returnType),\n },\n };\n }\n\n const operation: PathItemObject = {\n [httpMethod]: operationObject,\n };\n\n this.openApiDoc.paths ??= {};\n\n this.openApiDoc.paths[methodPath] = {\n ...this.openApiDoc.paths[methodPath],\n ...operation,\n };\n }\n }\n }\n\n private extractPathParameters(path: string): any[] {\n const paramRegex = /\\{([a-zA-Z0-9_]+)\\}/g;\n const params: any[] = [];\n let match;\n\n while ((match = paramRegex.exec(path)) !== null) {\n const paramName = match[1];\n params.push({\n name: paramName,\n in: \"path\",\n required: true,\n schema: {\n type: \"string\",\n },\n description: `The ${paramName} parameter`,\n });\n }\n\n return params;\n }\n\n private processParameterTypes(types: any[]): any[] {\n return types.map((type) => this.createParameterDefinition(type));\n }\n\n private processReturnType(type: any): SchemaObject {\n return this.createSchemaDefinition(type);\n }\n\n private createSchemaDefinition(type: any): SchemaObject {\n // Implementation for creating OpenAPI schema from TypeScript type\n // This is a simplified version - you'll want to expand this\n return {\n type: \"object\",\n properties: this.getTypeProperties(type),\n };\n }\n\n private createParameterDefinition(type: any): any {\n // Implementation for creating parameter definitions\n return {\n in: \"body\",\n schema: this.createSchemaDefinition(type),\n };\n }\n\n private getTypeProperties(type: any): Record<string, any> {\n const properties: Record<string, any> = {};\n const metadata = Reflect.getMetadata(API_METADATA_KEY, type) || {};\n\n Object.keys(metadata).forEach((key) => {\n properties[key] = {\n type: this.getPropertyType(metadata[key].type),\n description: metadata[key].description,\n example: metadata[key].example,\n };\n });\n\n return properties;\n }\n\n private getPropertyType(type: any): string {\n const typeMap: Record<string, string> = {\n String: \"string\",\n Number: \"number\",\n Boolean: \"boolean\",\n Object: \"object\",\n Array: \"array\",\n };\n\n return typeMap[type?.name] || \"string\";\n }\n}\n"],"mappings":"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"}
|