diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8beeeab..8dd184c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,9 @@ permissions: contents: read env: + NODE_ENV: production JSR_DEPENDENCIES: "@cross/test @std/assert @std/fmt @frytg/logger" - NPM_DEPENDENCIES: "luxon sinon" + NPM_DEPENDENCIES: "luxon minio sinon" jobs: test-on-deno-and-lint: diff --git a/biome.json b/biome.json index 344e858..6dcecc7 100644 --- a/biome.json +++ b/biome.json @@ -2,7 +2,7 @@ "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", "files": { "maxSize": 2097152, - "ignore": ["*.vue", "vendor"] + "ignore": ["*.vue", "coverage", "node_modules", "vendor"] }, "formatter": { "indentStyle": "tab", diff --git a/check-required-env/CHANGELOG.md b/check-required-env/CHANGELOG.md new file mode 100644 index 0000000..2970573 --- /dev/null +++ b/check-required-env/CHANGELOG.md @@ -0,0 +1,13 @@ +# Check Required Env Changelog + +## 2024-12-17 - 0.1.0 + +- feat: add `getRequiredEnv` + +## 2024-11-27 - 0.0.1 + +- feat: add test for required env + +## 2024-11-22 - 0.0.1 + +- feat: add check required env diff --git a/check-required-env/check-required-env.ts b/check-required-env/check-required-env.ts index b09f5ce..bdfb5e0 100644 --- a/check-required-env/check-required-env.ts +++ b/check-required-env/check-required-env.ts @@ -5,8 +5,8 @@ import logger from '@frytg/logger' /** * Check if an environment variable is required and log an alert and exit if it is not set. * - * @param name - The name of the environment variable. - * @returns void + * @param {string} name - The name of the environment variable. + * @returns {void} * * @example * ```ts diff --git a/check-required-env/deno.jsonc b/check-required-env/deno.jsonc index 695a889..0ee7324 100644 --- a/check-required-env/deno.jsonc +++ b/check-required-env/deno.jsonc @@ -1,8 +1,11 @@ { "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@frytg/check-required-env", - "version": "0.0.1", - "exports": "./check-required-env.ts", + "version": "0.1.0", + "exports": { + ".": "./check-required-env.ts", + "./get": "./get-required-env.ts" + }, "imports": { "@frytg/logger": "jsr:@frytg/logger@0.0.2" } diff --git a/check-required-env/get-required-env.ts b/check-required-env/get-required-env.ts new file mode 100644 index 0000000..1199a48 --- /dev/null +++ b/check-required-env/get-required-env.ts @@ -0,0 +1,34 @@ +// load packages +import process from 'node:process' +import logger from '@frytg/logger' + +/** + * Access an environment variable and log an alert and exit if it is not set. + * + * @param {string} name - The name of the environment variable. + * @param {boolean} preferThrowError - Whether to throw an error if the environment variable is not set. + * @returns {string} The value of the environment variable. + * + * @example + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * + * getRequiredEnv('MY_IMPORTANT_ENV_VAR', false) + * ``` + */ +export const getRequiredEnv = (name: string, preferThrowError = true): string => { + const value = process.env[name] + + // return if the env variable is set + if (value !== undefined) return value + + // log and exit if not set + logger.log({ + level: 'alert', + message: `env ${name} is required`, + source: 'getRequiredEnv', + data: { name }, + }) + if (preferThrowError) throw new Error(`env ${name} is required`) + process.exit(1) +} diff --git a/deno.jsonc b/deno.jsonc index b1b0a35..741f0a8 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -16,6 +16,7 @@ }, "tasks": { "check": "deno fmt --check && deno lint && biome lint", + "dry-run": "deno publish --dry-run", "test": "deno test --allow-sys --allow-env --clean --coverage" }, "lint": { @@ -30,7 +31,7 @@ }, "exclude": ["**/*.md", "**/*.yml", "**/*.yaml"] }, - "workspace": ["./check-required-env", "./crypto", "./dates", "./logger"], + "workspace": ["./check-required-env", "./crypto", "./dates", "./logger", "./storage-s3"], "imports": { "@biomejs/biome": "npm:@biomejs/biome@^1.9.4", "@types/node": "npm:@types/node@^22.10.2", diff --git a/deno.lock b/deno.lock index 2dfcb9d..22a4a20 100644 --- a/deno.lock +++ b/deno.lock @@ -12,6 +12,7 @@ "npm:@types/node@*": "22.5.4", "npm:@types/node@^22.10.2": "22.10.2", "npm:luxon@^3.5.0": "3.5.0", + "npm:minio@^8.0.2": "8.0.2", "npm:sinon@^19.0.2": "19.0.2", "npm:winston@^3.17.0": "3.17.0" }, @@ -131,9 +132,40 @@ "@types/triple-beam@1.3.5": { "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "@zxing/text-encoding@0.9.0": { + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==" + }, "async@3.2.6": { "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "available-typed-arrays@1.0.7": { + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": [ + "possible-typed-array-names" + ] + }, + "block-stream2@2.1.0": { + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "dependencies": [ + "readable-stream" + ] + }, + "browser-or-node@2.1.1": { + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, + "buffer-crc32@1.0.0": { + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, + "call-bind@1.0.7": { + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": [ + "es-define-property", + "es-errors", + "function-bind", + "get-intrinsic", + "set-function-length" + ] + }, "color-convert@1.9.3": { "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": [ @@ -164,30 +196,136 @@ "text-hex" ] }, + "decode-uri-component@0.2.2": { + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" + }, + "define-data-property@1.1.4": { + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": [ + "es-define-property", + "es-errors", + "gopd" + ] + }, "diff@7.0.0": { "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==" }, "enabled@2.0.0": { "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "es-define-property@1.0.0": { + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": [ + "get-intrinsic" + ] + }, + "es-errors@1.3.0": { + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "eventemitter3@5.0.1": { + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "fast-xml-parser@4.5.0": { + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dependencies": [ + "strnum" + ] + }, "fecha@4.2.3": { "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "filter-obj@1.1.0": { + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==" + }, "fn.name@1.1.0": { "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "for-each@0.3.3": { + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": [ + "is-callable" + ] + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic@1.2.4": { + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": [ + "es-errors", + "function-bind", + "has-proto", + "has-symbols", + "hasown" + ] + }, + "gopd@1.0.1": { + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": [ + "get-intrinsic" + ] + }, "has-flag@4.0.0": { "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "has-property-descriptors@1.0.2": { + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": [ + "es-define-property" + ] + }, + "has-proto@1.0.3": { + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols@1.0.3": { + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag@1.0.2": { + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": [ + "has-symbols" + ] + }, + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": [ + "function-bind" + ] + }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ipaddr.js@2.2.0": { + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==" + }, + "is-arguments@1.1.1": { + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": [ + "call-bind", + "has-tostringtag" + ] + }, "is-arrayish@0.3.2": { "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "is-callable@1.2.7": { + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-generator-function@1.0.10": { + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": [ + "has-tostringtag" + ] + }, "is-stream@2.0.1": { "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, + "is-typed-array@1.1.13": { + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": [ + "which-typed-array" + ] + }, "just-extend@6.2.0": { "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" }, @@ -197,6 +335,9 @@ "lodash.get@4.4.2": { "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "logform@2.7.0": { "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dependencies": [ @@ -211,6 +352,34 @@ "luxon@3.5.0": { "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, + "mime-db@1.52.0": { + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types@2.1.35": { + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": [ + "mime-db" + ] + }, + "minio@8.0.2": { + "integrity": "sha512-7ipWbtgzzboctf+McK+2cXwCrNOhuboTA/O1g9iWa0gH8R4GkeyFWwk12aVDEHdzjPiG8wxnjwfHS7pgraKuHw==", + "dependencies": [ + "async", + "block-stream2", + "browser-or-node", + "buffer-crc32", + "eventemitter3", + "fast-xml-parser", + "ipaddr.js", + "lodash", + "mime-types", + "query-string", + "stream-json", + "through2", + "web-encoding", + "xml2js" + ] + }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, @@ -233,6 +402,18 @@ "path-to-regexp@8.2.0": { "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" }, + "possible-typed-array-names@1.0.0": { + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" + }, + "query-string@7.1.3": { + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": [ + "decode-uri-component", + "filter-obj", + "split-on-first", + "strict-uri-encode" + ] + }, "readable-stream@3.6.2": { "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": [ @@ -247,6 +428,20 @@ "safe-stable-stringify@2.5.0": { "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" }, + "sax@1.4.1": { + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "set-function-length@1.2.2": { + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": [ + "define-data-property", + "es-errors", + "function-bind", + "get-intrinsic", + "gopd", + "has-property-descriptors" + ] + }, "simple-swizzle@0.2.2": { "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "dependencies": [ @@ -264,15 +459,33 @@ "supports-color" ] }, + "split-on-first@1.1.0": { + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "stack-trace@0.0.10": { "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" }, + "stream-chain@2.2.5": { + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==" + }, + "stream-json@1.9.1": { + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "dependencies": [ + "stream-chain" + ] + }, + "strict-uri-encode@2.0.0": { + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==" + }, "string_decoder@1.3.0": { "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": [ "safe-buffer" ] }, + "strnum@1.0.5": { + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "supports-color@7.2.0": { "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": [ @@ -282,6 +495,12 @@ "text-hex@1.0.0": { "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, + "through2@4.0.2": { + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": [ + "readable-stream" + ] + }, "triple-beam@1.4.1": { "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" }, @@ -300,6 +519,33 @@ "util-deprecate@1.0.2": { "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "util@0.12.5": { + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": [ + "inherits", + "is-arguments", + "is-generator-function", + "is-typed-array", + "which-typed-array" + ] + }, + "web-encoding@1.1.5": { + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dependencies": [ + "@zxing/text-encoding", + "util" + ] + }, + "which-typed-array@1.1.15": { + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": [ + "available-typed-arrays", + "call-bind", + "for-each", + "gopd", + "has-tostringtag" + ] + }, "winston-transport@4.9.0": { "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "dependencies": [ @@ -323,6 +569,16 @@ "triple-beam", "winston-transport" ] + }, + "xml2js@0.6.2": { + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": [ + "sax", + "xmlbuilder" + ] + }, + "xmlbuilder@11.0.1": { + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" } }, "redirects": { @@ -406,6 +662,11 @@ "dependencies": [ "npm:winston@^3.17.0" ] + }, + "storage-s3": { + "dependencies": [ + "npm:minio@^8.0.2" + ] } } } diff --git a/storage-s3/CHANGELOG.md b/storage-s3/CHANGELOG.md new file mode 100644 index 0000000..8be8814 --- /dev/null +++ b/storage-s3/CHANGELOG.md @@ -0,0 +1,5 @@ +# Storage S3 Changelog + +## 2024-12-18 - 0.0.1 + +- feat: added basic setup diff --git a/storage-s3/README.md b/storage-s3/README.md new file mode 100644 index 0000000..c940021 --- /dev/null +++ b/storage-s3/README.md @@ -0,0 +1,33 @@ +# Storage (S3) wrapper + +[![JSR @frytg/storage-s3](https://jsr.io/badges/@frytg/storage-s3)](https://jsr.io/@frytg/storage-s3) +[![ci](https://github.com/frytg/utility/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/frytg/utility/actions/workflows/test.yml) + +This is a simple opinionated wrapper around the S3 library [MinIO](https://min.io/) to access and manipulate S3 objects. + +## Usage + +```ts +import { getObject } from '@frytg/storage-s3'; + +const object = await getObject('path/to/object.json', { parseJson: true }); +``` + +The MinIO client will be initialized with the required environment variables `STORE_S3_ENDPOINT`, `STORE_S3_ACCESS_KEY`, and `STORE_S3_SECRET_KEY`. + +## Methods + +- [getObject](https://jsr.io/@frytg/storage-s3/doc/~/getObject) +- [uploadObject](https://jsr.io/@frytg/storage-s3/doc/~/uploadObject) +- [objectExists](https://jsr.io/@frytg/storage-s3/doc/~/objectExists) +- [listObjects](https://jsr.io/@frytg/storage-s3/doc/~/listObjects) + +Also see all options in the [MinIO API documentation](https://min.io/docs/minio/linux/developers/javascript/API.html). They can be used by importing the `Client` object. + +## Author + +Created by [@frytg](https://github.com/frytg) / [frytg.digital](https://www.frytg.digital) + +## License + +[Unlicense](https://github.com/frytg/utility/blob/main/LICENSE) - also see [unlicense.org](https://unlicense.org) diff --git a/storage-s3/deno.jsonc b/storage-s3/deno.jsonc new file mode 100644 index 0000000..edcb67e --- /dev/null +++ b/storage-s3/deno.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "https://jsr.io/schema/config-file.v1.json", + "name": "@frytg/storage-s3", + "version": "0.0.1", + "exports": "./storage.ts", + "imports": { + "minio": "npm:minio@^8.0.2" + } +} diff --git a/storage-s3/s3.test.ts b/storage-s3/s3.test.ts new file mode 100644 index 0000000..a7d8108 --- /dev/null +++ b/storage-s3/s3.test.ts @@ -0,0 +1,25 @@ +// load packages +import process from 'node:process' +import { test } from '@cross/test' +import { assertInstanceOf } from '@std/assert' +// @deno-types="minio/dist/esm/minio.d.mts" +import { Client } from 'minio' +import sinon from 'sinon' + +test('s3 - exports a Minio.Client instance', async () => { + // Setup + const envStub = sinon.stub(process, 'env').value({ + STORE_S3_ENDPOINT: 'test-endpoint', + STORE_S3_ACCESS_KEY: 'test-access-key', + STORE_S3_SECRET_KEY: 'test-secret-key', + }) + + // load module with stubbed env + const { minioClient } = await import('./s3.ts') + + // Verify + assertInstanceOf(minioClient, Client, 'Should export a Minio.Client instance') + + // Cleanup + envStub.restore() +}) diff --git a/storage-s3/s3.ts b/storage-s3/s3.ts new file mode 100644 index 0000000..a588144 --- /dev/null +++ b/storage-s3/s3.ts @@ -0,0 +1,17 @@ +// load packages +import process from 'node:process' +// @deno-types="minio/dist/esm/minio.d.mts" +import { Client } from 'minio' + +// check environment variables +if (!process.env.STORE_S3_ENDPOINT) throw new Error('Environment variable STORE_S3_ENDPOINT is not defined') +if (!process.env.STORE_S3_ACCESS_KEY) throw new Error('Environment variable STORE_S3_ACCESS_KEY is not defined') +if (!process.env.STORE_S3_SECRET_KEY) throw new Error('Environment variable STORE_S3_SECRET_KEY is not defined') + +// create a minio client +export const minioClient = new Client({ + useSSL: true, + endPoint: process.env.STORE_S3_ENDPOINT, + accessKey: process.env.STORE_S3_ACCESS_KEY, + secretKey: process.env.STORE_S3_SECRET_KEY, +}) diff --git a/storage-s3/storage.ts b/storage-s3/storage.ts new file mode 100644 index 0000000..269399d --- /dev/null +++ b/storage-s3/storage.ts @@ -0,0 +1,194 @@ +/** + * Storage module for S3 with common operations using MinIO. + * + * @module + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html + */ + +// import packages +import { Buffer } from 'node:buffer' +import type { BucketItem, BucketItemStat, BucketStream, Client as ClientType } from 'minio/dist/esm/minio.d.mts' + +// load utils +import { minioClient } from './s3.ts' + +/** + * The MinIO client. + * @type {Client} + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html + * + * @example Remove an object + * ```ts + * import { Client as minioClient } from '@frytg/storage-s3' + * + * await minioClient.removeObject('mybucket', 'photo.jpg') + * ``` + */ +export const Client: ClientType = minioClient + +/** + * Retrieves an object from S3. + * @param {string} bucketName - The name of the bucket to retrieve the object from. + * @param {string} path - The path to the object in S3. + * @param {object} options - The options for the operation. + * @param {boolean} options.parseJson - Whether to parse the object as JSON. Defaults to `false`. + * @param {boolean} options.throwError - Whether to throw an error if the object does not exist. Defaults to `true`. + * @returns {Promise} + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html#getObject + * + * @example + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { getObject } from '@frytg/storage-s3' + * + * const object = await getObject(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/object.json', { parseJson: true }) + * console.log(object) + * ``` + */ +export const getObject = ( + bucketName: string, + path: string, + { + parseJson = false, + throwError = true, + }: { + parseJson?: boolean + throwError?: boolean + }, +): Promise => + new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + minioClient + .getObject(bucketName, path) + .then((stream) => { + stream.on('data', (chunk) => chunks.push(chunk)) + stream.on('end', () => { + const buffer = Buffer.concat(chunks) + if (parseJson) resolve(JSON.parse(buffer.toString('utf-8'))) + else resolve(buffer) + }) + stream.on('error', reject) + }) + .catch((error) => { + if (throwError) reject(error) + else resolve(null) + }) + }) + +/** + * Uploads an object to S3. + * @param {string} path - The path to the object in S3. + * @param {Buffer | string} data - The data to upload. Can be a Buffer or a string. If JSON is passed, it will be stringified. + * @param {object} options - The options for the operation. + * @param {string} options.bucketName - The name of the bucket to upload the object to. Defaults to env `S3_BUCKET_NAME`. + * @returns {Promise} + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html#putObject + * + * @example JSON + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { uploadObject } from '@frytg/storage-s3' + * + * await uploadObject(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/object.json', { foo: 'bar' }) + * ``` + * + * @example Buffer + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { uploadObject } from '@frytg/storage-s3' + * + * await uploadObject(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/object.blob', Buffer.from('foo')) + * ``` + */ +export const uploadObject = async (bucketName: string, path: string, data: Buffer | string): Promise => { + // convert data to string if it's an object or array + let dataString = data + if (typeof data === 'object' || Array.isArray(data)) { + dataString = JSON.stringify(data, null, 2) + } + + // upload object + await minioClient.putObject(bucketName, path, dataString) +} + +/** + * Checks if an object exists in S3. + * @param {string} path - The path to the object in S3. + * @param {object} options - The options for the operation. + * @param {string} options.bucketName - The name of the bucket to check for the object. Defaults to env `S3_BUCKET_NAME`. + * @returns {Promise} + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html#statObject + * + * @example + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { objectExists } from '@frytg/storage-s3' + * + * const exists = await objectExists(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/object.json') + * console.log(exists) // null if it doesn't exist, otherwise the object stat + * ``` + */ +export const objectExists = async (bucketName: string, path: string): Promise> => { + try { + // explicitly awaiting the result to avoid unhandled promise rejection + const result: BucketItemStat = await minioClient.statObject(bucketName, path) + return result + } catch (_error) { + return null + } +} + +// convert a readable stream to a string (for listObjects) +const readableStreamForListObjects = (stream: BucketStream): Promise => + new Promise((resolve, reject) => { + const chunks: BucketItem[] = [] + stream.on('data', (chunk) => chunks.push(chunk)) + stream.on('end', () => resolve(chunks)) + stream.on('error', reject) + }) + +/** + * Lists objects in S3. + * @param {string} prefix - The prefix to filter the objects by. + * @param {object} options - The options for the operation. + * @param {boolean} options.recursive - Whether to list recursively. Defaults to `false`. + * @param {string} options.bucketName - The name of the bucket to list the objects from. Defaults to env `S3_BUCKET_NAME`. + * @returns {Promise} + * + * @see https://min.io/docs/minio/linux/developers/javascript/API.html#listObjectsV2 + * + * @example + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { listObjects } from '@frytg/storage-s3' + * + * const objects = await listObjects(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/prefix') + * console.log(objects) + * ``` + * + * @example Recursive + * ```ts + * import { getRequiredEnv } from '@frytg/check-required-env/get' + * import { listObjects } from '@frytg/storage-s3' + * + * const objects = await listObjects(getRequiredEnv('S3_BUCKET_NAME'), 'path/to/prefix', { recursive: true }) + * console.log(objects) + * ``` + */ +export const listObjects = async ( + bucketName: string, + prefix: string, + { + recursive = false, + }: { + recursive?: boolean + }, +): Promise => { + const result: BucketStream = await minioClient.listObjectsV2(bucketName, prefix, recursive) + return readableStreamForListObjects(result) +}