diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index df007484..0a15f421 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,8 @@ jobs: - run: npm run lint + - run: npm run types + - run: npm test - run: npx semantic-release diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65a56fcf..c009cc69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,6 @@ jobs: - run: npm run lint + - run: npm run types + - run: npm test diff --git a/.gitignore b/.gitignore index da095c9a..4a108a93 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ tmp/**/* coverage .vscode .nyc_output/ -.tap/ \ No newline at end of file +.tap/ +types/ diff --git a/lib/classes/asset.js b/lib/classes/asset.js index ee0cf16a..bb761941 100644 --- a/lib/classes/asset.js +++ b/lib/classes/asset.js @@ -1,16 +1,21 @@ import mime from "mime"; import path from "node:path"; +/** + * @typedef {object} AssetOptions + * @property {string} [pathname] + * @property {string} [version] + * @property {string} [name] + * @property {string} [type] + * @property {string} [org] + */ + /** * Meta information about an Asset. - * @class Asset */ const Asset = class Asset { /** - * Creates an instance of Asset. - * @param {string} [base=''] Base file path. Should contain the extension of the asset. - * @param {string} [dir=''] Directory path to the asset. Will be prefixed to "base". - * @memberof Asset + * @param {AssetOptions} options */ constructor({ pathname = "", @@ -19,7 +24,9 @@ const Asset = class Asset { type = "", org = "", } = {}) { - this._mimeType = mime.getType(pathname) || "application/octet-stream"; + this._mimeType = + /** @type {string} */ (mime.getType(pathname)) || + "application/octet-stream"; this._type = type.toLowerCase(); this._size = -1; @@ -83,7 +90,7 @@ const Asset = class Asset { return { integrity: this.integrity, pathname: this.pathname, - mimeType: this._mimeType, + mimeType: this.mimeType, type: this.type, size: this.size, }; diff --git a/lib/classes/package.js b/lib/classes/package.js index 8cbcff80..7d7ae45d 100644 --- a/lib/classes/package.js +++ b/lib/classes/package.js @@ -11,6 +11,7 @@ const Package = class Package { author = {}, } = {}) { this._version = version; + // @ts-expect-error this._created = Math.floor(new Date() / 1000); this._author = author; this._type = type; diff --git a/lib/handlers/alias.delete.js b/lib/handlers/alias.delete.js index df11a9f8..6183bbe0 100644 --- a/lib/handlers/alias.delete.js +++ b/lib/handlers/alias.delete.js @@ -9,7 +9,18 @@ import { decodeUriComponent } from "../utils/utils.js"; import HttpOutgoing from "../classes/http-outgoing.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} AliasDeleteOptions + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const AliasDel = class AliasDel { + /** + * @param {AliasDeleteOptions} options + */ constructor({ organizations, cacheControl, logger, sink } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl; diff --git a/lib/handlers/alias.get.js b/lib/handlers/alias.get.js index 65878687..c1ecb669 100644 --- a/lib/handlers/alias.get.js +++ b/lib/handlers/alias.get.js @@ -10,7 +10,18 @@ import { createFilePathToAlias } from "../utils/path-builders-fs.js"; import HttpOutgoing from "../classes/http-outgoing.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} AliasGetOptions + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const AliasGet = class AliasGet { + /** + * @param {AliasGetOptions} options + */ constructor({ organizations, cacheControl, logger, sink } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl || "public, max-age=1200"; diff --git a/lib/handlers/alias.post.js b/lib/handlers/alias.post.js index e2f45923..67716c43 100644 --- a/lib/handlers/alias.post.js +++ b/lib/handlers/alias.post.js @@ -17,7 +17,18 @@ import Alias from "../classes/alias.js"; import config from "../utils/defaults.js"; import { decodeUriComponent, writeJSON } from "../utils/utils.js"; +/** + * @typedef {object} AliasPostOptions + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const AliasPost = class AliasPost { + /** + * @param {AliasPostOptions} options + */ constructor({ organizations, cacheControl, logger, sink } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl; @@ -37,7 +48,6 @@ const AliasPost = class AliasPost { this._orgRegistry = new Map(this._organizations); this._multipart = new MultipartParser({ - pkgMaxFileSize: this._pkgMaxFileSize, legalFields: ["version"], sink: this._sink, }); diff --git a/lib/handlers/alias.put.js b/lib/handlers/alias.put.js index 969baf23..bc552781 100644 --- a/lib/handlers/alias.put.js +++ b/lib/handlers/alias.put.js @@ -17,7 +17,18 @@ import Author from "../classes/author.js"; import Alias from "../classes/alias.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} AliasPutOptions + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const AliasPut = class AliasPut { + /** + * @param {AliasPutOptions} options + */ constructor({ organizations, cacheControl, logger, sink } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl; @@ -37,7 +48,6 @@ const AliasPut = class AliasPut { this._orgRegistry = new Map(this._organizations); this._multipart = new MultipartParser({ - pkgMaxFileSize: this._pkgMaxFileSize, legalFields: ["version"], sink: this._sink, }); diff --git a/lib/handlers/auth.post.js b/lib/handlers/auth.post.js index 757cd9f9..423c30bc 100644 --- a/lib/handlers/auth.post.js +++ b/lib/handlers/auth.post.js @@ -9,7 +9,18 @@ import HttpOutgoing from "../classes/http-outgoing.js"; import Author from "../classes/author.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} AuthPostOptions + * @property {string} [authKey] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const AuthPost = class AuthPost { + /** + * @param {AuthPostOptions} options + */ constructor({ organizations, cacheControl, authKey, logger } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl; @@ -29,9 +40,7 @@ const AuthPost = class AuthPost { this._orgRegistry = new Map(this._organizations); this._multipart = new MultipartParser({ - pkgMaxFileSize: this._pkgMaxFileSize, legalFields: ["key"], - sink: this._sink, }); } diff --git a/lib/handlers/map.get.js b/lib/handlers/map.get.js index 24757470..7ffc2d76 100644 --- a/lib/handlers/map.get.js +++ b/lib/handlers/map.get.js @@ -9,7 +9,19 @@ import HttpOutgoing from "../classes/http-outgoing.js"; import config from "../utils/defaults.js"; import { decodeUriComponent } from "../utils/utils.js"; +/** + * @typedef {object} MapGetOptions + * @property {boolean} [etag] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const MapGet = class MapGet { + /** + * @param {MapGetOptions} options + */ constructor({ organizations, cacheControl, logger, sink, etag } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl || "public, max-age=31536000, immutable"; diff --git a/lib/handlers/map.put.js b/lib/handlers/map.put.js index 2da965df..d0aaa4e6 100644 --- a/lib/handlers/map.put.js +++ b/lib/handlers/map.put.js @@ -23,7 +23,20 @@ import { readJSON, } from "../utils/utils.js"; +/** + * @typedef {object} MapPutOptions + * @property {number} [mapMaxFileSize] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const MapPut = class MapPut { + /** + * + * @param {MapPutOptions} options + */ constructor({ mapMaxFileSize, organizations, diff --git a/lib/handlers/pkg.get.js b/lib/handlers/pkg.get.js index 7cfd4587..17a62ce9 100644 --- a/lib/handlers/pkg.get.js +++ b/lib/handlers/pkg.get.js @@ -10,7 +10,20 @@ import HttpOutgoing from "../classes/http-outgoing.js"; import Asset from "../classes/asset.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} PkgGetOptions + * @property {boolean} [etag] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const PkgGet = class PkgGet { + /** + * + * @param {PkgGetOptions} options + */ constructor({ organizations, cacheControl, logger, sink, etag } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl || "public, max-age=31536000, immutable"; diff --git a/lib/handlers/pkg.log.js b/lib/handlers/pkg.log.js index 7e7ce292..14f5e416 100644 --- a/lib/handlers/pkg.log.js +++ b/lib/handlers/pkg.log.js @@ -9,7 +9,19 @@ import { decodeUriComponent } from "../utils/utils.js"; import HttpOutgoing from "../classes/http-outgoing.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} PkgLogOptions + * @property {boolean} [etag] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const PkgLog = class PkgLog { + /** + * @param {PkgLogOptions} options + */ constructor({ organizations, cacheControl, logger, sink, etag } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl || "no-cache"; diff --git a/lib/handlers/pkg.put.js b/lib/handlers/pkg.put.js index 90705730..e984172c 100644 --- a/lib/handlers/pkg.put.js +++ b/lib/handlers/pkg.put.js @@ -24,7 +24,19 @@ import Package from "../classes/package.js"; import Author from "../classes/author.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} PkgPutOptions + * @property {number} [pkgMaxFileSize=10000000] + * @property {string} [cacheControl] + * @property {Array<[string, string]>} [organizations] List of key-value pairs [hostname, organization] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const PkgPut = class PkgPut { + /** + * @param {PkgPutOptions} options + */ constructor({ pkgMaxFileSize, organizations, diff --git a/lib/handlers/versions.get.js b/lib/handlers/versions.get.js index 655cbc55..7e1405a0 100644 --- a/lib/handlers/versions.get.js +++ b/lib/handlers/versions.get.js @@ -9,7 +9,19 @@ import { decodeUriComponent } from "../utils/utils.js"; import HttpOutgoing from "../classes/http-outgoing.js"; import config from "../utils/defaults.js"; +/** + * @typedef {object} VersionsGetOptions + * @property {string} [cacheControl="no-cache"] + * @property {boolean} [etag=true] + * @property {Array<[string, string]>} [organizations] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const VersionsGet = class VersionsGet { + /** + * @param {VersionsGetOptions} options + */ constructor({ organizations, cacheControl, logger, sink, etag } = {}) { this._organizations = organizations || config.organizations; this._cacheControl = cacheControl || "no-cache"; diff --git a/lib/multipart/parser.js b/lib/multipart/parser.js index a203ea14..efbc3ad3 100644 --- a/lib/multipart/parser.js +++ b/lib/multipart/parser.js @@ -10,7 +10,19 @@ import FormField from "./form-field.js"; import FormFile from "./form-file.js"; import Asset from "../classes/asset.js"; +/** + * @typedef {object} MultipartParserOptions + * @property {number} [pkgMaxFileSize=10000000] + * @property {string[]} [legalFields] + * @property {string[]} [legalFiles] + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const MultipartParser = class MultipartParser { + /** + * @param {MultipartParserOptions} options + */ constructor({ pkgMaxFileSize, legalFields, legalFiles, logger, sink } = {}) { this._pkgMaxFileSize = pkgMaxFileSize; this._legalFields = legalFields || []; diff --git a/lib/sinks/test.js b/lib/sinks/test.js index 72360e65..85ca4105 100644 --- a/lib/sinks/test.js +++ b/lib/sinks/test.js @@ -30,8 +30,8 @@ export default class SinkTest extends Sink { }, }); - this._writeDelayResolve = () => -1; - this._writeDelayChunks = () => -1; + this._writeDelayResolve = (a = -1) => a; + this._writeDelayChunks = (a = -1) => a; } get metrics() { @@ -73,6 +73,9 @@ export default class SinkTest extends Sink { this._state = new Map(items); } + /** + * @param {(count: number) => number} fn + */ set writeDelayResolve(fn) { if (typeof fn !== "function") { throw new TypeError("Value must be a function"); @@ -80,6 +83,9 @@ export default class SinkTest extends Sink { this._writeDelayResolve = fn; } + /** + * @param {(count: number) => number} fn + */ set writeDelayChunks(fn) { if (typeof fn !== "function") { throw new TypeError("Value must be a function"); @@ -94,8 +100,8 @@ export default class SinkTest extends Sink { const operation = "write"; try { - super.constructor.validateFilePath(filePath); - super.constructor.validateContentType(contentType); + Sink.validateFilePath(filePath); + Sink.validateContentType(contentType); } catch (error) { this._counter.inc({ labels: { operation } }); reject(error); @@ -163,7 +169,7 @@ export default class SinkTest extends Sink { const operation = "read"; try { - super.constructor.validateFilePath(filePath); + Sink.validateFilePath(filePath); } catch (error) { this._counter.inc({ labels: { operation } }); reject(error); @@ -213,7 +219,7 @@ export default class SinkTest extends Sink { const operation = "delete"; try { - super.constructor.validateFilePath(filePath); + Sink.validateFilePath(filePath); } catch (error) { this._counter.inc({ labels: { operation } }); reject(error); @@ -252,7 +258,7 @@ export default class SinkTest extends Sink { const operation = "exist"; try { - super.constructor.validateFilePath(filePath); + Sink.validateFilePath(filePath); } catch (error) { this._counter.inc({ labels: { operation } }); reject(error); diff --git a/lib/utils/defaults.js b/lib/utils/defaults.js index 24435b4b..d5433720 100644 --- a/lib/utils/defaults.js +++ b/lib/utils/defaults.js @@ -7,10 +7,10 @@ const config = { mapMaxFileSize: 1000000, sinkFsRootPath: path.join(os.tmpdir(), "/eik-files"), etag: true, - organizations: [ + organizations: /** @type {Array<[string, string]>} */ ([ ["localhost", "local"], ["127.0.0.1", "local"], - ], + ]), }; export default config; diff --git a/lib/utils/healthcheck.js b/lib/utils/healthcheck.js index 527663a2..3421643f 100644 --- a/lib/utils/healthcheck.js +++ b/lib/utils/healthcheck.js @@ -7,7 +7,16 @@ import fs from "node:fs"; const fileReader = (file = "../../README.md") => fs.createReadStream(new URL(file, import.meta.url)); +/** + * @typedef {object} HealthCheckOptions + * @property {import("@eik/sink").default} [sink] + * @property {import("abslog").AbstractLoggerOptions} [logger] + */ + const HealthCheck = class HealthCheck { + /** + * @param {HealthCheckOptions} options + */ constructor({ sink, logger } = {}) { this._sink = sink; this._name = `./system/tmp/health_${slug()}.txt`; diff --git a/lib/utils/path-builders-fs.js b/lib/utils/path-builders-fs.js index 9b86a444..3ea5ed12 100644 --- a/lib/utils/path-builders-fs.js +++ b/lib/utils/path-builders-fs.js @@ -53,7 +53,7 @@ const createFilePathToEikJson = ({ org = "", type = "", name = "", - version, + version = "", } = {}) => path.join(globals.ROOT, org, type, name, version, "eik.json"); const createFilePathToAliasOrigin = ({ diff --git a/package.json b/package.json index ab167256..184dfd1d 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "1.3.54", "description": "Core server package", "main": "lib/main.js", + "types": "./types/main.d.ts", "type": "module", "files": [ "CHANGELOG.md", "package.json", - "lib" + "lib", + "types" ], "scripts": { "clean": "rimraf node_modules .tap types", @@ -44,6 +46,7 @@ "@eik/prettier-config": "1.0.1", "@eik/semantic-release-config": "1.0.0", "@eik/typescript-config": "1.0.0", + "@types/readable-stream": "4.0.15", "eslint": "9.8.0", "form-data": "4.0.0", "mkdirp": "3.0.1", diff --git a/test/classes/alias.js b/test/classes/alias.js index 7a555f1f..cf86849b 100644 --- a/test/classes/alias.js +++ b/test/classes/alias.js @@ -23,7 +23,6 @@ tap.test("Alias() - Default property values", (t) => { tap.test("Alias() - Set values to the arguments on the constructor", (t) => { const obj = new Alias({ - version: "1.0.0", alias: "v1", name: "buzz", type: "pkg", @@ -38,9 +37,7 @@ tap.test("Alias() - Set values to the arguments on the constructor", (t) => { }); tap.test("Alias() - Set a value on the .version property", (t) => { - const obj = new Alias({ - version: "1.0.0", - }); + const obj = new Alias(); obj.version = "2.0.0"; t.equal(obj.version, "2.0.0", ".version should be value set on .version"); t.end(); @@ -48,7 +45,6 @@ tap.test("Alias() - Set a value on the .version property", (t) => { tap.test("Alias() - Serialize object", (t) => { const obj = new Alias({ - version: "1.0.0", alias: "v1", name: "buzz", type: "pkg", diff --git a/test/classes/http-outgoing.js b/test/classes/http-outgoing.js index 099ac378..3b984be9 100644 --- a/test/classes/http-outgoing.js +++ b/test/classes/http-outgoing.js @@ -39,6 +39,7 @@ tap.test("HttpOutgoing() - Set .statusCode to non numeric value", (t) => { t.throws( () => { const obj = new HttpOutgoing(); + // @ts-expect-error Testing bad input obj.statusCode = "fouronefour"; }, /Value is not a legal http status code/, diff --git a/test/handlers/map.get.js b/test/handlers/map.get.js index ac6d3388..48c8ff62 100644 --- a/test/handlers/map.get.js +++ b/test/handlers/map.get.js @@ -16,6 +16,7 @@ const pipeInto = (...streams) => }, }); + // @ts-expect-error pipeline(...streams, to, (error) => { if (error) return reject(error); const str = buffer.join("").toString(); diff --git a/test/handlers/pkg.get.js b/test/handlers/pkg.get.js index 309e0c39..8db28db9 100644 --- a/test/handlers/pkg.get.js +++ b/test/handlers/pkg.get.js @@ -16,6 +16,7 @@ const pipeInto = (...streams) => }, }); + // @ts-expect-error pipeline(...streams, to, (error) => { if (error) return reject(error); const str = buffer.join("").toString(); diff --git a/test/handlers/pkg.log.js b/test/handlers/pkg.log.js index fa03b6b7..148979ca 100644 --- a/test/handlers/pkg.log.js +++ b/test/handlers/pkg.log.js @@ -16,6 +16,7 @@ const pipeInto = (...streams) => }, }); + // @ts-expect-error pipeline(...streams, to, (error) => { if (error) return reject(error); const str = buffer.join("").toString(); diff --git a/test/handlers/versions.get.js b/test/handlers/versions.get.js index 9964a573..c5524b87 100644 --- a/test/handlers/versions.get.js +++ b/test/handlers/versions.get.js @@ -16,6 +16,7 @@ const pipeInto = (...streams) => }, }); + // @ts-expect-error pipeline(...streams, to, (error) => { if (error) return reject(error); const str = buffer.join("").toString(); diff --git a/test/multipart/form-file.js b/test/multipart/form-file.js index 63472c82..e61b9154 100644 --- a/test/multipart/form-file.js +++ b/test/multipart/form-file.js @@ -28,6 +28,7 @@ tap.test("FormFile() - Custom constructor values", (t) => { tap.test("FormFile() - Constructor value is illegal", (t) => { t.throws( () => { + // @ts-expect-error Testing bad input // eslint-disable-next-line no-unused-vars const obj = new FormFile({ name: "foo", value: "bar" }); }, diff --git a/test/sinks/test.js b/test/sinks/test.js index d1c9e8dc..c9104530 100644 --- a/test/sinks/test.js +++ b/test/sinks/test.js @@ -54,6 +54,7 @@ const pipeInto = (...streams) => }, }); + // @ts-expect-error pipeline(...streams, to, (error) => { if (error) return reject(error); const str = buffer.join("").toString(); @@ -62,12 +63,15 @@ const pipeInto = (...streams) => }); const pipe = (...streams) => - new Promise((resolve, reject) => { - pipeline(...streams, (error) => { - if (error) return reject(error); - return resolve(); - }); - }); + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + // @ts-expect-error + pipeline(...streams, (error) => { + if (error) return reject(error); + return resolve(); + }); + }) + ); tap.test("Sink() - Object type", (t) => { const sink = new Sink(DEFAULT_CONFIG);