Compare commits

...

41 Commits

Author SHA1 Message Date
github-actions[bot]
d0827df60e chore(main): release 4.6.1 (#1322)
🤖 I have created a release *beep* *boop*
---


## [4.6.1](https://github.com/unraid/api/compare/v4.6.0...v4.6.1)
(2025-04-03)


### Bug Fixes

* don't mv deploy on prod release
([9568aab](9568aabd17))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 10:10:20 -04:00
Eli Bosley
9568aabd17 fix: don't mv deploy on prod release 2025-04-03 10:07:26 -04:00
github-actions[bot]
5a61aec841 chore(main): release 4.6.0 (#1311)
🤖 I have created a release *beep* *boop*
---


## [4.6.0](https://github.com/unraid/api/compare/v4.5.0...v4.6.0)
(2025-04-03)


### Features

* add gui settings field for sso users
([#1310](https://github.com/unraid/api/issues/1310))
([5ba3fa6](5ba3fa67a2))


### Bug Fixes

* build
([ed67af9](ed67af9568))
* **plugin:** flaky masking of benign warning during pnpm install
([#1313](https://github.com/unraid/api/issues/1313))
([1f10b63](1f10b63c8b))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 09:59:38 -04:00
Eli Bosley
ed67af9568 fix: build 2025-04-03 09:58:41 -04:00
Pujit Mehrotra
5ba3fa67a2 feat: add gui settings field for sso users (#1310)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Bulk SSO account management is now supported, allowing users to update
multiple SSO account IDs at once.
- Connect settings now include SSO account identifiers for streamlined
configuration.
- Expanded array management functionality introduces advanced operations
for managing disk arrays.
- New fields for SSO user IDs have been added to various settings and
queries.

- **Style & Documentation**
- Improved formatting and support for rich HTML in form descriptions
enhance clarity and presentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-04-03 09:50:12 -04:00
Pujit Mehrotra
1f10b63c8b fix(plugin): flaky masking of benign warning during pnpm install (#1313)
The masking logic has a different effect in TTY and non-TTY environments
bc pnpm will change the `install` reporter. Now, we:

- set `--reporter=append-only` for consistency
- replace the grep loop with `sed` to mask and redirect the benign
"Ignored build scripts" warning

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Streamlined the background logging during dependency installations to
improve overall efficiency and reduce complexity. This update enhances
maintainability without any visible changes to the user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-03 09:20:55 -04:00
Zack Spear
df168224ea refactor: webgui sync script to copy uui build (#1318)
- enhances `pnpm sync-webgui-repo` to include Unraid UI build + sync
options
- changed the dev intended ThemeSwitcher to use local / session storage

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- The theme switcher now respects your saved preferences, ensuring a
more personalized interface.
- The component build and synchronization process has been enhanced with
new options for UI components and clearer feedback during operations.
- New computed properties added for better state management in account
and purchase functionalities.

- **Bug Fixes**
- Improved error handling and logging during the component build
process.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-03 09:17:21 -04:00
Pujit Mehrotra
3911be38bc chore: keep preview plg when merging release-please pr's (#1312)
prevents staging plugin from breaking due to being populated with prod entities

## Summary by CodeRabbit

- **Chores**
- Enhanced the deployment process to clearly distinguish between
production and preview releases.
- Added verification steps to ensure all required production assets are
present, improving the overall reliability of plugin updates.
2025-04-02 14:40:18 -04:00
github-actions[bot]
15153855f7 chore(main): release 4.5.0 (#1279)
🤖 I have created a release *beep* *boop*
---


## [4.5.0](https://github.com/unraid/api/compare/v4.4.1...v4.5.0)
(2025-04-02)


### Features

* add webgui theme switcher component
([#1304](https://github.com/unraid/api/issues/1304))
([e2d00dc](e2d00dc346))
* api plugin system & offline versioned dependency vendoring
([#1252](https://github.com/unraid/api/issues/1252))
([9f492bf](9f492bf217))
* **api:** add `unraid-api --delete` command
([#1289](https://github.com/unraid/api/issues/1289))
([2f09445](2f09445f2e))
* basic array controls
([#1291](https://github.com/unraid/api/issues/1291))
([61fe696](61fe6966ca))
* basic docker controls
([#1292](https://github.com/unraid/api/issues/1292))
([12eddf8](12eddf894e))
* copy to webgui repo script docs + wc build options
([#1285](https://github.com/unraid/api/issues/1285))
([e54f189](e54f189630))


### Bug Fixes

* additional url fixes
([4b2763c](4b2763c7f9))
* **api:** redirect benign pnpm postinstall warning to log file
([#1290](https://github.com/unraid/api/issues/1290))
([7fb7849](7fb78494cb))
* **deps:** update dependency chalk to v5
([#1296](https://github.com/unraid/api/issues/1296))
([6bed638](6bed63805f))
* **deps:** update dependency diff to v7
([#1297](https://github.com/unraid/api/issues/1297))
([3c6683c](3c6683c814))
* disable all config watchers
([#1306](https://github.com/unraid/api/issues/1306))
([5c1b435](5c1b4352cf))
* extract callbacks to library
([#1280](https://github.com/unraid/api/issues/1280))
([2266139](2266139742))
* OEM plugin issues ([#1288](https://github.com/unraid/api/issues/1288))
([d5a3d0d](d5a3d0dfac))
* replace files lost during pruning
([d0d2ff6](d0d2ff65ed))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 13:24:42 -04:00
Eli Bosley
65b7747c0e chore: remove flaky test 2025-04-02 13:16:17 -04:00
Eli Bosley
5c1b4352cf fix: disable all config watchers (#1306)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Updated CLI operations now use sequential stop and start actions for
service reloads and SSO user modifications, ensuring a more controlled
update process.
- Introduced a new `<uui-toaster>` component for notifications,
replacing the previous jGrowl notification system and enhancing user
experience.
- Added functionality for managing live updates based on window focus,
improving responsiveness during user interaction.

- **Refactor**
- Streamlined configuration updates by removing automatic monitoring,
resulting in a simpler update workflow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-02 13:09:47 -04:00
Eli Bosley
9004313ff8 chore: lint to fix build 2025-04-02 12:23:20 -04:00
Eli Bosley
7a421ce4ec chore: fix dev console and allowed-headers 2025-04-02 11:28:21 -04:00
renovate[bot]
3c6683c814 fix(deps): update dependency diff to v7 (#1297)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [diff](https://redirect.github.com/kpdecker/jsdiff) | [`^5.1.0` ->
`^7.0.0`](https://renovatebot.com/diffs/npm/diff/5.2.0/7.0.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/diff/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/diff/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/diff/5.2.0/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/diff/5.2.0/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>kpdecker/jsdiff (diff)</summary>

###
[`v7.0.0`](https://redirect.github.com/kpdecker/jsdiff/blob/HEAD/release-notes.md#700)

[Compare
Source](https://redirect.github.com/kpdecker/jsdiff/compare/v6.0.0...7.0.0)

Just a single (breaking) bugfix, undoing a behaviour change introduced
accidentally in 6.0.0:

- [#&#8203;554](https://redirect.github.com/kpdecker/jsdiff/pull/554)
**`diffWords` treats numbers and underscores as word characters again.**
This behaviour was broken in v6.0.0.

###
[`v6.0.0`](https://redirect.github.com/kpdecker/jsdiff/blob/HEAD/release-notes.md#600)

[Compare
Source](https://redirect.github.com/kpdecker/jsdiff/compare/v5.2.0...v6.0.0)

This is a release containing many, *many* breaking changes. The
objective of this release was to carry out a mass fix, in one go, of all
the open bugs and design problems that required breaking changes to fix.
A substantial, but exhaustive, changelog is below.


[Commits](https://redirect.github.com/kpdecker/jsdiff/compare/v5.2.0...v6.0.0)

- [#&#8203;497](https://redirect.github.com/kpdecker/jsdiff/pull/497)
**`diffWords` behavior has been radically changed.** Previously, even
with `ignoreWhitespace: true`, runs of whitespace were tokens, which led
to unhelpful and unintuitive diffing behavior in typical texts.
Specifically, even when two texts contained overlapping passages,
`diffWords` would sometimes choose to delete all the words from the old
text and insert them anew in their new positions in order to avoid
having to delete or insert whitespace tokens. Whitespace sequences are
no longer tokens as of this release, which affects both the generated
diffs and the `count`s.

    Runs of whitespace are still tokens in `diffWordsWithSpace`.

As part of the changes to `diffWords`, **a new `.postProcess` method has
been added on the base `Diff` type**, which can be overridden in custom
`Diff` implementations.

**`diffLines` with `ignoreWhitespace: true` will no longer ignore the
insertion or deletion of entire extra lines of whitespace at the end of
the text**. Previously, these would not show up as insertions or
deletions, as a side effect of a hack in the base diffing algorithm
meant to help ignore whitespace in `diffWords`. More generally, **the
undocumented special handling in the core algorithm for ignored
terminals has been removed entirely.** (This special case behavior used
to rewrite the final two change objects in a scenario where the final
change object was an addition or deletion and its `value` was treated as
equal to the empty string when compared using the diff object's
`.equals` method.)

- [#&#8203;500](https://redirect.github.com/kpdecker/jsdiff/pull/500)
**`diffChars` now diffs Unicode code points** instead of UTF-16 code
units.

- [#&#8203;508](https://redirect.github.com/kpdecker/jsdiff/pull/508)
**`parsePatch` now always runs in what was previously "strict" mode; the
undocumented `strict` option has been removed.** Previously, by default,
`parsePatch` (and other patch functions that use it under the hood to
parse patches) would accept a patch where the line counts in the headers
were inconsistent with the actual patch content - e.g. where a hunk
started with the header `@@&#8203; -1,3 +1,6 @&#8203;@&#8203;`,
indicating that the content below spanned 3 lines in the old file and 6
lines in the new file, but then the actual content below the header
consisted of some different number of lines, say 10 lines of context, 5
deletions, and 1 insertion. Actually trying to work with these patches
using `applyPatch` or `merge`, however, would produce incorrect results
instead of just ignoring the incorrect headers, making this "feature"
more of a trap than something actually useful. It's been ripped out, and
now we are always "strict" and will reject patches where the line counts
in the headers aren't consistent with the actual patch content.

- [#&#8203;435](https://redirect.github.com/kpdecker/jsdiff/pull/435)
**Fix `parsePatch` handling of control characters.** `parsePatch` used
to interpret various unusual control characters - namely vertical tabs,
form feeds, lone carriage returns without a line feed, and EBCDIC NELs -
as line breaks when parsing a patch file. This was inconsistent with the
behavior of both JsDiff's own `diffLines` method and also the Unix
`diff` and `patch` utils, which all simply treat those control
characters as ordinary characters. The result of this discrepancy was
that some well-formed patches - produced either by `diff` or by JsDiff
itself and handled properly by the `patch` util - would be wrongly
parsed by `parsePatch`, with the effect that it would disregard the
remainder of a hunk after encountering one of these control characters.

- [#&#8203;439](https://redirect.github.com/kpdecker/jsdiff/pull/439)
**Prefer diffs that order deletions before insertions.** When faced with
a choice between two diffs with an equal total edit distance, the Myers
diff algorithm generally prefers one that does deletions before
insertions rather than insertions before deletions. For instance, when
diffing `abcd` against `acbd`, it will prefer a diff that says to delete
the `b` and then insert a new `b` after the `c`, over a diff that says
to insert a `c` before the `b` and then delete the existing `c`. JsDiff
deviated from the published Myers algorithm in a way that led to it
having the opposite preference in many cases, including that example.
This is now fixed, meaning diffs output by JsDiff will more accurately
reflect what the published Myers diff algorithm would output.

- [#&#8203;455](https://redirect.github.com/kpdecker/jsdiff/pull/455)
**The `added` and `removed` properties of change objects are now
guaranteed to be set to a boolean value.** (Previously, they would be
set to `undefined` or omitted entirely instead of setting them to
false.)

- [#&#8203;464](https://redirect.github.com/kpdecker/jsdiff/pull/464)
Specifying `{maxEditLength: 0}` now sets a max edit length of 0 instead
of no maximum.

- [#&#8203;460](https://redirect.github.com/kpdecker/jsdiff/pull/460)
**Added `oneChangePerToken` option.**

- [#&#8203;467](https://redirect.github.com/kpdecker/jsdiff/pull/467)
**Consistent ordering of arguments to `comparator(left, right)`.**
Values from the old array will now consistently be passed as the first
argument (`left`) and values from the new array as the second argument
(`right`). Previously this was almost (but not quite) always the other
way round.

- [#&#8203;480](https://redirect.github.com/kpdecker/jsdiff/pull/480)
**Passing `maxEditLength` to `createPatch` & `createTwoFilesPatch` now
works properly** (i.e. returns undefined if the max edit distance is
exceeded; previous behavior was to crash with a `TypeError` if the edit
distance was exceeded).

- [#&#8203;486](https://redirect.github.com/kpdecker/jsdiff/pull/486)
**The `ignoreWhitespace` option of `diffLines` behaves more sensibly
now.** `value`s in returned change objects now include leading/trailing
whitespace even when `ignoreWhitespace` is used, just like how with
`ignoreCase` the `value`s still reflect the case of one of the original
texts instead of being all-lowercase. `ignoreWhitespace` is also now
compatible with `newlineIsToken`. Finally, **`diffTrimmedLines` is
deprecated** (and removed from the docs) in favour of using `diffLines`
with `ignoreWhitespace: true`; the two are, and always have been,
equivalent.

- [#&#8203;490](https://redirect.github.com/kpdecker/jsdiff/pull/490)
**When calling diffing functions in async mode by passing a `callback`
option, the diff result will now be passed as the *first* argument to
the callback instead of the second.** (Previously, the first argument
was never used at all and would always have value `undefined`.)

- [#&#8203;489](redirect.github.com/kpdecker/jsdiff/pull/489)
**`this.options` no longer exists on `Diff` objects.** Instead,
`options` is now passed as an argument to methods that rely on options,
like `equals(left, right, options)`. This fixes a race condition in
async mode, where diffing behaviour could be changed mid-execution if a
concurrent usage of the same `Diff` instances overwrote its `options`.

- [#&#8203;518](https://redirect.github.com/kpdecker/jsdiff/pull/518)
**`linedelimiters` no longer exists** on patch objects; instead, when a
patch with Windows-style CRLF line endings is parsed, **the lines in
`lines` will end with `\r`**. There is now a **new
`autoConvertLineEndings` option, on by default**, which makes it so that
when a patch with Windows-style line endings is applied to a source file
with Unix style line endings, the patch gets autoconverted to use
Unix-style line endings, and when a patch with Unix-style line endings
is applied to a source file with Windows-style line endings, it gets
autoconverted to use Windows-style line endings.

- [#&#8203;521](https://redirect.github.com/kpdecker/jsdiff/pull/521)
**the `callback` option is now supported by `structuredPatch`,
`createPatch`, and `createTwoFilesPatch`**

- [#&#8203;529](https://redirect.github.com/kpdecker/jsdiff/pull/529)
**`parsePatch` can now parse patches where lines starting with `--` or
`++` are deleted/inserted**; previously, there were edge cases where the
parser would choke on valid patches or give wrong results.

- [#&#8203;530](https://redirect.github.com/kpdecker/jsdiff/pull/530)
**Added `ignoreNewlineAtEof` option to `diffLines`**

- [#&#8203;533](https://redirect.github.com/kpdecker/jsdiff/pull/533)
**`applyPatch` uses an entirely new algorithm for fuzzy matching.**
Differences between the old and new algorithm are as follows:
- The `fuzzFactor` now indicates the maximum [*Levenshtein*
distance](https://en.wikipedia.org/wiki/Levenshtein_distance) that there
can be between the context shown in a hunk and the actual file content
at a location where we try to apply the hunk. (Previously, it
represented a maximum [*Hamming*
distance](https://en.wikipedia.org/wiki/Hamming_distance), meaning that
a single insertion or deletion in the source file could stop a hunk from
applying even with a high `fuzzFactor`.)
- A hunk containing a deletion can now only be applied in a context
where the line to be deleted actually appears verbatim. (Previously, as
long as enough context lines in the hunk matched, `applyPatch` would
apply the hunk anyway and delete a completely different line.)
- The context line immediately before and immediately after an insertion
must match exactly between the hunk and the file for a hunk to apply.
(Previously this was not required.)

- [#&#8203;535](https://redirect.github.com/kpdecker/jsdiff/pull/535)
**A bug in patch generation functions is now fixed** that would
sometimes previously cause `\ No newline at end of file` to appear in
the wrong place in the generated patch, resulting in the patch being
invalid. **These invalid patches can also no longer be applied
successfully with `applyPatch`.** (It was already the case that tools
other than jsdiff, like GNU `patch`, would consider them malformed and
refuse to apply them; versions of jsdiff with this fix now do the same
thing if you ask them to apply a malformed patch emitted by jsdiff v5.)

- [#&#8203;535](https://redirect.github.com/kpdecker/jsdiff/pull/535)
**Passing `newlineIsToken: true` to *patch*-generation functions is no
longer allowed.** (Passing it to `diffLines` is still supported - it's
only functions like `createPatch` where passing `newlineIsToken` is now
an error.) Allowing it to be passed never really made sense, since in
cases where the option had any effect on the output at all, the effect
tended to be causing a garbled patch to be created that couldn't
actually be applied to the source file.

- [#&#8203;539](https://redirect.github.com/kpdecker/jsdiff/pull/539)
**`diffWords` now takes an optional `intlSegmenter` option** which
should be an `Intl.Segmenter` with word-level granularity. This provides
better tokenization of text into words than the default behaviour, even
for English but especially for some other languages for which the
default behaviour is poor.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [x] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 09:42:46 -04:00
renovate[bot]
6bed63805f fix(deps): update dependency chalk to v5 (#1296)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [chalk](https://redirect.github.com/chalk/chalk) | [`^4.1.2` ->
`^5.0.0`](https://renovatebot.com/diffs/npm/chalk/4.1.2/5.4.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/chalk/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/chalk/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/chalk/4.1.2/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/chalk/4.1.2/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>chalk/chalk (chalk)</summary>

###
[`v5.4.1`](https://redirect.github.com/chalk/chalk/releases/tag/v5.4.1)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.4.0...v5.4.1)

- Fix `navigator` not defined `ReferenceError`
([#&#8203;642](https://redirect.github.com/chalk/chalk/issues/642))
[`4ebb62d`](https://redirect.github.com/chalk/chalk/commit/4ebb62d)

###
[`v5.4.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.4.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.3.0...v5.4.0)

- Update `CIRCLECI` environments to return level 3 color support
[`f838120`](https://redirect.github.com/chalk/chalk/commit/f838120)

###
[`v5.3.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.3.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.2.0...v5.3.0)

- Add `sideEffects` field to package.json
[`5aafc0a`](https://redirect.github.com/chalk/chalk/commit/5aafc0a)
- Add support for Gitea Actions
([#&#8203;603](https://redirect.github.com/chalk/chalk/issues/603))
[`29b8569`](https://redirect.github.com/chalk/chalk/commit/29b8569)

###
[`v5.2.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.2.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.1.2...v5.2.0)

- Improve Deno compatibility
([#&#8203;579](https://redirect.github.com/chalk/chalk/issues/579))
[`7443e9f`](https://redirect.github.com/chalk/chalk/commit/7443e9f)
- Detect true-color support for GitHub Actions
([#&#8203;579](https://redirect.github.com/chalk/chalk/issues/579))
[`7443e9f`](https://redirect.github.com/chalk/chalk/commit/7443e9f)
- Detect true-color support for Kitty terminal
([#&#8203;579](https://redirect.github.com/chalk/chalk/issues/579))
[`7443e9f`](https://redirect.github.com/chalk/chalk/commit/7443e9f)
- Fix test for Azure DevOps environment
([#&#8203;579](https://redirect.github.com/chalk/chalk/issues/579))
[`7443e9f`](https://redirect.github.com/chalk/chalk/commit/7443e9f)

###
[`v5.1.2`](https://redirect.github.com/chalk/chalk/releases/tag/v5.1.2)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.1.1...v5.1.2)

- Fix exported styles names
([#&#8203;569](https://redirect.github.com/chalk/chalk/issues/569))
[`a34bcf6`](https://redirect.github.com/chalk/chalk/commit/a34bcf6)

###
[`v5.1.1`](https://redirect.github.com/chalk/chalk/releases/tag/v5.1.1)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.1.0...v5.1.1)

- Improved the names of exports introduced in 5.1.0
([#&#8203;567](https://redirect.github.com/chalk/chalk/issues/567))
[`6e0df05`](https://redirect.github.com/chalk/chalk/commit/6e0df05)
    -   We of course preserved the old names.

###
[`v5.1.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.1.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.0.1...v5.1.0)

- Expose style names
([#&#8203;566](https://redirect.github.com/chalk/chalk/issues/566))
[`d7d7571`](https://redirect.github.com/chalk/chalk/commit/d7d7571)

###
[`v5.0.1`](https://redirect.github.com/chalk/chalk/releases/tag/v5.0.1)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.0.0...v5.0.1)

- Add `main` field to package.json for backwards compatibility with some
developer tools
[`85f7e96`](https://redirect.github.com/chalk/chalk/commit/85f7e96)

###
[`v5.0.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.0.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v4.1.2...v5.0.0)

##### Breaking

- **This package is now pure ESM. Please [read
this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).**
- If you use TypeScript, you need to use TypeScript 4.7 or later.
[Why.](https://redirect.github.com/microsoft/TypeScript/issues/46452)
- If you use a bundler, make sure it supports ESM and that you have
correctly configured it for ESM.
- The Chalk issue tracker is not a support channel for your favorite
build/bundler tool.
- It's totally fine to stay on Chalk v4. It's been stable for years.
- Require Node.js 12.20
[`fa16f4e`](https://redirect.github.com/chalk/chalk/commit/fa16f4e)
- Move some properties off the default export to individual named
exports:
    -   `chalk.Instance` → `Chalk`
    -   `chalk.supportsColor` → `supportsColor`
    -   `chalk.stderr` → `chalkStderr`
    -   `chalk.stderr.supportsColor` → `supportsColorStderr`
- Remove `.keyword()`, `.hsl()`, `.hsv()`, `.hwb()`, and `.ansi()`
coloring methods
([#&#8203;433](https://redirect.github.com/chalk/chalk/issues/433))
[`4cf2e40`](https://redirect.github.com/chalk/chalk/commit/4cf2e40)
- These were not commonly used and added a lot of bloat to Chalk. You
can achieve the same by using the [`color-convert`
package](https://redirect.github.com/Qix-/color-convert).
- The tagged template literal support moved into a separate package:
[`chalk-template`](https://redirect.github.com/chalk/chalk-template)
([#&#8203;524](https://redirect.github.com/chalk/chalk/issues/524))
[`c987c61`](https://redirect.github.com/chalk/chalk/commit/c987c61)

```diff
-import chalk from 'chalk';
+import chalkTemplate from 'chalk-template';

-chalk`2 + 3 = {bold ${2 + 3}}`;
+chalkTemplate`2 + 3 = {bold ${2 + 3}}`;
```

##### Improvements

- Bundle dependencies
[`04fdbd6`](https://redirect.github.com/chalk/chalk/commit/04fdbd6)
    -   This means Chalk no longer has any dependencies 🎉
- [The install size is less than half of
v4.](https://packagephobia.com/result?p=chalk)
- Add `overline` style
([#&#8203;433](https://redirect.github.com/chalk/chalk/issues/433))
[`4cf2e40`](https://redirect.github.com/chalk/chalk/commit/4cf2e40)
- Preserve function prototype methods
([#&#8203;434](https://redirect.github.com/chalk/chalk/issues/434))
[`0fba91b`](https://redirect.github.com/chalk/chalk/commit/0fba91b)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 09:40:22 -04:00
renovate[bot]
f7f40d7906 fix(deps): update all non-major dependencies (#1278)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@graphql-codegen/typescript-resolvers](https://redirect.github.com/dotansimha/graphql-code-generator)
([source](https://redirect.github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/resolvers))
| [`4.4.4` ->
`4.5.0`](https://renovatebot.com/diffs/npm/@graphql-codegen%2ftypescript-resolvers/4.4.4/4.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-codegen%2ftypescript-resolvers/4.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@graphql-codegen%2ftypescript-resolvers/4.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@graphql-codegen%2ftypescript-resolvers/4.4.4/4.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-codegen%2ftypescript-resolvers/4.4.4/4.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [lucide-vue-next](https://lucide.dev)
([source](https://redirect.github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-vue-next))
| [`^0.483.0` ->
`^0.487.0`](https://renovatebot.com/diffs/npm/lucide-vue-next/0.483.0/0.487.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-vue-next/0.487.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lucide-vue-next/0.487.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lucide-vue-next/0.483.0/0.487.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-vue-next/0.483.0/0.487.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`10.6.5` ->
`10.7.1`](https://renovatebot.com/diffs/npm/pnpm/10.6.5/10.7.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/pnpm/10.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/pnpm/10.6.5/10.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.6.5/10.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[vuetify-nuxt-module](https://redirect.github.com/vuetifyjs/nuxt-module)
| [`0.18.4` ->
`0.18.5`](https://renovatebot.com/diffs/npm/vuetify-nuxt-module/0.18.4/0.18.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/vuetify-nuxt-module/0.18.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vuetify-nuxt-module/0.18.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vuetify-nuxt-module/0.18.4/0.18.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vuetify-nuxt-module/0.18.4/0.18.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>dotansimha/graphql-code-generator
(@&#8203;graphql-codegen/typescript-resolvers)</summary>

###
[`v4.5.0`](https://redirect.github.com/dotansimha/graphql-code-generator/blob/HEAD/packages/plugins/typescript/resolvers/CHANGELOG.md#450)

[Compare
Source](https://redirect.github.com/dotansimha/graphql-code-generator/compare/@graphql-codegen/typescript-resolvers@4.4.4...@graphql-codegen/typescript-resolvers@4.5.0)

##### Minor Changes

-
[#&#8203;10315](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10315)
[`f6909d1`](f6909d1797)
Thanks [@&#8203;eddeee888](https://redirect.github.com/eddeee888)! -
Implement semanticNonNull custom directive

##### Patch Changes

- Updated dependencies
\[[`f6909d1`](f6909d1797)]:
-
[@&#8203;graphql-codegen/visitor-plugin-common](https://redirect.github.com/graphql-codegen/visitor-plugin-common)[@&#8203;5](https://redirect.github.com/5).8.0
-
[@&#8203;graphql-codegen/typescript](https://redirect.github.com/graphql-codegen/typescript)[@&#8203;4](https://redirect.github.com/4).1.6

</details>

<details>
<summary>lucide-icons/lucide (lucide-vue-next)</summary>

###
[`v0.487.0`](https://redirect.github.com/lucide-icons/lucide/releases/tag/0.487.0):
Version 0.487.0

[Compare
Source](https://redirect.github.com/lucide-icons/lucide/compare/0.486.0...0.487.0)

#### What's Changed

- feat(icons): added `spline-pointer` icon by
[@&#8203;Kaleidosium](https://redirect.github.com/Kaleidosium) in
[https://github.com/lucide-icons/lucide/pull/2932](https://redirect.github.com/lucide-icons/lucide/pull/2932)

#### New Contributors

- [@&#8203;Kaleidosium](https://redirect.github.com/Kaleidosium) made
their first contribution in
[https://github.com/lucide-icons/lucide/pull/2932](https://redirect.github.com/lucide-icons/lucide/pull/2932)

**Full Changelog**:
https://github.com/lucide-icons/lucide/compare/0.486.0...0.487.0

###
[`v0.486.0`](https://redirect.github.com/lucide-icons/lucide/releases/tag/0.486.0):
Version 0.486.0

[Compare
Source](https://redirect.github.com/lucide-icons/lucide/compare/0.485.0...0.486.0)

#### What's Changed

-
feat([@&#8203;lucide/astro](https://redirect.github.com/lucide/astro)):
add lucide package for Astro by
[@&#8203;MoustaphaDev](https://redirect.github.com/MoustaphaDev) in
[https://github.com/lucide-icons/lucide/pull/2665](https://redirect.github.com/lucide-icons/lucide/pull/2665)

#### New Contributors

- [@&#8203;MoustaphaDev](https://redirect.github.com/MoustaphaDev) made
their first contribution in
[https://github.com/lucide-icons/lucide/pull/2665](https://redirect.github.com/lucide-icons/lucide/pull/2665)

**Full Changelog**:
https://github.com/lucide-icons/lucide/compare/0.485.0...0.486.0

###
[`v0.485.0`](https://redirect.github.com/lucide-icons/lucide/releases/tag/0.485.0):
Version 0.485.0

[Compare
Source](https://redirect.github.com/lucide-icons/lucide/compare/0.484.0...0.485.0)

#### What's Changed

- feat(icons): added `banknote-arrow-down` icon by
[@&#8203;joffx](https://redirect.github.com/joffx) in
[https://github.com/lucide-icons/lucide/pull/2948](https://redirect.github.com/lucide-icons/lucide/pull/2948)
- feat(icons): added `banknote-x` icon by
[@&#8203;joffx](https://redirect.github.com/joffx) in
[https://github.com/lucide-icons/lucide/pull/2949](https://redirect.github.com/lucide-icons/lucide/pull/2949)

**Full Changelog**:
https://github.com/lucide-icons/lucide/compare/0.484.0...0.485.0

###
[`v0.484.0`](https://redirect.github.com/lucide-icons/lucide/releases/tag/0.484.0):
Version 0.484.0

[Compare
Source](https://redirect.github.com/lucide-icons/lucide/compare/0.483.0...0.484.0)

#### What's Changed

- build(deps): bump tj-actions/changed-files from 41 to 46 in
/.github/workflows by
[@&#8203;dependabot](https://redirect.github.com/dependabot) in
[https://github.com/lucide-icons/lucide/pull/2920](https://redirect.github.com/lucide-icons/lucide/pull/2920)
- fix(packages): consistent icon name class by
[@&#8203;danteissaias](https://redirect.github.com/danteissaias) in
[https://github.com/lucide-icons/lucide/pull/2878](https://redirect.github.com/lucide-icons/lucide/pull/2878)
- feat(ci): add `x.com` to brand filter by
[@&#8203;jguddas](https://redirect.github.com/jguddas) in
[https://github.com/lucide-icons/lucide/pull/2939](https://redirect.github.com/lucide-icons/lucide/pull/2939)
- fix(icons): arcified candy & candy-off by
[@&#8203;karsa-mistmere](https://redirect.github.com/karsa-mistmere) in
[https://github.com/lucide-icons/lucide/pull/2944](https://redirect.github.com/lucide-icons/lucide/pull/2944)

#### New Contributors

- [@&#8203;danteissaias](https://redirect.github.com/danteissaias) made
their first contribution in
[https://github.com/lucide-icons/lucide/pull/2878](https://redirect.github.com/lucide-icons/lucide/pull/2878)

**Full Changelog**:
https://github.com/lucide-icons/lucide/compare/0.483.0...0.484.0

</details>

<details>
<summary>pnpm/pnpm (pnpm)</summary>

###
[`v10.7.1`](https://redirect.github.com/pnpm/pnpm/releases/tag/v10.7.1):
pnpm 10.7.1

[Compare
Source](https://redirect.github.com/pnpm/pnpm/compare/v10.7.0...v10.7.1)

#### Patch Changes

- `pnpm config set` should convert the settings to their correct type
before adding them to `pnpm-workspace.yaml`
[#&#8203;9355](https://redirect.github.com/pnpm/pnpm/issues/9355).
- `pnpm config get` should read auth related settings via npm CLI
[#&#8203;9345](https://redirect.github.com/pnpm/pnpm/issues/9345).
- Replace leading `~/` in a path in `.npmrc` with the home directory
[#&#8203;9217](https://redirect.github.com/pnpm/pnpm/issues/9217).

#### Platinum Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://bit.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80"
alt="Bit"></a>
      </td>
      <td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/sanity.svg"
width="180" alt="Bit"></a>
      </td>
      <td align="center" valign="middle">
<a href="https://syntax.fm/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/syntaxfm.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/syntaxfm_light.svg" />
<img src="https://pnpm.io/img/users/syntaxfm.svg" width="90"
alt="Syntax" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220"
alt="Discord" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://uscreen.de/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/uscreen.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/uscreen_light.svg" />
<img src="https://pnpm.io/img/users/uscreen.svg" width="180"
alt="u|screen" />
          </picture>
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a
href="https://www.jetbrains.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/jetbrains.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/jetbrains.svg" />
<img src="https://pnpm.io/img/users/jetbrains.svg" width="180"
alt="JetBrains" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/nx.svg?0" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/nx_light.svg?0" />
<img src="https://pnpm.io/img/users/nx.svg" width="70" alt="Nx" />
          </picture>
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a
href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220"
alt="CodeRabbit" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://route4me.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
<img src="https://pnpm.io/img/users/route4me.svg" width="220"
alt="Route4Me" />
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190"
alt="Workleap" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190"
alt="Stackblitz" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

###
[`v10.7.0`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1070)

[Compare
Source](https://redirect.github.com/pnpm/pnpm/compare/v10.6.5...v10.7.0)

##### Minor Changes

- `pnpm config get` and `list` also show settings set in
`pnpm-workspace.yaml` files
[#&#8203;9316](https://redirect.github.com/pnpm/pnpm/pull/9316).

- It should be possible to use env variables in `pnpm-workspace.yaml`
setting names and value.

- Add an ability to patch dependencies by version ranges. Exact versions
override version ranges, which in turn override name-only patches.
Version range `*` is the same as name-only, except that patch
application failure will not be ignored.

    For example:

    ```yaml
    patchedDependencies:
      foo: patches/foo-1.patch
      foo@^2.0.0: patches/foo-2.patch
      foo@2.1.0: patches/foo-3.patch
    ```

The above configuration would apply `patches/foo-3.patch` to
`foo@2.1.0`, `patches/foo-2.patch` to all `foo` versions which satisfy
`^2.0.0` except `2.1.0`, and `patches/foo-1.patch` to the remaining
`foo` versions.

    > \[!WARNING]
> The version ranges should not overlap. If you want to specialize a sub
range, make sure to exclude it from the other keys. For example:
    >
    > ```yaml
    > # pnpm-workspace.yaml
    > patchedDependencies:
    >   # the specialized sub range
    >   'foo@2.2.0-2.8.0': patches/foo.2.2.0-2.8.0.patch
    >   # the more general patch, excluding the sub range above
    >   'foo@>=2.0.0 <2.2.0 || >2.8.0': 'patches/foo.gte2.patch
    > ```
    >
> In most cases, however, it's sufficient to just define an exact
version to override the range.

- `pnpm config set --location=project` saves the setting to a
`pnpm-workspace.yaml` file if no `.npmrc` file is present in the
directory
[#&#8203;9316](https://redirect.github.com/pnpm/pnpm/pull/9316).

- Rename `pnpm.allowNonAppliedPatches` to `pnpm.allowUnusedPatches`. The
old name is still supported but it would print a deprecation warning
message.

- Add `pnpm.ignorePatchFailures` to manage whether pnpm would ignore
patch application failures.

If `ignorePatchFailures` is not set, pnpm would throw an error when
patches with exact versions or version ranges fail to apply, and it
would ignore failures from name-only patches.

If `ignorePatchFailures` is explicitly set to `false`, pnpm would throw
an error when any type of patch fails to apply.

If `ignorePatchFailures` is explicitly set to `true`, pnpm would print a
warning when any type of patch fails to apply.

##### Patch Changes

- Remove dependency paths from audit output to prevent out-of-memory
errors
[#&#8203;9280](https://redirect.github.com/pnpm/pnpm/issues/9280).

</details>

<details>
<summary>vuetifyjs/nuxt-module (vuetify-nuxt-module)</summary>

###
[`v0.18.5`](https://redirect.github.com/vuetifyjs/nuxt-module/releases/tag/v0.18.5)

[Compare
Source](https://redirect.github.com/vuetifyjs/nuxt-module/compare/v0.18.4...v0.18.5)

#####    🐞 Bug Fixes

- **styles**: Resolve new scss component styles  -  by
[@&#8203;userquin](https://redirect.github.com/userquin) in
[https://github.com/vuetifyjs/nuxt-module/issues/305](https://redirect.github.com/vuetifyjs/nuxt-module/issues/305)
[<samp>(82139)</samp>](https://redirect.github.com/vuetifyjs/nuxt-module/commit/8213962)

#####     [View changes on
GitHub](https://redirect.github.com/vuetifyjs/nuxt-module/compare/v0.18.4...v0.18.5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIyNy4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 09:40:11 -04:00
Zack Spear
e2d00dc346 feat: add webgui theme switcher component (#1304)
For now just used in Dev Mode to help assist in testing webgui CSS
changes.

- Introduced a new `UnraidThemeSwitcher` component to allow users to
change themes dynamically.
- Updated `nuxt.config.ts` to register the new component.
- Created `ThemeSwitcher.ce.vue` with functionality to handle theme
changes and update the UI accordingly.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a theme switcher, allowing users to easily swap between
available application themes.
- Expanded the configuration to register the new theme switcher as a
custom element for seamless integration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-01 14:13:42 -07:00
Pujit Mehrotra
48c6ad7afa chore: fix license badge & add community links (#1300)
shields.io couldn't "recognize" our license.md, so it's statically
generated now.

## Summary by CodeRabbit

- **Documentation**
- Revised the project description and updated the documentation URL for
clearer API guidance.
  - Enhanced visual presentation by centering project shields.
  - Added a new badge to highlight the use of Vite.
- Restructured contact information into a new "Community" section with
updated channels.
  - Updated licensing details to reflect the GPL-2.0 license.
2025-03-31 16:22:26 -04:00
Eli Bosley
12eddf894e feat: basic docker controls (#1292)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Updated configuration settings by resetting various parameters to
default values.
	- Enhanced module integration with new Docker components.
- **New Features**
	- Introduced GraphQL API endpoints to start and stop Docker containers.
	- Added a backend service to manage Docker container operations.
- **Tests**
- Expanded test suites covering Docker resolvers and service methods to
ensure reliable container management.
- **Refactor**
- Consolidated code imports and streamlined internal calls for clearer
structure.
	- Enhanced type safety for Docker container retrieval parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-03-31 16:21:06 -04:00
Eli Bosley
61fe6966ca feat: basic array controls (#1291)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Expanded API operations for array management, including new mutations
for state changes and disk handling.
- Introduced new enumeration and input types for managing array states
in the GraphQL schema.
- Added a new resolver for handling array mutations in the GraphQL API.

- **Chores**
- Upgraded configuration version to 4.4.1 and refined connectivity
status reporting.

- **Refactor**
- Streamlined request processing with improved error handling to provide
clearer feedback on connectivity issues.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-31 15:47:33 -04:00
renovate[bot]
b4c8efa483 fix(deps): update dependency ignore to v7 (#1299)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ignore](https://redirect.github.com/kaelzhang/node-ignore) |
[`^5.2.4` ->
`^7.0.0`](https://renovatebot.com/diffs/npm/ignore/5.3.2/7.0.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/ignore/7.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/ignore/7.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/ignore/5.3.2/7.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ignore/5.3.2/7.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>kaelzhang/node-ignore (ignore)</summary>

###
[`v7.0.3`](52f8e855b6...7.0.3)

[Compare
Source](52f8e855b6...7.0.3)

###
[`v7.0.2`](https://redirect.github.com/kaelzhang/node-ignore/compare/7.0.1...52f8e855b6aca711579c008f38829f48e5a4c78b)

[Compare
Source](https://redirect.github.com/kaelzhang/node-ignore/compare/7.0.1...52f8e855b6aca711579c008f38829f48e5a4c78b)

###
[`v7.0.1`](https://redirect.github.com/kaelzhang/node-ignore/compare/7.0.0...7.0.1)

[Compare
Source](https://redirect.github.com/kaelzhang/node-ignore/compare/7.0.0...7.0.1)

###
[`v7.0.0`](https://redirect.github.com/kaelzhang/node-ignore/releases/tag/7.0.0)

[Compare
Source](ee70c66d32...7.0.0)

#### Minor feature

The primary feature introduced by `7.0.0` is the `.checkIgnore()`
method, which is most equivalent to `git check-ignore -v`. And also it
allows to pass `{pattern, mark?}` as the parameter of `.add()` so that
we could imitate the mechanism of `git check-ignore` as:

```js
ig.add({
  pattern: 'foo/*',
  mark: '60'
})

const {
  ignored,
  rule
} = checkIgnore('foo/')

if (ignored) {
  console.log(`.gitignore:${result}:${rule.mark}:${rule.pattern} foo/`)
}

// .gitignore:60:foo/* foo/
```

#### Potential Incompatibilities

The only situation that `7.0.0` might bring incompatibility is to
`.add()` a `< 7.0.0` instance into a `>= 7.0.0` instance, which might
occur when the current workspace has multiple `node-ignore` versions
installed, please be careful.

```js
const {anotherIgnoreInstance} = require('./ignore-6.0-instance')

// A 7.0.0 instance
ignore().add(anotherIgnoreInstance)

// It will break your code
```

Otherwise, in most cases, if you never do something above or there is
only one version of `node-ignore` installed in the current workspace, it
is quite safe to upgrade to `7.0.0`.

###
[`v6.0.2`](https://redirect.github.com/kaelzhang/node-ignore/compare/6.0.1...ee70c66d32e900f6e793c733a4ca27a1dafb3819)

[Compare
Source](https://redirect.github.com/kaelzhang/node-ignore/compare/6.0.1...ee70c66d32e900f6e793c733a4ca27a1dafb3819)

###
[`v6.0.1`](https://redirect.github.com/kaelzhang/node-ignore/releases/tag/6.0.1):
6.x (&#x3D;&#x3D; 5.3.2)

[Compare
Source](https://redirect.github.com/kaelzhang/node-ignore/compare/6.0.0...6.0.1)

### 6.x

-   The release of `6.x` is due to a mistake.
- Making an upgrade from `5.x` to 6.x for now actually changes nothing
and does no harm, everyone could just upgrade or ignore the version.
    -   Actually `6.0.2` is `5.3.2`
-   The next major release will be `7.0.0`

###
[`v6.0.0`](https://redirect.github.com/kaelzhang/node-ignore/compare/5.3.2...6.0.0)

[Compare
Source](https://redirect.github.com/kaelzhang/node-ignore/compare/5.3.2...6.0.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 15:38:37 -04:00
Eli Bosley
dc9d5ad661 chore: fix license path in readme 2025-03-31 15:16:05 -04:00
Pujit Mehrotra
4964cf557b chore: update license fields in package.json (#1298)
Sets license for all node.js packages to `GPL-2.0-or-later`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Simplified license compliance verification by removing legacy script
logic.
- Updated licensing terms across components to GPL-2.0-or-later for
broader compatibility.
- **Documentation**
- Revised setup instructions to indicate that the use of the Just build
tool is now optional.
- **New Features**
- Added fallback support in the build automation tool to ensure smoother
operations when targets are missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-31 14:52:49 -04:00
Zack Spear
e54f189630 feat: copy to webgui repo script docs + wc build options (#1285)
Previously the historical script only use to copy built web components
into the webgui repo.

The new script provides additional commands to find files in the API
repo's plugin dir and attempts to find "new" files. Then a command to
act upon each of these new file to sync in either direction, skip it, or
ignore it.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced an interactive synchronization tool that streamlines file
transfers between projects.
- Added a new command to easily trigger the synchronization process,
leveraging enhanced file management and notification capabilities.

- **Chores**
- Updated version control settings to ignore temporary synchronization
files.
  - Removed an outdated file copying script for improved maintenance.
  - Added new dependencies to support enhanced functionality.
- Modified a script to exclude specific files from deletion during the
activation code setup process.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-31 14:36:33 -04:00
Pujit Mehrotra
9f7218da79 chore: update license to GPL-2.0-or-later (#1295)
to accomodate GPL-3.0 based dependencies.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Revised licensing documentation to clearly state that the project is
distributed under GNU GPL v2 (or later) with complete terms provided.
- **Chores**
  - Updated overall license declarations to a more flexible version.
- Introduced new compliance tools to automatically verify that
dependency licenses meet the accepted criteria.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-31 14:34:33 -04:00
Eli Bosley
d5a3d0dfac fix: OEM plugin issues (#1288)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- The file synchronization tool now requires an explicitly defined
destination and offers a new option to remove remote password files
before syncing. Additionally, synchronization now omits ownership,
group, and permission metadata for smoother transfers.
- The activation flow has been refined, with legacy authentication
modifications removed to deliver a cleaner welcome modal experience.
- A new variable has been introduced to enhance server state management.

- **Refactor**
- Internal configuration adjustments have been made to enhance component
delivery and streamline system actions.

- **Bug Fixes**
- Updated handling of remote user and host parameters in the
synchronization script for improved functionality.
- Simplified the activation code removal process by focusing solely on
the `.set-password.php` file restoration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Zack Spear <hi@zackspear.com>
2025-03-28 16:39:15 -04:00
Pujit Mehrotra
2266139742 fix: extract callbacks to library (#1280)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
  - Updated environment configuration to enhance secure handling.
  - Integrated a shared dependency for consistent callback management.

- **Refactor**
  - Streamlined callback actions management for improved performance.
- Upgraded the operating system version from a beta release to stable
(7.0.0), delivering enhanced reliability.
- Centralized callback actions store usage across components for better
state management.
- Removed deprecated callback management code to improve clarity and
maintainability.

- **New Features**
- Introduced a new redirect component to enhance user navigation
experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Eli Bosley <ekbosley@gmail.com>
Co-authored-by: Zack Spear <hi@zackspear.com>
2025-03-28 16:11:29 -04:00
Pujit Mehrotra
7fb78494cb fix(api): redirect benign pnpm postinstall warning to log file (#1290)
which lives at `/var/log/unraid-api/build-scripts.log`
2025-03-28 13:50:41 -04:00
Pujit Mehrotra
2f09445f2e feat(api): add unraid-api --delete command (#1289)
- Added a `command:raw` package.json script to let devs run `dist/cli.js` without rebuilding, enabling experimentation in the transpiled script.
- `unraid-api --delete` allows users to select and remove keys with confirmation
prompts.
2025-03-28 10:17:22 -04:00
Eli Bosley
fa6a5c56b6 chore: update ranging strategy for renovate (#1286)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Updated the dependency update configuration to adopt a replace
strategy for version ranges, ensuring smoother and clearer version
management.
- Added a new dependency, `@unraid/shared-callbacks`, for enhanced
functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-28 10:12:07 -04:00
Eli Bosley
234017a828 chore: add self to work intent template 2025-03-27 17:12:32 -04:00
Eli Bosley
0210b9e62e chore: wrong charactrer 2025-03-27 17:11:17 -04:00
Eli Bosley
4b2763c7f9 fix: additional url fixes 2025-03-27 17:10:51 -04:00
Eli Bosley
ed44796d5d Update readme.md 2025-03-27 17:07:49 -04:00
Pujit Mehrotra
d23a89f0b3 chore: re-add .env.staging in api 2025-03-27 16:01:38 -04:00
Pujit Mehrotra
d0d2ff65ed fix: replace files lost during pruning 2025-03-27 15:47:29 -04:00
Pujit Mehrotra
9f492bf217 feat: api plugin system & offline versioned dependency vendoring (#1252)
- **New Features**
- Created a dynamic plugin system for the API to enable community
augmentation of GraphQL, CLI, and Cron functionalities capabilities.
- Included an example plugin under `packages/unraid-api-plugin-health`
that adds a new graphql query for API health checks.
- Added `rc.unraid-api` commands for backing up, restoring, and
installing production dependencies, streamlining maintenance and
deployment.
- Improved dependency vendoring by bundling a versioned pnpm store
(instead of `node_modules`). Versioning will allow users to add plugins
to a specific api release without requiring an internet connection on
subsequent reboots.

- **Chores**
- Upgraded build workflows and versioning processes to ensure more
reliable artifact handling and production packaging.
2025-03-27 13:23:55 -04:00
github-actions[bot]
c4b4d26af0 chore(main): release 4.4.1 (#1270)
🤖 I have created a release *beep* *boop*
---


## [4.4.1](https://github.com/unraid/api/compare/v4.4.0...v4.4.1)
(2025-03-26)


### Bug Fixes

* .env.production from allowing console logs on build
([#1273](https://github.com/unraid/api/issues/1273))
([49f6365](49f636541b))
* patch version override logic incorrect
([#1275](https://github.com/unraid/api/issues/1275))
([0bcfb47](0bcfb47bbc))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-26 20:01:06 -04:00
Eli Bosley
0bcfb47bbc fix: patch version override logic incorrect (#1275)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved the system’s version update mechanism so that updates are
applied only when the current system version matches the expected
version. This enhancement ensures more consistent and reliable version
checks during the update process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-26 17:57:30 -04:00
Michael Datelle
b0099421f3 refactor: swap out radix with reka (#1271)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Improved type safety and consistency across UI components by
leveraging centralized type definitions.

- **Chores**
- Updated and consolidated UI component dependencies by migrating from a
previous library to a new one and refining package configurations.

- **Style**
- Standardized code formatting for uniform syntax and improved
readability.

- **Tests**
- Expanded testing configuration to include additional file types for
enhanced test coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-03-26 14:56:43 -04:00
Zack Spear
49f636541b fix: .env.production from allowing console logs on build (#1273)
`VITE_ALLOW_CONSOLE_LOGS` should not be present in `.env.production`. We
don't want basic debugs logs in prod.

Found this because the latest OS release `7.1.0-beta.1.6` which included
the latest web components had debug logs included.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Disabled non-essential console logging in the production environment
for a cleaner runtime experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-26 09:16:14 -04:00
208 changed files with 4952 additions and 3697 deletions

View File

@@ -3,7 +3,7 @@ name: Work Intent
about: Request approval for planned development work (must be approved before starting)
title: 'Work Intent: '
labels: work-intent, unapproved
assignees: ''
assignees: 'elibosley'
---
<!--

View File

@@ -152,6 +152,11 @@ jobs:
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
- name: Upload PNPM Store to Github artifacts
uses: actions/upload-artifact@v4
with:
name: packed-pnpm-store
path: ${{ github.workspace }}/api/deploy/packed-pnpm-store.txz
build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version)
@@ -316,6 +321,15 @@ jobs:
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Get API Version
id: vars
run: |
GIT_SHA=$(git rev-parse --short HEAD)
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
echo "API_VERSION=${API_VERSION}" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
@@ -347,6 +361,11 @@ jobs:
with:
name: unraid-api
path: ${{ github.workspace }}/plugin/api/
- name: Download PNPM Store
uses: actions/download-artifact@v4
with:
name: packed-pnpm-store
path: ${{ github.workspace }}/plugin/
- name: Extract Unraid API
run: |
mkdir -p ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
@@ -355,6 +374,7 @@ jobs:
id: build-plugin
run: |
cd ${{ github.workspace }}/plugin
ls -al
pnpm run build:txz
if [ -n "${{ github.event.pull_request.number }}" ]; then
@@ -365,19 +385,20 @@ jobs:
BUCKET_PATH="unraid-api"
fi
# On release, build both prod and preview plugins
if [ "${{ needs.release-please.outputs.releases_created }}" == 'true' ]; then
BASE_URL="https://stable.dl.unraid.net/unraid-api"
else
BASE_URL="https://preview.dl.unraid.net/unraid-api"
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
cp -r ./deploy ./deploy-prod
fi
BASE_URL="https://preview.dl.unraid.net/unraid-api"
echo "BUCKET_PATH=${BUCKET_PATH}" >> $GITHUB_OUTPUT
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
- name: Ensure Plugin Files Exist
run: |
ls -al ./deploy
if [ ! -f ./deploy/*.plg ]; then
echo "Error: .plg file not found in plugin/deploy/"
exit 1
@@ -387,6 +408,19 @@ jobs:
echo "Error: .txz file not found in plugin/deploy/"
exit 1
fi
- name: Ensure Production Plugin Files Exist
if: needs.release-please.outputs.releases_created == 'true'
run: |
ls -al ./deploy-prod
if [ ! -f ./deploy-prod/*.plg ]; then
echo "Error: .plg file not found in plugin/deploy-prod/"
exit 1
fi
if [ ! -f ./deploy-prod/*.txz ]; then
echo "Error: .txz file not found in plugin/deploy-prod/"
exit 1
fi
- name: Upload to GHA
uses: actions/upload-artifact@v4
with:
@@ -414,7 +448,7 @@ jobs:
run: |
release_name=$(gh release list --repo ${{ github.repository }} --json name,isDraft --jq '.[] | select(.isDraft == true) | .name' | head -n 1)
# For each file in release directory
for file in deploy/*; do
for file in deploy-prod/*; do
echo "Uploading $file to release..."
gh release upload "${release_name}" "$file" --clobber
done

7
.gitignore vendored
View File

@@ -88,7 +88,6 @@ deploy/*
.nitro
.cache
.output
.env*
!.env.example
fb_keepalive
@@ -101,3 +100,9 @@ result
result-*
.direnv/
.envrc
# Webgui sync script helpers
web/scripts/.sync-webgui-repo-*
# Activation code data
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php

View File

@@ -1 +1 @@
{".":"4.4.0"}
{".":"4.6.1"}

View File

@@ -1,3 +1,13 @@
Project License Notice
----------------------
This project is licensed under the terms of the GNU General Public License version 2,
**or (at your option) any later version** published by the Free Software Foundation.
The full text of the GNU GPL v2.0 is provided below for reference.
----------------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991

4
api/.env.staging Normal file
View File

@@ -0,0 +1,4 @@
ENVIRONMENT="staging"
NODE_ENV="production"
PORT="/var/run/unraid-api.sock"
MOTHERSHIP_GRAPHQL_LINK="https://staging.mothership.unraid.net/ws"

5
api/.gqlconfig Normal file
View File

@@ -0,0 +1,5 @@
{
schema: {
files: 'src/graphql/schema/types/**/*.graphql'
}
}

11
api/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"recommendations": [
"mikestead.dotenv",
"eamodio.gitlens",
"dbaeumer.vscode-eslint",
"antfu.goto-alias",
"bierner.markdown-mermaid",
"github.vscode-pull-request-github",
"bierner.markdown-preview-github-styles"
]
}

View File

@@ -1,5 +1,57 @@
# Changelog
## [4.6.1](https://github.com/unraid/api/compare/v4.6.0...v4.6.1) (2025-04-03)
### Bug Fixes
* don't mv deploy on prod release ([9568aab](https://github.com/unraid/api/commit/9568aabd17fbab9e7e2f06f723ee57dc2026583c))
## [4.6.0](https://github.com/unraid/api/compare/v4.5.0...v4.6.0) (2025-04-03)
### Features
* add gui settings field for sso users ([#1310](https://github.com/unraid/api/issues/1310)) ([5ba3fa6](https://github.com/unraid/api/commit/5ba3fa67a26828f29e1e234c6838e7beaa3fdff3))
### Bug Fixes
* build ([ed67af9](https://github.com/unraid/api/commit/ed67af956802eec95845519997bc15b32c84c6ee))
* **plugin:** flaky masking of benign warning during pnpm install ([#1313](https://github.com/unraid/api/issues/1313)) ([1f10b63](https://github.com/unraid/api/commit/1f10b63c8b015e9a2527f79e15a7042feb2d2aca))
## [4.5.0](https://github.com/unraid/api/compare/v4.4.1...v4.5.0) (2025-04-02)
### Features
* add webgui theme switcher component ([#1304](https://github.com/unraid/api/issues/1304)) ([e2d00dc](https://github.com/unraid/api/commit/e2d00dc3464f9663062ac759a8aad85e61804b91))
* api plugin system & offline versioned dependency vendoring ([#1252](https://github.com/unraid/api/issues/1252)) ([9f492bf](https://github.com/unraid/api/commit/9f492bf2175b1b909d3bec079ce901ba34765eb7))
* **api:** add `unraid-api --delete` command ([#1289](https://github.com/unraid/api/issues/1289)) ([2f09445](https://github.com/unraid/api/commit/2f09445f2ed6b23cd851ca64ac5b84cfde3cbd50))
* basic array controls ([#1291](https://github.com/unraid/api/issues/1291)) ([61fe696](https://github.com/unraid/api/commit/61fe6966caf973eec3d74c67741302dd4b507134))
* basic docker controls ([#1292](https://github.com/unraid/api/issues/1292)) ([12eddf8](https://github.com/unraid/api/commit/12eddf894e1808b61f5d4e007f3a7a39a3f2e4d6))
* copy to webgui repo script docs + wc build options ([#1285](https://github.com/unraid/api/issues/1285)) ([e54f189](https://github.com/unraid/api/commit/e54f189630f70aeff5af6bdef4271f0a01fedb74))
### Bug Fixes
* additional url fixes ([4b2763c](https://github.com/unraid/api/commit/4b2763c7f9d8b85d5b0ce066dfc9a9a80a115658))
* **api:** redirect benign pnpm postinstall warning to log file ([#1290](https://github.com/unraid/api/issues/1290)) ([7fb7849](https://github.com/unraid/api/commit/7fb78494cb23630f60a889e6252fc06754e14ef9))
* **deps:** update dependency chalk to v5 ([#1296](https://github.com/unraid/api/issues/1296)) ([6bed638](https://github.com/unraid/api/commit/6bed63805ff026be98a8e20c4d8a37cd47048357))
* **deps:** update dependency diff to v7 ([#1297](https://github.com/unraid/api/issues/1297)) ([3c6683c](https://github.com/unraid/api/commit/3c6683c81422a088c13e9545aaecececd78b8628))
* disable all config watchers ([#1306](https://github.com/unraid/api/issues/1306)) ([5c1b435](https://github.com/unraid/api/commit/5c1b4352cf71d8525f667822f8ca202e2934f463))
* extract callbacks to library ([#1280](https://github.com/unraid/api/issues/1280)) ([2266139](https://github.com/unraid/api/commit/226613974258f15d39932de94316a54aec2e29d2))
* OEM plugin issues ([#1288](https://github.com/unraid/api/issues/1288)) ([d5a3d0d](https://github.com/unraid/api/commit/d5a3d0dfac214fc433c2c0aec578de564a990dd4))
* replace files lost during pruning ([d0d2ff6](https://github.com/unraid/api/commit/d0d2ff65ed2d51223414e50bb1c2ecf82e32a071))
## [4.4.1](https://github.com/unraid/api/compare/v4.4.0...v4.4.1) (2025-03-26)
### Bug Fixes
* .env.production from allowing console logs on build ([#1273](https://github.com/unraid/api/issues/1273)) ([32acc2d](https://github.com/unraid/api/commit/32acc2d27c8bb565b38a66d8233030de3711ea12))
* patch version override logic incorrect ([#1275](https://github.com/unraid/api/issues/1275)) ([6a59756](https://github.com/unraid/api/commit/6a597561a3e21c27fff8d4530cf59cf382eaa015))
## [4.4.0](https://github.com/unraid/api/compare/v4.3.1...v4.4.0) (2025-03-25)

View File

@@ -0,0 +1 @@
┘[5╢╦О яb┴ ю└;R╛леЩ²ДА├y÷шd│яя╛Еlя▓ё"Hи╜ь;QДs≈@Вы▄╠╩1·Qy╓к|й╔+╨фM)X9jя▄тГО⌠1а2WHщ'│.ЕJё-╨MPгS╜╧:Ю▓]o9^ЮО0┴$"░ l^`╪ >3к:╦я ЯО┤q~ёш≈└с ш5ёЗ=р╟─]╗IWf╥и ⌡?:У2ВоE5[р╨Ш(÷╤Е+о│ШIмAч²%╞╓дq:ё╤эb╣┼

1
api/dev/data/machine-id Normal file
View File

@@ -0,0 +1 @@
d0b5433294c110f1eed72bdb63910a9a

View File

@@ -0,0 +1 @@
version="6.12.0-beta5"

BIN
api/dev/dynamix/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1 @@
case-model.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

0
api/dev/states/devs.ini Normal file
View File

View File

@@ -1,5 +1,5 @@
[api]
version="4.1.3"
version="4.4.1"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
@@ -20,5 +20,5 @@ dynamicRemoteAccessType="DISABLED"
ssoSubIds=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
[connectionStatus]
minigraph="PRE_INIT"
minigraph="ERROR_RETRYING"
upnpStatus=""

View File

@@ -0,0 +1,30 @@
[eth0]
DHCP_KEEPRESOLV="no"
DNS_SERVER1="1.1.1.1"
DNS_SERVER2="8.8.8.8"
DHCP6_KEEPRESOLV="no"
BONDING="yes"
BONDNAME=""
BONDNICS="eth0,eth1,eth2,eth3"
BONDING_MODE="1"
BONDING_MIIMON="100"
BRIDGING="yes"
BRNAME=""
BRNICS="bond0"
BRSTP="0"
BRFD="0"
DESCRIPTION:0=""
PROTOCOL:0=""
USE_DHCP:0="yes"
IPADDR:0="192.168.1.150"
NETMASK:0="255.255.255.0"
GATEWAY:0="192.168.1.1"
METRIC:0=""
USE_DHCP6:0=""
IPADDR6:0=""
NETMASK6:0=""
GATEWAY6:0=""
METRIC6:0=""
PRIVACY6:0=""
MTU=""
TYPE="access"

190
api/dev/states/sec.ini Normal file
View File

@@ -0,0 +1,190 @@
["disk1"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk2"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk3"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk4"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk5"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk6"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk7"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk8"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk9"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk10"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk11"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk12"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk13"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk14"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk15"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk16"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk17"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk18"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk19"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk20"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk21"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["disk22"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["abc"]
export="e"
fruit="no"
caseSensitive="auto"
security="public"
readList=""
writeList=""
volsizelimit=""
["flash"]
export="e"
fruit="no"
security="public"
readList=""
writeList=""

View File

@@ -0,0 +1,92 @@
["disk1"]
export="-"
security="public"
hostList=""
["disk2"]
export="-"
security="public"
hostList=""
["disk3"]
export="-"
security="public"
hostList=""
["disk4"]
export="-"
security="public"
hostList=""
["disk5"]
export="-"
security="public"
hostList=""
["disk6"]
export="-"
security="public"
hostList=""
["disk7"]
export="-"
security="public"
hostList=""
["disk8"]
export="-"
security="public"
hostList=""
["disk9"]
export="-"
security="public"
hostList=""
["disk10"]
export="-"
security="public"
hostList=""
["disk11"]
export="-"
security="public"
hostList=""
["disk12"]
export="-"
security="public"
hostList=""
["disk13"]
export="-"
security="public"
hostList=""
["disk14"]
export="-"
security="public"
hostList=""
["disk15"]
export="-"
security="public"
hostList=""
["disk16"]
export="-"
security="public"
hostList=""
["disk17"]
export="-"
security="public"
hostList=""
["disk18"]
export="-"
security="public"
hostList=""
["disk19"]
export="-"
security="public"
hostList=""
["disk20"]
export="-"
security="public"
hostList=""
["disk21"]
export="-"
security="public"
hostList=""
["disk22"]
export="-"
security="public"
hostList=""
["abc"]
export="-"
security="public"
hostList=""

68
api/dev/states/shares.ini Normal file
View File

@@ -0,0 +1,68 @@
["appdata"]
name="appdata"
nameOrig="appdata"
comment=""
allocator="highwater"
splitLevel=""
floor="0"
include=""
exclude=""
useCache="no"
cachePool="cache"
cow="auto"
color="yellow-on"
size="0"
free="9091184"
used="32831348"
luksStatus="0"
["domains"]
name="domains"
nameOrig="domains"
comment="saved VM instances"
allocator="highwater"
splitLevel="1"
floor="0"
include=""
exclude=""
useCache="prefer"
cachePool="cache"
cow="auto"
color="yellow-on"
size="0"
free="9091184"
used="32831348"
luksStatus="0"
["isos"]
name="isos"
nameOrig="isos"
comment="ISO images"
allocator="highwater"
splitLevel=""
floor="0"
include=""
exclude=""
useCache="yes"
cachePool="cache"
cow="auto"
color="yellow-on"
size="0"
free="9091184"
used="32831348"
luksStatus="0"
["system"]
name="system"
nameOrig="system"
comment="system data"
allocator="highwater"
splitLevel="1"
floor="0"
include=""
exclude=""
useCache="prefer"
cachePool="cache"
cow="auto"
color="yellow-on"
size="0"
free="9091184"
used="32831348"
luksStatus="0"

15
api/dev/states/users.ini Normal file
View File

@@ -0,0 +1,15 @@
["root"]
idx="0"
name="root"
desc="Console and webGui login account"
passwd="yes"
["xo"]
idx="1"
name="xo"
desc=""
passwd="yes"
["test_user"]
idx="2"
name="test_user"
desc=""
passwd="no"

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.4.0",
"version": "4.6.1",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -8,7 +8,7 @@
},
"repository": "git@github.com:unraid/api.git",
"author": "Lime Technology, Inc. <unraid.net>",
"license": "GPL-2.0-only",
"license": "GPL-2.0-or-later",
"engines": {
"pnpm": ">=8.0.0"
},
@@ -17,6 +17,7 @@
"start": "node dist/main.js",
"dev": "vite",
"command": "pnpm run build && clear && ./dist/cli.js",
"command:raw": "./dist/cli.js",
"// Build and Deploy": "",
"build": "vite build --mode=production",
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
@@ -77,6 +78,7 @@
"cacheable-lookup": "^7.0.0",
"camelcase-keys": "^9.1.3",
"casbin": "^5.32.0",
"change-case": "^5.4.4",
"chokidar": "^4.0.1",
"cli-table": "^0.3.11",
"command-exists": "^1.2.9",
@@ -143,7 +145,7 @@
"@graphql-codegen/typed-document-node": "^5.0.11",
"@graphql-codegen/typescript": "^4.1.1",
"@graphql-codegen/typescript-operations": "^4.3.1",
"@graphql-codegen/typescript-resolvers": "4.4.4",
"@graphql-codegen/typescript-resolvers": "4.5.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@nestjs/testing": "^11.0.11",
@@ -186,6 +188,7 @@
"rollup-plugin-node-externals": "^8.0.0",
"standard-version": "^9.5.0",
"tsx": "^4.19.2",
"type-fest": "^4.37.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"unplugin-swc": "^1.5.1",
@@ -201,5 +204,5 @@
}
},
"private": true,
"packageManager": "pnpm@10.6.5"
"packageManager": "pnpm@10.7.1"
}

View File

@@ -25,6 +25,8 @@ try {
// Update the package.json version to the deployment version
parsedPackageJson.version = deploymentVersion;
// omit dev dependencies from release build
parsedPackageJson.devDependencies = {};
// Create a temporary directory for packaging
await mkdir('./deploy/pack/', { recursive: true });
@@ -36,9 +38,18 @@ try {
// Change to the pack directory and install dependencies
cd('./deploy/pack');
console.log('Installing production dependencies...');
console.log('Building production pnpm store...');
$.verbose = true;
await $`pnpm install --prod --ignore-workspace --node-linker hoisted`;
await $`pnpm install --prod --ignore-workspace --store-dir=../.pnpm-store`;
await $`rm -rf node_modules`; // Don't include node_modules in final package
const sudoCheck = await $`command -v sudo`.nothrow();
const SUDO = sudoCheck.exitCode === 0 ? 'sudo' : '';
await $`${SUDO} chown -R 0:0 ../.pnpm-store`;
await $`XZ_OPT=-5 tar -cJf ../packed-pnpm-store.txz ../.pnpm-store`;
await $`${SUDO} rm -rf ../.pnpm-store`;
// chmod the cli
await $`chmod +x ./dist/cli.js`;

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Returns generated data');

View File

@@ -0,0 +1,5 @@
import { test } from 'vitest';
test.todo('Adds a disk to the array');
test.todo('Fails to add the disk if the array is started');

View File

@@ -0,0 +1,5 @@
import { test } from 'vitest';
test.todo('Removes a disk from the array');
test.todo('Fails to remove the disk if the array is started');

View File

@@ -0,0 +1,5 @@
import { test } from 'vitest';
test.todo('Starts the array');
test.todo('Stops the array');

View File

@@ -0,0 +1,7 @@
import { test } from 'vitest';
test.todo('Can start a parity check');
test.todo('Can pause a parity check');
test.todo('Can start a parity check');

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Returns the current content');

View File

@@ -0,0 +1,5 @@
import { test } from 'vitest';
test.todo('Returns a single disk by ID');
test.todo('Returns nothing if no disk matches the ID');

View File

@@ -0,0 +1,5 @@
import { test } from 'vitest';
test.todo('Returns all the Docker containers');
test.todo('Returns running Docker containers');

View File

@@ -0,0 +1,7 @@
import { test } from 'vitest';
test.todo('Returns all USB devices');
test.todo('Returns all PCI-e devices');
test.todo('Returns all audio devices');

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Gets total count of Docker containers installed/running');

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Returns baseboard info');

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Gets CPU info');

View File

@@ -1,23 +0,0 @@
import { expect, test, vi } from 'vitest';
import type { SliceState } from '@app/store/modules/emhttp.js';
import { getters } from '@app/store/index.js';
test('Returns true if the array is started', async () => {
vi.spyOn(getters, 'emhttp').mockImplementation(
() => ({ var: { mdState: 'STARTED' } }) as unknown as SliceState
);
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running.js');
expect(arrayIsRunning()).toBe(true);
vi.spyOn(getters, 'emhttp').mockReset();
});
test('Returns false if the array is stopped', async () => {
vi.spyOn(getters, 'emhttp').mockImplementation(
() => ({ var: { mdState: 'Stopped' } }) as unknown as SliceState
);
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running.js');
expect(arrayIsRunning()).toBe(false);
vi.spyOn(getters, 'emhttp').mockReset();
});

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Sleeps atomically for n milliseconds');

View File

@@ -0,0 +1,3 @@
import { test } from 'vitest';
test.todo('Returns machine-id');

View File

@@ -20,7 +20,16 @@ const getUnraidApiLocation = async () => {
};
try {
await CommandFactory.run(CliModule, {
// Register plugins and create a dynamic module configuration
const dynamicModule = await CliModule.registerWithPlugins();
// Create a new class that extends CliModule with the dynamic configuration
const DynamicCliModule = class extends CliModule {
static module = dynamicModule.module;
static imports = dynamicModule.imports;
static providers = dynamicModule.providers;
};
await CommandFactory.run(DynamicCliModule, {
cliName: 'unraid-api',
logger: LOG_LEVEL === 'TRACE' ? new LogService() : false, // - enable this to see nest initialization issues
completion: {

View File

@@ -2,7 +2,6 @@ import { GraphQLError } from 'graphql';
import { sum } from 'lodash-es';
import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types.js';
import { getServerIdentifier } from '@app/core/utils/server-identifier.js';
import { ArrayDiskType } from '@app/graphql/generated/api/types.js';
import { store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';

View File

@@ -1,7 +1,6 @@
import fs from 'fs';
import camelCaseKeys from 'camelcase-keys';
import { ContainerInfo } from 'dockerode';
import type { ContainerPort, Docker, DockerContainer } from '@app/graphql/generated/api/types.js';
import { dockerLogger } from '@app/core/log.js';
@@ -11,13 +10,16 @@ import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/ty
import { getters, store } from '@app/store/index.js';
import { updateDockerState } from '@app/store/modules/docker.js';
export interface ContainerListingOptions {
useCache?: boolean;
}
/**
* Get all Docker containers.
* @returns All the in/active Docker containers on the system.
*/
export const getDockerContainers = async (
{ useCache } = { useCache: true }
{ useCache }: ContainerListingOptions = { useCache: true }
): Promise<Array<DockerContainer>> => {
const dockerState = getters.docker();
if (useCache && dockerState.containers) {

View File

@@ -0,0 +1,2 @@
export type Device = Record<string, unknown>;
export type Devices = Array<Record<string, unknown>>;

View File

@@ -1,8 +1,8 @@
import { got } from 'got';
import { AppError } from '@app/core/errors/app-error.js';
import { logger } from '@app/core/log.js';
import { type LooseObject } from '@app/core/types/index.js';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
import { DRY_RUN } from '@app/environment.js';
import { getters } from '@app/store/index.js';
@@ -27,10 +27,15 @@ export const emcmd = async (commands: LooseObject) => {
// Ensure we only log on dry-run
return;
}
// Untested, this code is unused right now so going to assume it's probably not working well anyway, swapped
// to got to remove this request-promise dependency
return got
.get(url, { searchParams: { ...commands, csrf_token: csrfToken } })
.catch(catchHandlers.emhttpd);
// return request.get(url, options).catch(catchHandlers.emhttpd);
.get(url, {
enableUnixSockets: true,
searchParams: { ...commands, csrf_token: csrfToken },
})
.catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') {
throw new AppError('emhttpd socket unavailable.');
}
throw error;
});
};

View File

@@ -0,0 +1,3 @@
import { extname } from 'path';
export const getExtensionFromPath = (filePath: string): string => extname(filePath);

View File

@@ -3,36 +3,57 @@ import { homedir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const getPackageJsonVersion = () => {
import type { PackageJson, SetRequired } from 'type-fest';
/**
* Tries to get the package.json at the given location.
* @param location - The location of the package.json file, relative to the current file
* @returns The package.json object or undefined if unable to read
*/
function readPackageJson(location: string): PackageJson | undefined {
try {
// Try different possible locations for package.json
const possibleLocations = ['../package.json', '../../package.json'];
for (const location of possibleLocations) {
try {
const packageJsonUrl = import.meta.resolve(location);
const packageJsonPath = fileURLToPath(packageJsonUrl);
const packageJson = readFileSync(packageJsonPath, 'utf-8');
const packageJsonObject = JSON.parse(packageJson);
if (packageJsonObject.version) {
return packageJsonObject.version;
}
} catch {
// Continue to next location if this one fails
}
let packageJsonPath: string;
try {
const packageJsonUrl = import.meta.resolve(location);
packageJsonPath = fileURLToPath(packageJsonUrl);
} catch {
// Fallback (e.g. for local development): resolve the path relative to this module
packageJsonPath = fileURLToPath(new URL(location, import.meta.url));
}
// If we get here, we couldn't find a valid package.json in any location
console.error('Could not find package.json in any of the expected locations');
return undefined;
} catch (error) {
console.error('Failed to load package.json:', error);
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
return JSON.parse(packageJsonRaw) as PackageJson;
} catch {
return undefined;
}
}
/**
* Retrieves the Unraid API package.json. Throws if unable to find.
* This should be considered a fatal error.
*
* @returns The package.json object
*/
export const getPackageJson = () => {
const packageJson = readPackageJson('../package.json') || readPackageJson('../../package.json');
if (!packageJson) {
throw new Error('Could not find package.json in any of the expected locations');
}
return packageJson as SetRequired<PackageJson, 'version' | 'dependencies'>;
};
export const API_VERSION =
process.env.npm_package_version ?? getPackageJsonVersion() ?? new Error('API_VERSION not set');
/**
* Returns list of runtime dependencies from the Unraid-API package.json. Returns undefined if
* the package.json or its dependency object cannot be found or read.
*
* Does not log or produce side effects.
* @returns The names of all runtime dependencies. Undefined if failed.
*/
export const getPackageJsonDependencies = (): string[] | undefined => {
const { dependencies } = getPackageJson();
return Object.keys(dependencies);
};
export const API_VERSION = process.env.npm_package_version ?? getPackageJson().version;
export const NODE_ENV =
(process.env.NODE_ENV as 'development' | 'test' | 'staging' | 'production') ?? 'production';

View File

@@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types.js';
import { z } from 'zod'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, arrayDiskInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerMutations, DockerMutationsstartContainerArgs, DockerMutationsstopContainerArgs, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
type Properties<T> = Required<{
@@ -25,6 +25,8 @@ export const ArrayPendingStateSchema = z.nativeEnum(ArrayPendingState);
export const ArrayStateSchema = z.nativeEnum(ArrayState);
export const ArrayStateInputStateSchema = z.nativeEnum(ArrayStateInputState);
export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState);
export const ContainerPortTypeSchema = z.nativeEnum(ContainerPortType);
@@ -158,7 +160,8 @@ export function ApiSettingsInputSchema(): z.ZodObject<Properties<ApiSettingsInpu
extraOrigins: z.array(z.string()).nullish(),
forwardType: WAN_FORWARD_TYPESchema.nullish(),
port: z.number().nullish(),
sandbox: z.boolean().nullish()
sandbox: z.boolean().nullish(),
ssoUserIds: z.array(z.string()).nullish()
})
}
@@ -213,6 +216,67 @@ export function ArrayDiskSchema(): z.ZodObject<Properties<ArrayDisk>> {
})
}
export function ArrayDiskInputSchema(): z.ZodObject<Properties<ArrayDiskInput>> {
return z.object({
id: z.string(),
slot: z.number().nullish()
})
}
export function ArrayMutationsSchema(): z.ZodObject<Properties<ArrayMutations>> {
return z.object({
__typename: z.literal('ArrayMutations').optional(),
addDiskToArray: ArrayTypeSchema().nullish(),
clearArrayDiskStatistics: z.record(z.string(), z.any()).nullish(),
mountArrayDisk: DiskSchema().nullish(),
removeDiskFromArray: ArrayTypeSchema().nullish(),
setState: ArrayTypeSchema().nullish(),
unmountArrayDisk: DiskSchema().nullish()
})
}
export function ArrayMutationsaddDiskToArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsaddDiskToArrayArgs>> {
return z.object({
input: z.lazy(() => ArrayDiskInputSchema().nullish())
})
}
export function ArrayMutationsclearArrayDiskStatisticsArgsSchema(): z.ZodObject<Properties<ArrayMutationsclearArrayDiskStatisticsArgs>> {
return z.object({
id: z.string()
})
}
export function ArrayMutationsmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsmountArrayDiskArgs>> {
return z.object({
id: z.string()
})
}
export function ArrayMutationsremoveDiskFromArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsremoveDiskFromArrayArgs>> {
return z.object({
input: z.lazy(() => ArrayDiskInputSchema().nullish())
})
}
export function ArrayMutationssetStateArgsSchema(): z.ZodObject<Properties<ArrayMutationssetStateArgs>> {
return z.object({
input: z.lazy(() => ArrayStateInputSchema().nullish())
})
}
export function ArrayMutationsunmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsunmountArrayDiskArgs>> {
return z.object({
id: z.string()
})
}
export function ArrayStateInputSchema(): z.ZodObject<Properties<ArrayStateInput>> {
return z.object({
desiredState: z.lazy(() => ArrayStateInputStateSchema)
})
}
export function BaseboardSchema(): z.ZodObject<Properties<Baseboard>> {
return z.object({
__typename: z.literal('Baseboard').optional(),
@@ -299,7 +363,8 @@ export function ConnectSettingsValuesSchema(): z.ZodObject<Properties<ConnectSet
extraOrigins: z.array(z.string()),
forwardType: WAN_FORWARD_TYPESchema.nullish(),
port: z.number().nullish(),
sandbox: z.boolean()
sandbox: z.boolean(),
ssoUserIds: z.array(z.string())
})
}
@@ -438,6 +503,7 @@ export function DockerSchema(): z.ZodObject<Properties<Docker>> {
__typename: z.literal('Docker').optional(),
containers: z.array(DockerContainerSchema()).nullish(),
id: z.string(),
mutations: DockerMutationsSchema(),
networks: z.array(DockerNetworkSchema()).nullish()
})
}
@@ -463,6 +529,26 @@ export function DockerContainerSchema(): z.ZodObject<Properties<DockerContainer>
})
}
export function DockerMutationsSchema(): z.ZodObject<Properties<DockerMutations>> {
return z.object({
__typename: z.literal('DockerMutations').optional(),
startContainer: DockerContainerSchema(),
stopContainer: DockerContainerSchema()
})
}
export function DockerMutationsstartContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstartContainerArgs>> {
return z.object({
id: z.string()
})
}
export function DockerMutationsstopContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstopContainerArgs>> {
return z.object({
id: z.string()
})
}
export function DockerNetworkSchema(): z.ZodObject<Properties<DockerNetwork>> {
return z.object({
__typename: z.literal('DockerNetwork').optional(),
@@ -1303,13 +1389,6 @@ export function addUserInputSchema(): z.ZodObject<Properties<addUserInput>> {
})
}
export function arrayDiskInputSchema(): z.ZodObject<Properties<arrayDiskInput>> {
return z.object({
id: z.string(),
slot: z.number().nullish()
})
}
export function deleteUserInputSchema(): z.ZodObject<Properties<deleteUserInput>> {
return z.object({
name: z.string()

View File

@@ -107,6 +107,8 @@ export type ApiSettingsInput = {
* If false, the GraphQL sandbox will be disabled and only the production API will be available.
*/
sandbox?: InputMaybe<Scalars['Boolean']['input']>;
/** A list of Unique Unraid Account ID's. */
ssoUserIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type ArrayType = Node & {
@@ -191,6 +193,13 @@ export enum ArrayDiskFsColor {
YELLOW_ON = 'yellow_on'
}
export type ArrayDiskInput = {
/** Disk ID */
id: Scalars['ID']['input'];
/** The slot for the disk */
slot?: InputMaybe<Scalars['Int']['input']>;
};
export enum ArrayDiskStatus {
/** disabled, old disk still present */
DISK_DSBL = 'DISK_DSBL',
@@ -223,6 +232,49 @@ export enum ArrayDiskType {
PARITY = 'Parity'
}
export type ArrayMutations = {
__typename?: 'ArrayMutations';
/** Add new disk to array */
addDiskToArray?: Maybe<ArrayType>;
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
mountArrayDisk?: Maybe<Disk>;
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
removeDiskFromArray?: Maybe<ArrayType>;
/** Set array state */
setState?: Maybe<ArrayType>;
unmountArrayDisk?: Maybe<Disk>;
};
export type ArrayMutationsaddDiskToArrayArgs = {
input?: InputMaybe<ArrayDiskInput>;
};
export type ArrayMutationsclearArrayDiskStatisticsArgs = {
id: Scalars['ID']['input'];
};
export type ArrayMutationsmountArrayDiskArgs = {
id: Scalars['ID']['input'];
};
export type ArrayMutationsremoveDiskFromArrayArgs = {
input?: InputMaybe<ArrayDiskInput>;
};
export type ArrayMutationssetStateArgs = {
input?: InputMaybe<ArrayStateInput>;
};
export type ArrayMutationsunmountArrayDiskArgs = {
id: Scalars['ID']['input'];
};
export enum ArrayPendingState {
/** Array has no data disks */
NO_DATA_DISKS = 'no_data_disks',
@@ -259,6 +311,18 @@ export enum ArrayState {
TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS'
}
export type ArrayStateInput = {
/** Array state */
desiredState: ArrayStateInputState;
};
export enum ArrayStateInputState {
/** Start array */
START = 'START',
/** Stop array */
STOP = 'STOP'
}
export type Baseboard = {
__typename?: 'Baseboard';
assetTag?: Maybe<Scalars['String']['output']>;
@@ -346,6 +410,8 @@ export type ConnectSettingsValues = {
* If false, the GraphQL sandbox is disabled and only the production API will be available.
*/
sandbox: Scalars['Boolean']['output'];
/** A list of Unique Unraid Account ID's. */
ssoUserIds: Array<Scalars['String']['output']>;
};
export type ConnectSignInInput = {
@@ -493,6 +559,7 @@ export type Docker = Node & {
__typename?: 'Docker';
containers?: Maybe<Array<DockerContainer>>;
id: Scalars['ID']['output'];
mutations: DockerMutations;
networks?: Maybe<Array<DockerNetwork>>;
};
@@ -516,6 +583,22 @@ export type DockerContainer = {
status: Scalars['String']['output'];
};
export type DockerMutations = {
__typename?: 'DockerMutations';
startContainer: DockerContainer;
stopContainer: DockerContainer;
};
export type DockerMutationsstartContainerArgs = {
id: Scalars['ID']['input'];
};
export type DockerMutationsstopContainerArgs = {
id: Scalars['ID']['input'];
};
export type DockerNetwork = {
__typename?: 'DockerNetwork';
attachable: Scalars['Boolean']['output'];
@@ -731,8 +814,6 @@ export type Mount = {
export type Mutation = {
__typename?: 'Mutation';
/** Add new disk to array */
addDiskToArray?: Maybe<ArrayType>;
addPermission: Scalars['Boolean']['output'];
addRoleForApiKey: Scalars['Boolean']['output'];
addRoleForUser: Scalars['Boolean']['output'];
@@ -742,9 +823,9 @@ export type Mutation = {
/** Marks a notification as archived. */
archiveNotification: Notification;
archiveNotifications: NotificationOverview;
array?: Maybe<ArrayMutations>;
/** Cancel parity check */
cancelParityCheck?: Maybe<Scalars['JSON']['output']>;
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
connectSignIn: Scalars['Boolean']['output'];
connectSignOut: Scalars['Boolean']['output'];
createApiKey: ApiKeyWithSecret;
@@ -756,29 +837,21 @@ export type Mutation = {
deleteUser?: Maybe<User>;
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
login?: Maybe<Scalars['String']['output']>;
mountArrayDisk?: Maybe<Disk>;
/** Pause parity check */
pauseParityCheck?: Maybe<Scalars['JSON']['output']>;
reboot?: Maybe<Scalars['String']['output']>;
/** Reads each notification to recompute & update the overview. */
recalculateOverview: NotificationOverview;
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
removeDiskFromArray?: Maybe<ArrayType>;
removeRoleFromApiKey: Scalars['Boolean']['output'];
/** Resume parity check */
resumeParityCheck?: Maybe<Scalars['JSON']['output']>;
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
setupRemoteAccess: Scalars['Boolean']['output'];
shutdown?: Maybe<Scalars['String']['output']>;
/** Start array */
startArray?: Maybe<ArrayType>;
/** Start parity check */
startParityCheck?: Maybe<Scalars['JSON']['output']>;
/** Stop array */
stopArray?: Maybe<ArrayType>;
unarchiveAll: NotificationOverview;
unarchiveNotifications: NotificationOverview;
unmountArrayDisk?: Maybe<Disk>;
/** Marks a notification as unread. */
unreadNotification: Notification;
/**
@@ -789,11 +862,6 @@ export type Mutation = {
};
export type MutationaddDiskToArrayArgs = {
input?: InputMaybe<arrayDiskInput>;
};
export type MutationaddPermissionArgs = {
input: AddPermissionInput;
};
@@ -829,11 +897,6 @@ export type MutationarchiveNotificationsArgs = {
};
export type MutationclearArrayDiskStatisticsArgs = {
id: Scalars['ID']['input'];
};
export type MutationconnectSignInArgs = {
input: ConnectSignInInput;
};
@@ -871,16 +934,6 @@ export type MutationloginArgs = {
};
export type MutationmountArrayDiskArgs = {
id: Scalars['ID']['input'];
};
export type MutationremoveDiskFromArrayArgs = {
input?: InputMaybe<arrayDiskInput>;
};
export type MutationremoveRoleFromApiKeyArgs = {
input: RemoveRoleFromApiKeyInput;
};
@@ -911,11 +964,6 @@ export type MutationunarchiveNotificationsArgs = {
};
export type MutationunmountArrayDiskArgs = {
id: Scalars['ID']['input'];
};
export type MutationunreadNotificationArgs = {
id: Scalars['String']['input'];
};
@@ -1823,13 +1871,6 @@ export type addUserInput = {
password: Scalars['String']['input'];
};
export type arrayDiskInput = {
/** Disk ID */
id: Scalars['ID']['input'];
/** The slot for the disk */
slot?: InputMaybe<Scalars['Int']['input']>;
};
export type deleteUserInput = {
name: Scalars['String']['input'];
};
@@ -1945,10 +1986,14 @@ export type ResolversTypes = ResolversObject<{
ArrayCapacity: ResolverTypeWrapper<ArrayCapacity>;
ArrayDisk: ResolverTypeWrapper<ArrayDisk>;
ArrayDiskFsColor: ArrayDiskFsColor;
ArrayDiskInput: ArrayDiskInput;
ArrayDiskStatus: ArrayDiskStatus;
ArrayDiskType: ArrayDiskType;
ArrayMutations: ResolverTypeWrapper<ArrayMutations>;
ArrayPendingState: ArrayPendingState;
ArrayState: ArrayState;
ArrayStateInput: ArrayStateInput;
ArrayStateInputState: ArrayStateInputState;
Baseboard: ResolverTypeWrapper<Baseboard>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
Capacity: ResolverTypeWrapper<Capacity>;
@@ -1978,6 +2023,7 @@ export type ResolversTypes = ResolversObject<{
Display: ResolverTypeWrapper<Display>;
Docker: ResolverTypeWrapper<Docker>;
DockerContainer: ResolverTypeWrapper<DockerContainer>;
DockerMutations: ResolverTypeWrapper<DockerMutations>;
DockerNetwork: ResolverTypeWrapper<DockerNetwork>;
DynamicRemoteAccessStatus: ResolverTypeWrapper<DynamicRemoteAccessStatus>;
DynamicRemoteAccessType: DynamicRemoteAccessType;
@@ -2057,7 +2103,6 @@ export type ResolversTypes = ResolversObject<{
WAN_FORWARD_TYPE: WAN_FORWARD_TYPE;
Welcome: ResolverTypeWrapper<Welcome>;
addUserInput: addUserInput;
arrayDiskInput: arrayDiskInput;
deleteUserInput: deleteUserInput;
mdState: mdState;
registrationType: registrationType;
@@ -2079,6 +2124,9 @@ export type ResolversParentTypes = ResolversObject<{
Array: ArrayType;
ArrayCapacity: ArrayCapacity;
ArrayDisk: ArrayDisk;
ArrayDiskInput: ArrayDiskInput;
ArrayMutations: ArrayMutations;
ArrayStateInput: ArrayStateInput;
Baseboard: Baseboard;
Boolean: Scalars['Boolean']['output'];
Capacity: Capacity;
@@ -2102,6 +2150,7 @@ export type ResolversParentTypes = ResolversObject<{
Display: Display;
Docker: Docker;
DockerContainer: DockerContainer;
DockerMutations: DockerMutations;
DockerNetwork: DockerNetwork;
DynamicRemoteAccessStatus: DynamicRemoteAccessStatus;
EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput;
@@ -2165,7 +2214,6 @@ export type ResolversParentTypes = ResolversObject<{
Vms: Vms;
Welcome: Welcome;
addUserInput: addUserInput;
arrayDiskInput: arrayDiskInput;
deleteUserInput: deleteUserInput;
usersInput: usersInput;
}>;
@@ -2250,6 +2298,16 @@ export type ArrayDiskResolvers<ContextType = Context, ParentType extends Resolve
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ArrayMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ArrayMutations'] = ResolversParentTypes['ArrayMutations']> = ResolversObject<{
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsaddDiskToArrayArgs>>;
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<ArrayMutationsclearArrayDiskStatisticsArgs, 'id'>>;
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsmountArrayDiskArgs, 'id'>>;
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsremoveDiskFromArrayArgs>>;
setState?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationssetStateArgs>>;
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsunmountArrayDiskArgs, 'id'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type BaseboardResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Baseboard'] = ResolversParentTypes['Baseboard']> = ResolversObject<{
assetTag?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
manufacturer?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2319,6 +2377,7 @@ export type ConnectSettingsValuesResolvers<ContextType = Context, ParentType ext
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
sandbox?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
ssoUserIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
@@ -2417,6 +2476,7 @@ export type DisplayResolvers<ContextType = Context, ParentType extends Resolvers
export type DockerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Docker'] = ResolversParentTypes['Docker']> = ResolversObject<{
containers?: Resolver<Maybe<Array<ResolversTypes['DockerContainer']>>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
mutations?: Resolver<ResolversTypes['DockerMutations'], ParentType, ContextType>;
networks?: Resolver<Maybe<Array<ResolversTypes['DockerNetwork']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
@@ -2440,6 +2500,12 @@ export type DockerContainerResolvers<ContextType = Context, ParentType extends R
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerMutations'] = ResolversParentTypes['DockerMutations']> = ResolversObject<{
startContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstartContainerArgs, 'id'>>;
stopContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstopContainerArgs, 'id'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerNetworkResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerNetwork'] = ResolversParentTypes['DockerNetwork']> = ResolversObject<{
attachable?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
configFrom?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
@@ -2612,7 +2678,6 @@ export type MountResolvers<ContextType = Context, ParentType extends ResolversPa
}>;
export type MutationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = ResolversObject<{
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationaddDiskToArrayArgs>>;
addPermission?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddPermissionArgs, 'input'>>;
addRoleForApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForApiKeyArgs, 'input'>>;
addRoleForUser?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForUserArgs, 'input'>>;
@@ -2620,8 +2685,8 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
archiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveAllArgs>>;
archiveNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationarchiveNotificationArgs, 'id'>>;
archiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveNotificationsArgs>>;
array?: Resolver<Maybe<ResolversTypes['ArrayMutations']>, ParentType, ContextType>;
cancelParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<MutationclearArrayDiskStatisticsArgs, 'id'>>;
connectSignIn?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationconnectSignInArgs, 'input'>>;
connectSignOut?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
createApiKey?: Resolver<ResolversTypes['ApiKeyWithSecret'], ParentType, ContextType, RequireFields<MutationcreateApiKeyArgs, 'input'>>;
@@ -2631,22 +2696,17 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
deleteUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationdeleteUserArgs, 'input'>>;
enableDynamicRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationenableDynamicRemoteAccessArgs, 'input'>>;
login?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationloginArgs, 'password' | 'username'>>;
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationmountArrayDiskArgs, 'id'>>;
pauseParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
reboot?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
recalculateOverview?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType>;
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationremoveDiskFromArrayArgs>>;
removeRoleFromApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationremoveRoleFromApiKeyArgs, 'input'>>;
resumeParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
setAdditionalAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationsetAdditionalAllowedOriginsArgs, 'input'>>;
setupRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationsetupRemoteAccessArgs, 'input'>>;
shutdown?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
startArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
startParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, Partial<MutationstartParityCheckArgs>>;
stopArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
unarchiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveAllArgs>>;
unarchiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveNotificationsArgs>>;
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationunmountArrayDiskArgs, 'id'>>;
unreadNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationunreadNotificationArgs, 'id'>>;
updateApiSettings?: Resolver<ResolversTypes['ConnectSettingsValues'], ParentType, ContextType, RequireFields<MutationupdateApiSettingsArgs, 'input'>>;
}>;
@@ -3273,6 +3333,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Array?: ArrayResolvers<ContextType>;
ArrayCapacity?: ArrayCapacityResolvers<ContextType>;
ArrayDisk?: ArrayDiskResolvers<ContextType>;
ArrayMutations?: ArrayMutationsResolvers<ContextType>;
Baseboard?: BaseboardResolvers<ContextType>;
Capacity?: CapacityResolvers<ContextType>;
Case?: CaseResolvers<ContextType>;
@@ -3292,6 +3353,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Display?: DisplayResolvers<ContextType>;
Docker?: DockerResolvers<ContextType>;
DockerContainer?: DockerContainerResolvers<ContextType>;
DockerMutations?: DockerMutationsResolvers<ContextType>;
DockerNetwork?: DockerNetworkResolvers<ContextType>;
DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers<ContextType>;
Flash?: FlashResolvers<ContextType>;

View File

@@ -2,7 +2,7 @@ import { mergeTypeDefs } from '@graphql-tools/merge';
import { logger } from '@app/core/log.js';
export const loadTypeDefs = async () => {
export const loadTypeDefs = async (additionalTypeDefs: string[] = []) => {
// TypeScript now knows this returns Record<string, () => Promise<string>>
const typeModules = import.meta.glob('./types/**/*.graphql', { query: '?raw', import: 'default' });
@@ -19,6 +19,7 @@ export const loadTypeDefs = async () => {
if (!files.length) {
throw new Error('No GraphQL type definitions found');
}
files.push(...additionalTypeDefs);
return mergeTypeDefs(files);
} catch (error) {
logger.error('Failed to load GraphQL type definitions:', error);

View File

@@ -3,16 +3,26 @@ type Query {
array: Array!
}
type Mutation {
enum ArrayStateInputState {
"""Start array"""
startArray: Array
START
"""Stop array"""
stopArray: Array
STOP
}
input ArrayStateInput {
"""Array state"""
desiredState: ArrayStateInputState!
}
type ArrayMutations {
"""Set array state"""
setState(input: ArrayStateInput): Array
"""Add new disk to array"""
addDiskToArray(input: arrayDiskInput): Array
addDiskToArray(input: ArrayDiskInput): Array
"""Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error."""
removeDiskFromArray(input: arrayDiskInput): Array
removeDiskFromArray(input: ArrayDiskInput): Array
mountArrayDisk(id: ID!): Disk
unmountArrayDisk(id: ID!): Disk
@@ -20,11 +30,15 @@ type Mutation {
clearArrayDiskStatistics(id: ID!): JSON
}
type Mutation {
array: ArrayMutations
}
type Subscription {
array: Array!
}
input arrayDiskInput {
input ArrayDiskInput {
"""Disk ID"""
id: ID!
"""The slot for the disk"""

View File

@@ -0,0 +1,43 @@
type ApiKeyResponse {
valid: Boolean!
error: String
}
enum MinigraphStatus {
PRE_INIT
CONNECTING
CONNECTED
PING_FAILURE
ERROR_RETRYING
}
type MinigraphqlResponse {
status: MinigraphStatus!
timeout: Int
error: String
}
type CloudResponse {
status: String!
ip: String
error: String
}
type RelayResponse {
status: String!
timeout: String
error: String
}
type Cloud {
error: String
apiKey: ApiKeyResponse!
relay: RelayResponse
minigraphql: MinigraphqlResponse!
cloud: CloudResponse!
allowedOrigins: [String!]!
}
type Query {
cloud: Cloud
}

View File

@@ -39,8 +39,6 @@ input SetupRemoteAccessInput {
port: Port
}
input EnableDynamicRemoteAccessInput {
url: AccessUrlInput!
enabled: Boolean!
@@ -59,59 +57,67 @@ type DynamicRemoteAccessStatus {
}
"""
Intersection type of ApiSettings and RemoteAccess
Intersection type of ApiSettings and RemoteAccess
"""
type ConnectSettingsValues {
"""
If true, the GraphQL sandbox is enabled and available at /graphql.
If false, the GraphQL sandbox is disabled and only the production API will be available.
If true, the GraphQL sandbox is enabled and available at /graphql.
If false, the GraphQL sandbox is disabled and only the production API will be available.
"""
sandbox: Boolean!
"""
A list of origins allowed to interact with the API.
A list of origins allowed to interact with the API.
"""
extraOrigins: [String!]!
"""
The type of WAN access used for Remote Access.
The type of WAN access used for Remote Access.
"""
accessType: WAN_ACCESS_TYPE!
"""
The type of port forwarding used for Remote Access.
The type of port forwarding used for Remote Access.
"""
forwardType: WAN_FORWARD_TYPE
"""
The port used for Remote Access.
The port used for Remote Access.
"""
port: Port
"""
A list of Unique Unraid Account ID's.
"""
ssoUserIds: [String!]!
}
"""
Input should be a subset of ApiSettings that can be updated.
Some field combinations may be required or disallowed. Please refer to each field for more information.
Input should be a subset of ApiSettings that can be updated.
Some field combinations may be required or disallowed. Please refer to each field for more information.
"""
input ApiSettingsInput {
"""
If true, the GraphQL sandbox will be enabled and available at /graphql.
If false, the GraphQL sandbox will be disabled and only the production API will be available.
If true, the GraphQL sandbox will be enabled and available at /graphql.
If false, the GraphQL sandbox will be disabled and only the production API will be available.
"""
sandbox: Boolean
"""
A list of origins allowed to interact with the API.
A list of origins allowed to interact with the API.
"""
extraOrigins: [String!]
"""
The type of WAN access to use for Remote Access.
The type of WAN access to use for Remote Access.
"""
accessType: WAN_ACCESS_TYPE
"""
The type of port forwarding to use for Remote Access.
The type of port forwarding to use for Remote Access.
"""
forwardType: WAN_FORWARD_TYPE
"""
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
Ignored if accessType is DISABLED or forwardType is UPNP.
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
Ignored if accessType is DISABLED or forwardType is UPNP.
"""
port: Port
"""
A list of Unique Unraid Account ID's.
"""
ssoUserIds: [String!]
}
type ConnectSettings implements Node {
@@ -140,8 +146,8 @@ type Mutation {
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
"""
Update the API settings.
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
Update the API settings.
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
"""
updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues!
}
}

View File

@@ -0,0 +1,60 @@
type Query {
"""All Docker containers"""
dockerContainers(all: Boolean): [DockerContainer!]!
}
type Subscription {
dockerContainer(id: ID!): DockerContainer!
dockerContainers: [DockerContainer]
}
enum ContainerPortType {
TCP
UDP
}
type ContainerPort {
ip: String
privatePort: Int
publicPort: Int
type: ContainerPortType
}
enum ContainerState {
RUNNING
EXITED
}
type ContainerHostConfig {
networkMode: String!
}
type ContainerMount {
type: String!
name: String!
source: String!
destination: String!
driver: String!
mode: String!
rw: Boolean!
propagation: String!
}
type DockerContainer {
id: ID!
names: [String!]
image: String!
imageId: String!
command: String!
created: Int!
ports: [ContainerPort!]!
""" (B) Total size of all the files in the container """
sizeRootFs: Long
labels: JSON
state: ContainerState!
status: String!
hostConfig: ContainerHostConfig
networkSettings: JSON
mounts: [JSON]
autoStart: Boolean!
}

View File

@@ -6,4 +6,13 @@ type Docker implements Node {
type Query {
docker: Docker!
}
type DockerMutations {
startContainer(id: ID!): DockerContainer!
stopContainer(id: ID!): DockerContainer!
}
extend type Docker {
mutations: DockerMutations!
}

View File

@@ -0,0 +1,13 @@
type Query {
flash: Flash
}
type Subscription {
flash: Flash!
}
type Flash {
guid: String
vendor: String
product: String
}

View File

@@ -0,0 +1,11 @@
type Info {
"""Count of docker containers"""
apps: InfoApps
}
type InfoApps {
"""How many docker containers are installed"""
installed: Int
"""How many docker containers are running"""
started: Int
}

View File

@@ -0,0 +1,14 @@
type Info {
baseboard: Baseboard
}
type Baseboard {
# Dell Inc.
manufacturer: String!
# 0MD99X
model: String
# A07
version: String
serial: String
assetTag: String
}

View File

@@ -0,0 +1,39 @@
type Info {
cpu: InfoCpu
}
type InfoCpu {
# 'Intel®'
manufacturer: String!
# 'Xeon® L5640'
brand: String!
# 'GenuineIntel'
vendor: String!
# '6'
family: String!
# '44'
model: String!
# '2'
stepping: Int!
# ''
revision: String!
# ''
voltage: String
# '2.27'
speed: Float!
# '1.60'
speedmin: Float!
# '2.26'
speedmax: Float!
# 12
threads: Int!
# 6
cores: Int!
# 1
processors: Long!
# 'LGA1366'
socket: String!
# { l1d: 196608, l1i: 196608, l2: 1, l3: 12 }
cache: JSON!
flags: [String!]
}

View File

@@ -0,0 +1,52 @@
type Info {
devices: Devices
}
type Devices {
gpu: [Gpu]
network: [Network]
pci: [Pci]
usb: [Usb]
}
type Gpu {
id: ID!
type: String!
typeid: String!
vendorname: String!
productid: String!
blacklisted: Boolean!
class: String!
}
type Network {
iface: String
ifaceName: String
ipv4: String
ipv6: String
mac: String
internal: String
operstate: String
type: String
duplex: String
mtu: String
speed: String
carrierChanges: String
}
type Pci {
id: ID!
type: String
typeid: String
vendorname: String
vendorid: String
productname: String
productid: String
blacklisted: String
class: String
}
type Usb {
id: ID!
name: String
}

View File

@@ -0,0 +1,34 @@
type Info {
display: Display
}
type Display {
date: String
number: String
scale: Boolean
tabs: Boolean
users: String
resize: Boolean
wwn: Boolean
total: Boolean
usage: Boolean
banner: String
dashapps: String
theme: Theme
text: Boolean
unit: Temperature
warning: Int
critical: Int
hot: Int
max: Int
locale: String
}
enum Temperature {
C
F
}
enum Theme {
white
}

View File

@@ -0,0 +1,4 @@
type Info {
"""Machine ID"""
machineId: ID
}

View File

@@ -0,0 +1,41 @@
type Info {
memory: InfoMemory
}
type InfoMemory {
max: Long!
total: Long!
free: Long!
used: Long!
active: Long!
available: Long!
buffcache: Long!
swaptotal: Long!
swapused: Long!
swapfree: Long!
layout: [MemoryLayout!]
}
type MemoryLayout {
size: Long!
bank: String
type: MemoryType
clockSpeed: Long
formFactor: MemoryFormFactor
manufacturer: String
partNum: String
serialNum: String
voltageConfigured: Long
voltageMin: Long
voltageMax: Long
}
enum MemoryType {
DDR2
DDR3
DDR4
}
enum MemoryFormFactor {
DIMM
}

View File

@@ -0,0 +1,18 @@
type Info {
os: Os
}
type Os {
platform: String
distro: String
release: String
codename: String
kernel: String
arch: String
hostname: String
codepage: String
logofile: String
serial: String
build: String
uptime: DateTime
}

View File

@@ -0,0 +1,12 @@
type Info {
system: System
}
type System {
manufacturer: String
model: String
version: String
serial: String
uuid: String
sku: String
}

View File

@@ -0,0 +1,32 @@
type Info {
versions: Versions
}
type Versions {
kernel: String
openssl: String
systemOpenssl: String
systemOpensslLib: String
node: String
v8: String
npm: String
yarn: String
pm2: String
gulp: String
grunt: String
git: String
tsc: String
mysql: String
redis: String
mongodb: String
apache: String
nginx: String
php: String
docker: String
postfix: String
postgresql: String
perl: String
python: String
gcc: String
unraid: String
}

View File

@@ -0,0 +1,13 @@
type Query {
owner: Owner
}
type Subscription {
owner: Owner!
}
type Owner {
username: String
url: String
avatar: String
}

View File

@@ -0,0 +1,6 @@
type Mount {
name: String
directory: String
type: String
permissions: String
}

View File

@@ -0,0 +1,60 @@
type Partition {
devlinks: String
devname: String
devpath: String
devtype: String
idAta: String
idAtaDownloadMicrocode: String
idAtaFeatureSetAam: String
idAtaFeatureSetAamCurrentValue: String
idAtaFeatureSetAamEnabled: String
idAtaFeatureSetAamVendorRecommendedValue: String
idAtaFeatureSetApm: String
idAtaFeatureSetApmCurrentValue: String
idAtaFeatureSetApmEnabled: String
idAtaFeatureSetHpa: String
idAtaFeatureSetHpaEnabled: String
idAtaFeatureSetPm: String
idAtaFeatureSetPmEnabled: String
idAtaFeatureSetPuis: String
idAtaFeatureSetPuisEnabled: String
idAtaFeatureSetSecurity: String
idAtaFeatureSetSecurityEnabled: String
idAtaFeatureSetSecurityEnhancedEraseUnitMin: String
idAtaFeatureSetSecurityEraseUnitMin: String
idAtaFeatureSetSmart: String
idAtaFeatureSetSmartEnabled: String
idAtaRotationRateRpm: String
idAtaSata: String
idAtaSataSignalRateGen1: String
idAtaSataSignalRateGen2: String
idAtaWriteCache: String
idAtaWriteCacheEnabled: String
idBus: String
idFsType: String
idFsUsage: String
idFsUuid: String
idFsUuidEnc: String
idModel: String
idModelEnc: String
idPartEntryDisk: String
idPartEntryNumber: String
idPartEntryOffset: String
idPartEntryScheme: String
idPartEntrySize: String
idPartEntryType: String
idPartTableType: String
idPath: String
idPathTag: String
idRevision: String
idSerial: String
idSerialShort: String
idType: String
idWwn: String
idWwnWithExtension: String
major: String
minor: String
partn: String
subsystem: String
usecInitialized: String
}

View File

@@ -35,6 +35,7 @@ export const store = configureStore({
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type ApiStore = typeof store;
export const getters = {
cache: () => store.getState().cache,

View File

@@ -219,6 +219,9 @@ export const config = createSlice({
stateAsArray.push(action.payload);
state.remote.ssoSubIds = stateAsArray.join(',');
},
setSsoUsers(state, action: PayloadAction<string[]>) {
state.remote.ssoSubIds = action.payload.filter((id) => id).join(',');
},
removeSsoUser(state, action: PayloadAction<string | null>) {
if (action.payload === null) {
state.remote.ssoSubIds = '';
@@ -309,6 +312,7 @@ const { actions, reducer } = config;
export const {
addSsoUser,
setSsoUsers,
updateUserConfig,
updateAccessTokens,
updateAllowedOrigins,
@@ -324,6 +328,7 @@ export const {
*/
export const configUpdateActionsFlash = isAnyOf(
addSsoUser,
setSsoUsers,
updateUserConfig,
updateAccessTokens,
updateAllowedOrigins,

View File

@@ -9,7 +9,6 @@ import { store } from '@app/store/index.js';
import { syncInfoApps } from '@app/store/sync/info-apps-sync.js';
import { syncRegistration } from '@app/store/sync/registration-sync.js';
import { FileLoadStatus } from '@app/store/types.js';
import { setupConfigPathWatch } from '@app/store/watch/config-watch.js';
export const startStoreSync = async () => {
// The last state is stored so we don't end up in a loop of writing -> reading -> writing
@@ -45,6 +44,4 @@ export const startStoreSync = async () => {
lastState = state;
});
setupConfigPathWatch();
};

View File

@@ -1,39 +0,0 @@
import { existsSync, writeFileSync } from 'fs';
import { watch } from 'chokidar';
import { logger } from '@app/core/log.js';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js';
import { CHOKIDAR_USEPOLLING, ENVIRONMENT } from '@app/environment.js';
import { getters, store } from '@app/store/index.js';
import { initialState, loadConfigFile, logoutUser } from '@app/store/modules/config.js';
export const setupConfigPathWatch = () => {
const myServersConfigPath = getters.paths()?.['myservers-config'];
if (myServersConfigPath) {
logger.info('Watch Setup on Config Path: %s', myServersConfigPath);
if (!existsSync(myServersConfigPath)) {
const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash'));
writeFileSync(myServersConfigPath, config, 'utf-8');
}
const watcher = watch(myServersConfigPath, {
persistent: true,
ignoreInitial: false,
usePolling: CHOKIDAR_USEPOLLING === true,
})
.on('change', async (change) => {
logger.trace('Config File Changed, Reloading Config %s', change);
await store.dispatch(loadConfigFile());
})
.on('unlink', async () => {
const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash'));
await writeFileSync(myServersConfigPath, config, 'utf-8');
watcher.close();
setupConfigPathWatch();
store.dispatch(logoutUser({ reason: 'Config File was Deleted' }));
});
} else {
logger.error('[FATAL] Failed to setup watch on My Servers Config (Could Not Read Config Path)');
}
};

View File

@@ -11,6 +11,7 @@ import { GraphqlAuthGuard } from '@app/unraid-api/auth/auth.guard.js';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { CronModule } from '@app/unraid-api/cron/cron.module.js';
import { GraphModule } from '@app/unraid-api/graph/graph.module.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { RestModule } from '@app/unraid-api/rest/rest.module.js';
import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.module.js';
@@ -46,6 +47,7 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u
},
]),
UnraidFileModifierModule,
PluginModule.registerPlugins(),
],
controllers: [],
providers: [

View File

@@ -1,6 +1,6 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import crypto from 'crypto';
import { readdir, readFile, writeFile } from 'fs/promises';
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
import { join } from 'path';
import { watch } from 'chokidar';
@@ -23,6 +23,7 @@ import {
import { getters, store } from '@app/store/index.js';
import { setLocalApiKey } from '@app/store/modules/config.js';
import { FileLoadStatus } from '@app/store/types.js';
import { batchProcess } from '@app/utils.js';
@Injectable()
export class ApiKeyService implements OnModuleInit {
@@ -312,4 +313,36 @@ export class ApiKeyService implements OnModuleInit {
basePath: this.basePath,
};
}
/**
* Deletes API keys from the disk and updates the in-memory store.
*
* This method first verifies that all the provided API key IDs exist in the in-memory store.
* If any keys are missing, it throws an Error detailing the missing keys.
* It then deletes the corresponding JSON files concurrently using batch processing.
* If any errors occur during the file deletion process, an array of errors is thrown.
*
* @param ids An array of API key identifiers to delete.
* @throws Error if one or more API keys are not found.
* @throws Array<Error> if errors occur during the file deletion.
*/
public async deleteApiKeys(ids: string[]): Promise<void> {
// First verify all keys exist
const missingKeys = ids.filter((id) => !this.findByField('id', id));
if (missingKeys.length > 0) {
throw new Error(`API keys not found: ${missingKeys.join(', ')}`);
}
// Delete all files in parallel
const { errors, data: deletedIds } = await batchProcess(ids, async (id) => {
await unlink(join(this.basePath, `${id}.json`));
return id;
});
const deletedSet = new Set(deletedIds);
this.memoryApiKeys = this.memoryApiKeys.filter((key) => !deletedSet.has(key.id));
if (errors.length > 0) {
throw errors;
}
}
}

View File

@@ -2,14 +2,17 @@ import { AuthActionVerb } from 'nest-authz';
import { Command, CommandRunner, InquirerService, Option } from 'nest-commander';
import type { Permission } from '@app/graphql/generated/api/types.js';
import type { DeleteApiKeyAnswers } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
import { Resource, Role } from '@app/graphql/generated/api/types.js';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
interface KeyOptions {
name: string;
create: boolean;
delete?: boolean;
description?: string;
roles?: Role[];
permissions?: Permission[];
@@ -17,7 +20,7 @@ interface KeyOptions {
@Command({
name: 'apikey',
description: `Create / Fetch Connect API Keys - use --create with no arguments for a creation wizard`,
description: `Create / Fetch / Delete Connect API Keys - use --create with no arguments for a creation wizard, or --delete to remove keys`,
})
export class ApiKeyCommand extends CommandRunner {
constructor(
@@ -88,8 +91,50 @@ ACTIONS: ${Object.values(AuthActionVerb).join(', ')}`,
return description;
}
async run(_: string[], options: KeyOptions = { create: false, name: '' }): Promise<void> {
@Option({
flags: '--delete',
description: 'Delete selected API keys',
})
parseDelete(): boolean {
return true;
}
/** Prompt the user to select API keys to delete. Then, delete the selected keys. */
private async deleteKeys() {
const allKeys = this.apiKeyService.findAll();
if (allKeys.length === 0) {
this.logger.log('No API keys found to delete');
return;
}
const answers = await this.inquirerService.prompt<DeleteApiKeyAnswers>(
DeleteApiKeyQuestionSet.name,
{}
);
if (!answers.selectedKeys || answers.selectedKeys.length === 0) {
this.logger.log('No keys selected for deletion');
return;
}
try {
await this.apiKeyService.deleteApiKeys(answers.selectedKeys);
this.logger.log(`Successfully deleted ${answers.selectedKeys.length} API keys`);
} catch (error) {
this.logger.error(error as any);
process.exit(1);
}
}
async run(
_: string[],
options: KeyOptions = { create: false, name: '', delete: false }
): Promise<void> {
try {
if (options.delete) {
await this.deleteKeys();
return;
}
const key = this.apiKeyService.findByField('name', options.name);
if (key) {
this.logger.log(key.key);

View File

@@ -0,0 +1,35 @@
import { ChoicesFor, Question, QuestionSet } from 'nest-commander';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
export interface DeleteApiKeyAnswers {
selectedKeys: string[];
}
@QuestionSet({ name: 'delete-api-key' })
export class DeleteApiKeyQuestionSet {
constructor(
private readonly apiKeyService: ApiKeyService,
private readonly logger: LogService
) {}
static name = 'delete-api-key';
@Question({
name: 'selectedKeys',
type: 'checkbox',
message: 'Select API keys to delete:',
})
parseSelectedKeys(keyIds: string[]): string[] {
return keyIds;
}
@ChoicesFor({ name: 'selectedKeys' })
async getKeys() {
return this.apiKeyService.findAll().map((key) => ({
name: `${key.name} (${key.description ?? ''}) [${key.id}]`,
value: key.id,
}));
}
}

View File

@@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
import { CommandRunner } from 'nest-commander';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
import { ApiKeyCommand } from '@app/unraid-api/cli/apikey/api-key.command.js';
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
import { ConfigCommand } from '@app/unraid-api/cli/config.command.js';
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
@@ -23,32 +26,77 @@ import { StatusCommand } from '@app/unraid-api/cli/status.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command.js';
import { VersionCommand } from '@app/unraid-api/cli/version.command.js';
import { ApiPluginDefinition } from '@app/unraid-api/plugin/plugin.interface.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
const DEFAULT_COMMANDS = [
ApiKeyCommand,
ConfigCommand,
DeveloperCommand,
LogsCommand,
ReportCommand,
RestartCommand,
StartCommand,
StatusCommand,
StopCommand,
SwitchEnvCommand,
VersionCommand,
SSOCommand,
ValidateTokenCommand,
AddSSOUserCommand,
RemoveSSOUserCommand,
ListSSOUserCommand,
] as const;
const DEFAULT_PROVIDERS = [
AddApiKeyQuestionSet,
DeleteApiKeyQuestionSet,
AddSSOUserQuestionSet,
RemoveSSOUserQuestionSet,
DeveloperQuestions,
LogService,
PM2Service,
ApiKeyService,
] as const;
type PluginProvider = Provider & {
provide: string | symbol | Type<any>;
useValue?: ApiPluginDefinition;
};
@Module({
providers: [
AddSSOUserCommand,
AddSSOUserQuestionSet,
RemoveSSOUserCommand,
RemoveSSOUserQuestionSet,
ListSSOUserCommand,
LogService,
PM2Service,
StartCommand,
StopCommand,
RestartCommand,
ReportCommand,
ApiKeyService,
ApiKeyCommand,
AddApiKeyQuestionSet,
SwitchEnvCommand,
VersionCommand,
StatusCommand,
SSOCommand,
ValidateTokenCommand,
LogsCommand,
ConfigCommand,
DeveloperCommand,
DeveloperQuestions,
],
imports: [PluginModule],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
})
export class CliModule {}
export class CliModule {
/**
* Get all registered commands
* @returns Array of registered command classes
*/
static getCommands(): Type<CommandRunner>[] {
return [...DEFAULT_COMMANDS];
}
/**
* Register the module with plugin support
* @returns DynamicModule configuration including plugin commands
*/
static async registerWithPlugins(): Promise<DynamicModule> {
const pluginModule = await PluginModule.registerPlugins();
// Get commands from plugins
const pluginCommands: Type<CommandRunner>[] = [];
for (const provider of (pluginModule.providers || []) as PluginProvider[]) {
if (provider.provide !== PluginService && provider.useValue?.commands) {
pluginCommands.push(...provider.useValue.commands);
}
}
return {
module: CliModule,
imports: [pluginModule],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS, ...pluginCommands],
};
}
}

View File

@@ -6,7 +6,8 @@ import { loadConfigFile, updateUserConfig } from '@app/store/modules/config.js';
import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
interface DeveloperOptions {
disclaimer: boolean;
@@ -21,7 +22,8 @@ export class DeveloperCommand extends CommandRunner {
constructor(
private logger: LogService,
private readonly inquirerService: InquirerService,
private readonly restartCommand: RestartCommand
private readonly startCommand: StartCommand,
private readonly stopCommand: StopCommand
) {
super();
}
@@ -33,6 +35,7 @@ export class DeveloperCommand extends CommandRunner {
}
const { store } = await import('@app/store/index.js');
await store.dispatch(loadConfigFile());
await this.stopCommand.run([]);
store.dispatch(updateUserConfig({ local: { sandbox: options.sandbox ? 'yes' : 'no' } }));
writeConfigSync('flash');
@@ -40,6 +43,6 @@ export class DeveloperCommand extends CommandRunner {
'Updated Developer Configuration - restart the API in 5 seconds to apply them...'
);
await new Promise((resolve) => setTimeout(resolve, 5000));
await this.restartCommand.run([]);
await this.startCommand.run([], {});
}
}

View File

@@ -9,6 +9,8 @@ import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
import { AddSSOUserQuestionSet } from '@app/unraid-api/cli/sso/add-sso-user.questions.js';
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
interface AddSSOUserCommandOptions {
disclaimer: string;
@@ -25,7 +27,8 @@ export class AddSSOUserCommand extends CommandRunner {
constructor(
private readonly logger: LogService,
private readonly inquirerService: InquirerService,
private readonly restartCommand: RestartCommand
private readonly startCommand: StartCommand,
private readonly stopCommand: StopCommand
) {
super();
}
@@ -34,16 +37,12 @@ export class AddSSOUserCommand extends CommandRunner {
try {
options = await this.inquirerService.prompt(AddSSOUserQuestionSet.name, options);
if (options.disclaimer === 'y' && options.username) {
await this.stopCommand.run([]);
await store.dispatch(loadConfigFile());
const shouldRestart = store.getState().config.remote.ssoSubIds.length === 0;
store.dispatch(addSsoUser(options.username));
writeConfigSync('flash');
this.logger.info(`User added ${options.username}`);
if (shouldRestart) {
this.logger.info('Restarting the Unraid API in 5 seconds to enable the SSO button');
await new Promise((resolve) => setTimeout(resolve, 5000));
await this.restartCommand.run([]);
}
this.logger.info(`User added ${options.username}, starting the API`);
await this.startCommand.run([], {});
}
} catch (e: unknown) {
if (e instanceof Error) {

View File

@@ -1,12 +1,14 @@
import { Injectable } from '@nestjs/common';
import { CommandRunner, InquirerService, Option, OptionChoiceFor, SubCommand } from 'nest-commander';
import { CommandRunner, InquirerService, Option, SubCommand } from 'nest-commander';
import { store } from '@app/store/index.js';
import { loadConfigFile, removeSsoUser } from '@app/store/modules/config.js';
import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RemoveSSOUserQuestionSet } from '@app/unraid-api/cli/sso/remove-sso-user.questions.js';
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
interface RemoveSSOUserCommandOptions {
username: string;
@@ -21,13 +23,17 @@ interface RemoveSSOUserCommandOptions {
export class RemoveSSOUserCommand extends CommandRunner {
constructor(
private readonly logger: LogService,
private readonly inquirerService: InquirerService
private readonly inquirerService: InquirerService,
private readonly stopCommand: StopCommand,
private readonly startCommand: StartCommand
) {
super();
}
public async run(_input: string[], options: RemoveSSOUserCommandOptions): Promise<void> {
await store.dispatch(loadConfigFile());
options = await this.inquirerService.prompt(RemoveSSOUserQuestionSet.name, options);
await this.stopCommand.run([]);
store.dispatch(removeSsoUser(options.username === 'all' ? null : options.username));
if (options.username === 'all') {
this.logger.info('All users removed from SSO');
@@ -35,6 +41,7 @@ export class RemoveSSOUserCommand extends CommandRunner {
this.logger.info('User removed: ' + options.username);
}
writeConfigSync('flash');
await this.startCommand.run([], {});
}
@Option({

View File

@@ -18,7 +18,7 @@ import {
WAN_FORWARD_TYPE,
} from '@app/graphql/generated/api/types.js';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
import { updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
import { setSsoUsers, updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js';
import { csvStringToArray } from '@app/utils.js';
@@ -50,11 +50,12 @@ export class ConnectSettingsService {
async getCurrentSettings(): Promise<ConnectSettingsValues> {
const { getters } = await import('@app/store/index.js');
const { local, api } = getters.config();
const { local, api, remote } = getters.config();
return {
...(await this.dynamicRemoteAccessSettings()),
sandbox: local.sandbox === 'yes',
extraOrigins: csvStringToArray(api.extraOrigins),
ssoUserIds: csvStringToArray(remote.ssoSubIds),
};
}
@@ -63,7 +64,7 @@ export class ConnectSettingsService {
* @param settings - The settings to sync
* @returns true if a restart is required, false otherwise
*/
async syncSettings(settings: Partial<ApiSettingsInput>) {
async syncSettings(settings: Partial<ApiSettingsInput>): Promise<boolean> {
let restartRequired = false;
const { getters } = await import('@app/store/index.js');
const { nginx } = getters.emhttp();
@@ -86,13 +87,15 @@ export class ConnectSettingsService {
port: settings.port,
});
}
if (settings.extraOrigins) {
await this.updateAllowedOrigins(settings.extraOrigins);
}
if (typeof settings.sandbox === 'boolean') {
restartRequired ||= await this.setSandboxMode(settings.sandbox);
}
if (settings.ssoUserIds) {
restartRequired ||= await this.updateSSOUsers(settings.ssoUserIds);
}
const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
writeConfigSync('flash');
return restartRequired;
@@ -117,6 +120,32 @@ export class ConnectSettingsService {
return true;
}
/**
* Updates the SSO users and returns true if a restart is required
* @param userIds - The list of SSO user IDs
* @returns true if a restart is required, false otherwise
*/
private async updateSSOUsers(userIds: string[]): Promise<boolean> {
const { ssoUserIds } = await this.getCurrentSettings();
const currentUserSet = new Set(ssoUserIds);
const newUserSet = new Set(userIds);
if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
// there's no change, so no need to update
return false;
}
// make sure we aren't adding invalid user ids
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const invalidUserIds = userIds.filter((id) => !uuidRegex.test(id));
if (invalidUserIds.length > 0) {
throw new GraphQLError(`Invalid SSO user ID's: ${invalidUserIds.join(', ')}`);
}
const { store } = await import('@app/store/index.js');
store.dispatch(setSsoUsers(userIds));
// request a restart if we're there were no sso users before
return currentUserSet.size === 0;
}
private async updateRemoteAccess(input: SetupRemoteAccessInput): Promise<boolean> {
const { store } = await import('@app/store/index.js');
await store.dispatch(setupRemoteAccessThunk(input)).unwrap();
@@ -151,6 +180,7 @@ export class ConnectSettingsService {
await this.remoteAccessSlice(),
await this.sandboxSlice(),
this.flashBackupSlice(),
this.ssoUsersSlice(),
// Because CORS is effectively disabled, this setting is no longer necessary
// keeping it here for in case it needs to be re-enabled
//
@@ -344,4 +374,32 @@ export class ConnectSettingsService {
],
};
}
/**
* Extra origins settings slice
*/
ssoUsersSlice(): SettingSlice {
return {
properties: {
ssoUserIds: {
type: 'array',
items: {
type: 'string',
},
title: 'Unraid API SSO Users',
description: `Provide a list of Unique Unraid Account ID's. Find yours at <a href="https://account.unraid.net/settings" target="_blank">account.unraid.net/settings</a>`,
},
},
elements: [
{
type: 'Control',
scope: '#/properties/ssoUserIds',
options: {
inputType: 'text',
placeholder: 'UUID',
},
},
],
};
}
}

View File

@@ -69,11 +69,11 @@ export class ConnectResolver implements ConnectResolvers {
const restartRequired = await this.connectSettingsService.syncSettings(settings);
const currentSettings = await this.connectSettingsService.getCurrentSettings();
if (restartRequired) {
const restartDelayMs = 3_000;
setTimeout(async () => {
// Send restart out of band to avoid blocking the return of this resolver
this.logger.log('Restarting API');
await this.connectService.restartApi();
}, restartDelayMs);
}, 300);
}
return currentSettings;
}

View File

@@ -24,38 +24,46 @@ import { ResolversModule } from '@app/unraid-api/graph/resolvers/resolvers.modul
import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js';
import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js';
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
@Module({
imports: [
ResolversModule,
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: async () => ({
introspection: getters.config()?.local?.sandbox === 'yes',
playground: false,
context: ({ req, connectionParams, extra }: any) => ({
req,
connectionParams,
extra,
}),
plugins: [sandboxPlugin, idPrefixPlugin] as any[],
subscriptions: {
'graphql-ws': {
path: '/graphql',
imports: [PluginModule],
inject: [PluginService],
useFactory: async (pluginService: PluginService) => {
const plugins = await pluginService.getGraphQLConfiguration();
return {
introspection: getters.config()?.local?.sandbox === 'yes',
playground: false,
context: ({ req, connectionParams, extra }: any) => ({
req,
connectionParams,
extra,
}),
plugins: [sandboxPlugin, idPrefixPlugin] as any[],
subscriptions: {
'graphql-ws': {
path: '/graphql',
},
},
},
path: '/graphql',
typeDefs: print(await loadTypeDefs()),
resolvers: {
JSON: JSONResolver,
Long: GraphQLLong,
UUID: UUIDResolver,
DateTime: DateTimeResolver,
Port: PortResolver,
URL: URLResolver,
},
validationRules: [NoUnusedVariablesRule],
}),
path: '/graphql',
typeDefs: [print(await loadTypeDefs([plugins.typeDefs]))],
resolvers: {
JSON: JSONResolver,
Long: GraphQLLong,
UUID: UUIDResolver,
DateTime: DateTimeResolver,
Port: PortResolver,
URL: URLResolver,
...plugins.resolvers,
},
validationRules: [NoUnusedVariablesRule],
};
},
}),
],
providers: [

View File

@@ -0,0 +1,72 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
import { Resource } from '@app/graphql/generated/api/types.js';
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
@Resolver('ArrayMutations')
export class ArrayMutationsResolver {
constructor(private readonly arrayService: ArrayService) {}
@ResolveField('setState')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async setState(@Args('input') input: ArrayStateInput) {
return this.arrayService.updateArrayState(input);
}
@ResolveField('addDiskToArray')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async addDiskToArray(@Args('input') input: ArrayDiskInput) {
return this.arrayService.addDiskToArray(input);
}
@ResolveField('removeDiskFromArray')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async removeDiskFromArray(@Args('input') input: ArrayDiskInput) {
return this.arrayService.removeDiskFromArray(input);
}
@ResolveField('mountArrayDisk')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async mountArrayDisk(@Args('id') id: string) {
return this.arrayService.mountArrayDisk(id);
}
@ResolveField('unmountArrayDisk')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async unmountArrayDisk(@Args('id') id: string) {
return this.arrayService.unmountArrayDisk(id);
}
@ResolveField('clearArrayDiskStatistics')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.ARRAY,
possession: AuthPossession.ANY,
})
public async clearArrayDiskStatistics(@Args('id') id: string) {
return this.arrayService.clearArrayDiskStatistics(id);
}
}

View File

@@ -1,19 +1,35 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
describe('ArrayResolver', () => {
let resolver: ArrayResolver;
let arrayService: ArrayService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ArrayResolver],
providers: [
ArrayResolver,
{
provide: ArrayService,
useValue: {
updateArrayState: vi.fn(),
addDiskToArray: vi.fn(),
removeDiskFromArray: vi.fn(),
mountArrayDisk: vi.fn(),
unmountArrayDisk: vi.fn(),
clearArrayDiskStatistics: vi.fn(),
},
},
],
}).compile();
resolver = module.get<ArrayResolver>(ArrayResolver);
arrayService = module.get<ArrayService>(ArrayService);
});
it('should be defined', () => {

View File

@@ -6,9 +6,12 @@ import { getArrayData } from '@app/core/modules/array/get-array-data.js';
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { Resource } from '@app/graphql/generated/api/types.js';
import { store } from '@app/store/index.js';
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
@Resolver('Array')
export class ArrayResolver {
constructor(private readonly arrayService: ArrayService) {}
@Query()
@UsePermissions({
action: AuthActionVerb.READ,

View File

@@ -0,0 +1,210 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
import { emcmd } from '@app/core/utils/clients/emcmd.js';
import { ArrayState, ArrayStateInputState } from '@app/graphql/generated/api/types.js';
import { getters } from '@app/store/index.js';
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
vi.mock('@app/core/utils/clients/emcmd.js', () => ({
emcmd: vi.fn(),
}));
vi.mock('@app/store/index.js', () => ({
getters: {
emhttp: vi.fn(),
},
}));
vi.mock('@app/core/modules/array/get-array-data.js', () => ({
getArrayData: vi.fn(),
}));
describe('ArrayService', () => {
let service: ArrayService;
let mockArrayData: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ArrayService],
}).compile();
service = module.get<ArrayService>(ArrayService);
// Mock getters.emhttp()
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STOPPED,
},
} as any);
// Mock getArrayData
mockArrayData = {
id: 'array',
state: ArrayState.STOPPED,
capacity: {
kilobytes: {
free: '1000',
used: '1000',
total: '2000',
},
disks: {
free: '10',
used: '5',
total: '15',
},
},
boot: null,
parities: [],
disks: [],
caches: [],
};
vi.mocked(getArrayData).mockReturnValue(mockArrayData);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should update array state', async () => {
const input: ArrayStateInput = {
desiredState: ArrayStateInputState.START,
};
const result = await service.updateArrayState(input);
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
cmdStart: 'Start',
startState: 'STOPPED',
});
});
it('should add disk to array', async () => {
const input: ArrayDiskInput = {
id: 'test-disk',
slot: 1,
};
const result = await service.addDiskToArray(input);
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
changeDevice: 'apply',
'slotId.1': 'test-disk',
});
});
it('should remove disk from array', async () => {
const input: ArrayDiskInput = {
id: 'test-disk',
slot: 1,
};
const result = await service.removeDiskFromArray(input);
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
changeDevice: 'apply',
'slotId.1': '',
});
});
it('should mount array disk', async () => {
// Mock array as running
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STARTED,
},
} as any);
const result = await service.mountArrayDisk('test-disk');
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
mount: 'apply',
'diskId.test-disk': '1',
});
});
it('should unmount array disk', async () => {
// Mock array as running
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STARTED,
},
} as any);
const result = await service.unmountArrayDisk('test-disk');
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
unmount: 'apply',
'diskId.test-disk': '1',
});
});
it('should clear array disk statistics', async () => {
// Mock array as running
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STARTED,
},
} as any);
const result = await service.clearArrayDiskStatistics('test-disk');
expect(result).toEqual(mockArrayData);
expect(emcmd).toHaveBeenCalledWith({
clearStats: 'apply',
'diskId.test-disk': '1',
});
});
it('should throw error when array is running for add disk', async () => {
// Mock array as running
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STARTED,
},
} as any);
const input: ArrayDiskInput = {
id: 'test-disk',
slot: 1,
};
await expect(service.addDiskToArray(input)).rejects.toThrow(
'Array needs to be stopped before any changes can occur.'
);
});
it('should throw error when array is running for remove disk', async () => {
// Mock array as running
vi.mocked(getters.emhttp).mockReturnValue({
var: {
mdState: ArrayState.STARTED,
},
} as any);
const input: ArrayDiskInput = {
id: 'test-disk',
slot: 1,
};
await expect(service.removeDiskFromArray(input)).rejects.toThrow(
'Array needs to be stopped before any changes can occur.'
);
});
it('should throw error when array is not running for mount disk', async () => {
await expect(service.mountArrayDisk('test-disk')).rejects.toThrow(
'Array must be running to mount disks'
);
});
it('should throw error when array is not running for unmount disk', async () => {
await expect(service.unmountArrayDisk('test-disk')).rejects.toThrow(
'Array must be running to unmount disks'
);
});
it('should throw error when array is not running for clear disk statistics', async () => {
await expect(service.clearArrayDiskStatistics('test-disk')).rejects.toThrow(
'Array must be running to clear disk statistics'
);
});
});

View File

@@ -0,0 +1,145 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { capitalCase, constantCase } from 'change-case';
import type { ArrayDiskInput, ArrayStateInput, ArrayType } from '@app/graphql/generated/api/types.js';
import { AppError } from '@app/core/errors/app-error.js';
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
import { emcmd } from '@app/core/utils/clients/emcmd.js';
import { arrayIsRunning as arrayIsRunningUtil } from '@app/core/utils/index.js';
import {
ArrayPendingState,
ArrayState,
ArrayStateInputState,
} from '@app/graphql/generated/api/types.js';
@Injectable()
export class ArrayService {
private pendingState: ArrayPendingState | null = null;
/**
* Is the array running?
* @todo Refactor this to include this util in the service directly
*/
private arrayIsRunning() {
return arrayIsRunningUtil();
}
async updateArrayState({ desiredState }: ArrayStateInput): Promise<ArrayType> {
const startState = this.arrayIsRunning() ? ArrayState.STARTED : ArrayState.STOPPED;
const pendingState =
desiredState === ArrayStateInputState.STOP
? ArrayPendingState.STOPPING
: ArrayPendingState.STARTING;
// Prevent this running multiple times at once
if (this.pendingState) {
throw new BadRequestException(
new AppError(`Array state is still being updated. Changing to ${pendingState}`)
);
}
// Prevent starting/stopping array when it's already in the same state
if (
(this.arrayIsRunning() && desiredState === ArrayStateInputState.START) ||
(!this.arrayIsRunning() && desiredState === ArrayStateInputState.STOP)
) {
throw new BadRequestException(new AppError(`The array is already ${startState}`));
}
// Set lock then start/stop array
this.pendingState = pendingState;
const command = {
[`cmd${capitalCase(desiredState)}`]: capitalCase(desiredState),
startState: constantCase(startState),
};
try {
await emcmd(command);
} finally {
this.pendingState = null;
}
// Get new array JSON
const array = getArrayData();
return array;
}
async addDiskToArray(input: ArrayDiskInput): Promise<ArrayType> {
if (this.arrayIsRunning()) {
throw new ArrayRunningError();
}
const { id: diskId, slot: preferredSlot } = input;
const slot = preferredSlot?.toString() ?? '';
// Add disk
await emcmd({
changeDevice: 'apply',
[`slotId.${slot}`]: diskId,
});
return getArrayData();
}
async removeDiskFromArray(input: ArrayDiskInput): Promise<ArrayType> {
if (this.arrayIsRunning()) {
throw new ArrayRunningError();
}
const { slot } = input;
const slotStr = slot?.toString() ?? '';
// Remove disk
await emcmd({
changeDevice: 'apply',
[`slotId.${slotStr}`]: '',
});
return getArrayData();
}
async mountArrayDisk(id: string): Promise<ArrayType> {
if (!this.arrayIsRunning()) {
throw new BadRequestException('Array must be running to mount disks');
}
// Mount disk
await emcmd({
mount: 'apply',
[`diskId.${id}`]: '1',
});
return getArrayData();
}
async unmountArrayDisk(id: string): Promise<ArrayType> {
if (!this.arrayIsRunning()) {
throw new BadRequestException('Array must be running to unmount disks');
}
// Unmount disk
await emcmd({
unmount: 'apply',
[`diskId.${id}`]: '1',
});
return getArrayData();
}
async clearArrayDiskStatistics(id: string): Promise<ArrayType> {
if (!this.arrayIsRunning()) {
throw new BadRequestException('Array must be running to clear disk statistics');
}
// Clear disk statistics
await emcmd({
clearStats: 'apply',
[`diskId.${id}`]: '1',
});
return getArrayData();
}
}

View File

@@ -4,7 +4,7 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import type { AllowedOriginInput } from '@app/graphql/generated/api/types.js';
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
import { Config, ConfigErrorState, Resource } from '@app/graphql/generated/api/types.js';
import { Config, Resource } from '@app/graphql/generated/api/types.js';
import { getters, store } from '@app/store/index.js';
import { updateAllowedOrigins } from '@app/store/modules/config.js';

View File

@@ -0,0 +1,74 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
describe('DockerMutationsResolver', () => {
let resolver: DockerMutationsResolver;
let dockerService: DockerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DockerMutationsResolver,
{
provide: DockerService,
useValue: {
startContainer: vi.fn(),
stopContainer: vi.fn(),
},
},
],
}).compile();
resolver = module.get<DockerMutationsResolver>(DockerMutationsResolver);
dockerService = module.get<DockerService>(DockerService);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
it('should start container', async () => {
const mockContainer: DockerContainer = {
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.RUNNING,
status: 'Up 2 hours',
};
vi.mocked(dockerService.startContainer).mockResolvedValue(mockContainer);
const result = await resolver.startContainer('1');
expect(result).toEqual(mockContainer);
expect(dockerService.startContainer).toHaveBeenCalledWith('1');
});
it('should stop container', async () => {
const mockContainer: DockerContainer = {
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
};
vi.mocked(dockerService.stopContainer).mockResolvedValue(mockContainer);
const result = await resolver.stopContainer('1');
expect(result).toEqual(mockContainer);
expect(dockerService.stopContainer).toHaveBeenCalledWith('1');
});
});

View File

@@ -0,0 +1,31 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { Resource } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
@Resolver('DockerMutations')
export class DockerMutationsResolver {
constructor(private readonly dockerService: DockerService) {}
@ResolveField('startContainer')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
public async startContainer(@Args('id') id: string) {
return this.dockerService.startContainer(id);
}
@ResolveField('stopContainer')
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
public async stopContainer(@Args('id') id: string) {
return this.dockerService.stopContainer(id);
}
}

View File

@@ -1,22 +1,77 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
describe('DockerResolver', () => {
let resolver: DockerResolver;
let dockerService: DockerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerResolver],
providers: [
DockerResolver,
{
provide: DockerService,
useValue: {
getContainers: vi.fn(),
},
},
],
}).compile();
resolver = module.get<DockerResolver>(DockerResolver);
dockerService = module.get<DockerService>(DockerService);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
it('should return docker object with id', () => {
const result = resolver.docker();
expect(result).toEqual({ id: 'docker' });
});
it('should return containers from service', async () => {
const mockContainers: DockerContainer[] = [
{
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
},
{
id: '2',
autoStart: true,
command: 'test2',
created: 1234567891,
image: 'test-image2',
imageId: 'test-image-id2',
ports: [],
state: ContainerState.RUNNING,
status: 'Up 2 hours',
},
];
vi.mocked(dockerService.getContainers).mockResolvedValue(mockContainers);
const result = await resolver.containers();
expect(result).toEqual(mockContainers);
expect(dockerService.getContainers).toHaveBeenCalledWith({ useCache: false });
});
it('should return mutations object with id', () => {
const result = resolver.mutations();
expect(result).toEqual({ id: 'docker-mutations' });
});
});

View File

@@ -3,9 +3,12 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { Resource } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
@Resolver('Docker')
export class DockerResolver {
constructor(private readonly dockerService: DockerService) {}
@UsePermissions({
action: AuthActionVerb.READ,
resource: Resource.DOCKER,
@@ -25,10 +28,13 @@ export class DockerResolver {
})
@ResolveField()
public async containers() {
const { getDockerContainers } = await import(
'@app/core/modules/docker/get-docker-containers.js'
);
return this.dockerService.getContainers({ useCache: false });
}
return getDockerContainers({ useCache: false });
@ResolveField()
public mutations() {
return {
id: 'docker-mutations',
};
}
}

View File

@@ -0,0 +1,125 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { getDockerContainers } from '@app/core/modules/docker/get-docker-containers.js';
import { docker } from '@app/core/utils/clients/docker.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
vi.mock('@app/core/utils/clients/docker.js', () => ({
docker: {
getContainer: vi.fn(),
listContainers: vi.fn(),
},
}));
vi.mock('@app/core/modules/docker/get-docker-containers.js', () => ({
getDockerContainers: vi.fn(),
}));
describe('DockerService', () => {
let service: DockerService;
let mockContainer: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerService],
}).compile();
service = module.get<DockerService>(DockerService);
mockContainer = {
start: vi.fn(),
stop: vi.fn(),
};
vi.mocked(docker.getContainer).mockReturnValue(mockContainer as any);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should get containers', async () => {
const mockContainers: DockerContainer[] = [
{
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
},
];
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
const result = await service.getContainers({ useCache: false });
expect(result).toEqual(mockContainers);
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
});
it('should start container', async () => {
const mockContainers: DockerContainer[] = [
{
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.RUNNING,
status: 'Up 2 hours',
},
];
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
const result = await service.startContainer('1');
expect(result).toEqual(mockContainers[0]);
expect(docker.getContainer).toHaveBeenCalledWith('1');
expect(mockContainer.start).toHaveBeenCalled();
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
});
it('should stop container', async () => {
const mockContainers: DockerContainer[] = [
{
id: '1',
autoStart: false,
command: 'test',
created: 1234567890,
image: 'test-image',
imageId: 'test-image-id',
ports: [],
state: ContainerState.EXITED,
status: 'Exited',
},
];
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
const result = await service.stopContainer('1');
expect(result).toEqual(mockContainers[0]);
expect(docker.getContainer).toHaveBeenCalledWith('1');
expect(mockContainer.stop).toHaveBeenCalled();
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
});
it('should throw error if container not found after start', async () => {
vi.mocked(getDockerContainers).mockResolvedValue([]);
await expect(service.startContainer('1')).rejects.toThrow(
'Container 1 not found after starting'
);
});
it('should throw error if container not found after stop', async () => {
vi.mocked(getDockerContainers).mockResolvedValue([]);
await expect(service.stopContainer('1')).rejects.toThrow('Container 1 not found after stopping');
});
});

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import {
ContainerListingOptions,
getDockerContainers,
} from '@app/core/modules/docker/get-docker-containers.js';
import { docker } from '@app/core/utils/clients/docker.js';
@Injectable()
export class DockerService {
public async getContainers({ useCache }: ContainerListingOptions): Promise<DockerContainer[]> {
return getDockerContainers({ useCache });
}
public async startContainer(id: string): Promise<DockerContainer> {
const container = docker.getContainer(id);
await container.start();
const containers = await this.getContainers({ useCache: false });
const updatedContainer = containers.find((c) => c.id === id);
if (!updatedContainer) {
throw new Error(`Container ${id} not found after starting`);
}
return updatedContainer;
}
public async stopContainer(id: string): Promise<DockerContainer> {
const container = docker.getContainer(id);
await container.stop();
const containers = await this.getContainers({ useCache: false });
const updatedContainer = containers.find((c) => c.id === id);
if (!updatedContainer) {
throw new Error(`Container ${id} not found after stopping`);
}
return updatedContainer;
}
}

View File

@@ -0,0 +1,11 @@
import { ResolveField, Resolver } from '@nestjs/graphql';
@Resolver('Mutation')
export class MutationResolver {
@ResolveField()
public async array() {
return {
__typename: 'ArrayMutations',
};
}
}

View File

@@ -3,17 +3,22 @@ import { Module } from '@nestjs/common';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js';
import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js';
import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js';
import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resolver.js';
import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js';
import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js';
import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js';
import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js';
import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';
import { MeResolver } from '@app/unraid-api/graph/resolvers/me/me.resolver.js';
import { MutationResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
@@ -27,13 +32,18 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js
imports: [AuthModule],
providers: [
ArrayResolver,
ArrayMutationsResolver,
ArrayService,
ApiKeyResolver,
CloudResolver,
ConfigResolver,
DisksResolver,
DisplayResolver,
DockerResolver,
DockerMutationsResolver,
DockerService,
FlashResolver,
MutationResolver,
InfoResolver,
NotificationsResolver,
OnlineResolver,

View File

@@ -52,7 +52,13 @@ export async function bootstrapNestServer(): Promise<NestFastifyApplication> {
origin: true, // Allows all origins
credentials: true,
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-CSRF-TOKEN',
'X-API-KEY',
],
});
// Setup Nestjs Pino Logger

View File

@@ -0,0 +1,61 @@
import { Logger, Type } from '@nestjs/common';
import { CommandRunner } from 'nest-commander';
import { z } from 'zod';
import { ApiStore } from '@app/store/index.js';
export interface PluginMetadata {
name: string;
description: string;
}
const asyncArray = () => z.function().returns(z.promise(z.array(z.any())));
const asyncString = () => z.function().returns(z.promise(z.string()));
const asyncVoid = () => z.function().returns(z.promise(z.void()));
// GraphQL resolver type definitions
const resolverFunction = z
.function()
.args(
z.any().optional(), // parent
z.any().optional(), // args
z.any().optional(), // context
z.any().optional() // info
)
.returns(z.any());
const resolverFieldMap = z.record(z.string(), resolverFunction);
const resolverTypeMap = z.record(
z.enum(['Query', 'Mutation', 'Subscription']).or(z.string()),
resolverFieldMap
);
const asyncResolver = () => z.function().returns(z.promise(resolverTypeMap));
/** Warning: unstable API. The config mechanism and API may soon change. */
export const apiPluginSchema = z.object({
_type: z.literal('UnraidApiPlugin'),
name: z.string(),
description: z.string(),
commands: z.array(z.custom<Type<CommandRunner>>()),
registerGraphQLResolvers: asyncResolver().optional(),
registerGraphQLTypeDefs: asyncString().optional(),
registerRESTControllers: asyncArray().optional(),
registerRESTRoutes: asyncArray().optional(),
registerServices: asyncArray().optional(),
registerCronJobs: asyncArray().optional(),
// These schema definitions are picked up as nest modules as well.
onModuleInit: asyncVoid().optional(),
onModuleDestroy: asyncVoid().optional(),
});
/** Warning: unstable API. The config mechanism and API may soon change. */
export type ApiPluginDefinition = z.infer<typeof apiPluginSchema>;
// todo: the blocker to publishing this type is the 'ApiStore' type.
// It pulls in a lot of irrelevant types (e.g. graphql types) and triggers js transpilation of everything related to the store.
// If we can isolate the type, we can publish it to npm and developers can use it as a dev dependency.
/**
* Represents a subclass of UnraidAPIPlugin that can be instantiated.
*/
export type ConstructablePlugin = (options: { store: ApiStore; logger: Logger }) => ApiPluginDefinition;

Some files were not shown because too many files have changed in this diff Show More