nexus-shared 1.1.20 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { isAbortError, readApiResponse, readFetchResponse, toFormData } from "./ApiResponses.js";
|
|
2
|
+
import { appendQuery, fillApiPath, isApiEndpoint } from "./ApiRoutes.js";
|
|
3
|
+
import { buildMessage } from "./MessageBuilder.js";
|
|
4
|
+
export const DEFAULT_API_TIMEOUT = 60_000;
|
|
5
|
+
/** Statuses worth one more try for a GET: the server or a gateway was briefly unavailable. */
|
|
6
|
+
const RETRY_STATUSES = new Set([502, 503, 504]);
|
|
7
|
+
/** What a call does when it names nothing: GET loads, DELETE deletes, the rest save. */
|
|
8
|
+
export function defaultAction(method) {
|
|
9
|
+
return method === "GET" ? "load" : method === "DELETE" ? "delete" : "save";
|
|
10
|
+
}
|
|
11
|
+
/** What a call does and acts on: its endpoint, its method, and the context its messages are built from. */
|
|
12
|
+
export function apiCallContext(target, options = {}) {
|
|
13
|
+
const endpoint = isApiEndpoint(target) ? target : undefined;
|
|
14
|
+
const method = options.method ?? endpoint?.method ?? "GET";
|
|
15
|
+
const context = { action: options.action ?? endpoint?.action ?? defaultAction(method), subject: options.subject ?? endpoint?.subject };
|
|
16
|
+
if (options.count !== undefined)
|
|
17
|
+
context.count = options.count;
|
|
18
|
+
if (options.plural)
|
|
19
|
+
context.plural = options.plural;
|
|
20
|
+
return { endpoint, method, context };
|
|
21
|
+
}
|
|
22
|
+
function isRawBody(body) {
|
|
23
|
+
return ((typeof Blob !== "undefined" && body instanceof Blob) ||
|
|
24
|
+
(typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) ||
|
|
25
|
+
body instanceof ArrayBuffer ||
|
|
26
|
+
ArrayBuffer.isView(body) ||
|
|
27
|
+
(typeof ReadableStream !== "undefined" && body instanceof ReadableStream));
|
|
28
|
+
}
|
|
29
|
+
function pause(ms, signal) {
|
|
30
|
+
return new Promise(resolve => {
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
return resolve();
|
|
33
|
+
const done = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener("abort", done);
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(done, ms);
|
|
39
|
+
signal.addEventListener("abort", done, { once: true });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const aborted = () => ({ ok: false, aborted: true });
|
|
43
|
+
/**
|
|
44
|
+
* Whether an error is a framework's own signal passing through a call, never a failed request: Next.js throws one from
|
|
45
|
+
* `headers()`, `cookies()`, or a `no-store` fetch while it prerenders, to learn that a page renders per request (and
|
|
46
|
+
* for `redirect()` and `notFound()`). Each carries a `digest`, or React's `$typeof`. The engine throws them on.
|
|
47
|
+
*/
|
|
48
|
+
export function isFrameworkError(error) {
|
|
49
|
+
const value = error;
|
|
50
|
+
return typeof value === "object" && value !== null && (typeof value.digest === "string" || value.$typeof !== undefined);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Makes an API client. Apps use the two made here, `api` (browser) and `serverApi` (server); make another for an API
|
|
54
|
+
* that is not a Nexus module.
|
|
55
|
+
*/
|
|
56
|
+
export function createApiClient(config) {
|
|
57
|
+
const inflight = new Map();
|
|
58
|
+
function describe(target, options) {
|
|
59
|
+
return { target, ...apiCallContext(target, options), url: "", options };
|
|
60
|
+
}
|
|
61
|
+
async function resolve(call) {
|
|
62
|
+
const { endpoint, options } = call;
|
|
63
|
+
if (endpoint) {
|
|
64
|
+
const path = fillApiPath(endpoint.path, options.params, options.body);
|
|
65
|
+
return appendQuery(await config.resolveUrl(endpoint, path, call), endpoint.query, options.query);
|
|
66
|
+
}
|
|
67
|
+
const url = call.target;
|
|
68
|
+
return appendQuery(config.resolveTarget ? await config.resolveTarget(url, call) : url, options.query);
|
|
69
|
+
}
|
|
70
|
+
/** Sends the request and reads its answer. */
|
|
71
|
+
async function request(call) {
|
|
72
|
+
const { method, options, endpoint } = call;
|
|
73
|
+
const headers = { Accept: "application/json", ...(await config.headers?.(call)) };
|
|
74
|
+
const token = await config.token?.(call);
|
|
75
|
+
if (token)
|
|
76
|
+
headers.Authorization = /^bearer /i.test(token) ? token : `Bearer ${token}`;
|
|
77
|
+
let body;
|
|
78
|
+
if (method !== "GET" && options.body !== undefined) {
|
|
79
|
+
const bodyType = options.bodyType ?? endpoint?.bodyType ?? (typeof FormData !== "undefined" && options.body instanceof FormData ? "form-data" : "json");
|
|
80
|
+
if (isRawBody(options.body))
|
|
81
|
+
body = options.body;
|
|
82
|
+
else if (bodyType === "form-data")
|
|
83
|
+
body = toFormData(options.body);
|
|
84
|
+
else {
|
|
85
|
+
body = JSON.stringify(options.body);
|
|
86
|
+
headers["Content-Type"] = "application/json";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
Object.assign(headers, options.headers);
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const caller = options.signal;
|
|
92
|
+
const stop = () => controller.abort();
|
|
93
|
+
caller?.addEventListener("abort", stop, { once: true });
|
|
94
|
+
let timedOut = false;
|
|
95
|
+
const timeout = options.timeout ?? config.timeout ?? DEFAULT_API_TIMEOUT;
|
|
96
|
+
const timer = timeout > 0
|
|
97
|
+
? setTimeout(() => {
|
|
98
|
+
timedOut = true;
|
|
99
|
+
controller.abort();
|
|
100
|
+
}, timeout)
|
|
101
|
+
: undefined;
|
|
102
|
+
const read = options.read ?? ((answer, status) => readApiResponse(answer, status, call.context));
|
|
103
|
+
const stopped = () => caller?.aborted ? aborted() : timedOut ? { ok: false, timeout: true, message: buildMessage("reason", { ...call.context, timeout: true }) } : undefined;
|
|
104
|
+
let init = { method, headers, body, credentials: options.credentials ?? config.credentials, signal: controller.signal };
|
|
105
|
+
if (config.prepare)
|
|
106
|
+
init = config.prepare(init, call);
|
|
107
|
+
const send = config.fetch ?? globalThis.fetch;
|
|
108
|
+
const retries = options.retries ?? (method === "GET" ? 1 : 0);
|
|
109
|
+
try {
|
|
110
|
+
for (let attempt = 0;; attempt++) {
|
|
111
|
+
if (caller?.aborted)
|
|
112
|
+
return aborted();
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await send(call.url, init);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (isFrameworkError(error))
|
|
119
|
+
throw error;
|
|
120
|
+
const early = stopped();
|
|
121
|
+
if (early)
|
|
122
|
+
return early;
|
|
123
|
+
// Aborted by a signal the call does not know, such as the page closing: nothing to show either.
|
|
124
|
+
if (isAbortError(error))
|
|
125
|
+
return aborted();
|
|
126
|
+
if (attempt < retries) {
|
|
127
|
+
await pause(300 * 2 ** attempt, controller.signal);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, network: true, message: buildMessage("reason", { ...call.context, network: true }) };
|
|
131
|
+
}
|
|
132
|
+
if (RETRY_STATUSES.has(response.status) && attempt < retries) {
|
|
133
|
+
await response.body?.cancel().catch(() => undefined);
|
|
134
|
+
await pause(300 * 2 ** attempt, controller.signal);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const result = await readFetchResponse(response, read);
|
|
138
|
+
// A body cut off by an abort or the timeout reads as empty: say what happened instead.
|
|
139
|
+
return stopped() ?? result;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
caller?.removeEventListener("abort", stop);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function send(target, options = {}) {
|
|
148
|
+
const call = describe(target, options);
|
|
149
|
+
const done = config.onStart?.(call);
|
|
150
|
+
let result;
|
|
151
|
+
try {
|
|
152
|
+
call.url = await resolve(call);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (isFrameworkError(error)) {
|
|
156
|
+
done?.(aborted());
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
// A module without a root URL, or a path that names a value the call did not give: nothing is sent.
|
|
160
|
+
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
161
|
+
}
|
|
162
|
+
if (!result) {
|
|
163
|
+
try {
|
|
164
|
+
result = await run(call);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// A framework's signal (or a throw in the app's own headers or token): a loading toast closes, the error goes on.
|
|
168
|
+
done?.(aborted());
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
config.onResult?.(result, call);
|
|
173
|
+
done?.(result);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
async function run(call) {
|
|
177
|
+
if (!config.dedupe || call.method !== "GET" || call.options.signal)
|
|
178
|
+
return request(call);
|
|
179
|
+
let shared = inflight.get(call.url);
|
|
180
|
+
if (!shared) {
|
|
181
|
+
shared = request(call).finally(() => inflight.delete(call.url));
|
|
182
|
+
inflight.set(call.url, shared);
|
|
183
|
+
}
|
|
184
|
+
// Each caller gets its own copy, so one cannot change another's answer.
|
|
185
|
+
return { ...(await shared) };
|
|
186
|
+
}
|
|
187
|
+
const withBody = (method) => (target, body, options) => send(target, { ...options, body, method });
|
|
188
|
+
return {
|
|
189
|
+
send,
|
|
190
|
+
get: (target, options) => send(target, { ...options, method: "GET" }),
|
|
191
|
+
post: withBody("POST"),
|
|
192
|
+
put: withBody("PUT"),
|
|
193
|
+
patch: withBody("PATCH"),
|
|
194
|
+
delete: (target, options) => send(target, { ...options, method: "DELETE" }),
|
|
195
|
+
url: (target, options = {}) => resolve(describe(target, options)),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ApiCallOptions, ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { ApiRequest, FormErrors } from "../Interfaces/FormInterfaces.ts";
|
|
3
|
+
export interface SendApiOptions {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
read?: (body: unknown, status: number | undefined) => ApiResult;
|
|
6
|
+
}
|
|
7
|
+
/** The call options of a form's `{ url, method, headers, bodyType, credentials }` request with its values. */
|
|
8
|
+
export declare function apiRequestOptions(request: ApiRequest, body: unknown, { signal, read }?: SendApiOptions): ApiCallOptions;
|
|
9
|
+
/** An abort as the error `fetch` throws, for code that expects a stopped request to reject. */
|
|
10
|
+
export declare function abortError(): Error;
|
|
11
|
+
/**
|
|
12
|
+
* Sends a body with `fetch` and reads the answer. Resolves with a failed result for a network error or a timeout;
|
|
13
|
+
* rejects only when the request is aborted. `ApiForm` sends through the app's `api` instead, with its headers.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendApiRequest(request: ApiRequest, body: unknown, options?: SendApiOptions): Promise<ApiResult>;
|
|
16
|
+
/** What an `ApiSend` function answered, read the same way: a `Response`, an `ApiResult`, or a body. */
|
|
17
|
+
export declare function toApiResult(answer: unknown, read?: (body: unknown, status: number | undefined) => ApiResult): Promise<ApiResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Puts a server's messages on the inputs they name. Names match without case, and a path matches its first part that is
|
|
20
|
+
* an input ("Addresses[1].City" and "$.addresses" go to "addresses"; "dto.Email" to "email"). Messages that match no
|
|
21
|
+
* input are returned in `others`, for the form's message.
|
|
22
|
+
*/
|
|
23
|
+
export declare function matchFieldErrors(errors: Record<string, string> | undefined, inputNames: readonly string[]): {
|
|
24
|
+
fields: FormErrors;
|
|
25
|
+
others: string[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createApiClient } from "./ApiClient.js";
|
|
2
|
+
import { isApiResult, readApiResponse, readFetchResponse } from "./ApiResponses.js";
|
|
3
|
+
/** The call options of a form's `{ url, method, headers, bodyType, credentials }` request with its values. */
|
|
4
|
+
export function apiRequestOptions(request, body, { signal, read = readApiResponse } = {}) {
|
|
5
|
+
const { method = "POST", headers, bodyType = "json", credentials } = request;
|
|
6
|
+
return { method, headers, bodyType, credentials, signal, read, body: body ?? {} };
|
|
7
|
+
}
|
|
8
|
+
/** An abort as the error `fetch` throws, for code that expects a stopped request to reject. */
|
|
9
|
+
export function abortError() {
|
|
10
|
+
return typeof DOMException === "function" ? new DOMException("The request was aborted.", "AbortError") : Object.assign(new Error("The request was aborted."), { name: "AbortError" });
|
|
11
|
+
}
|
|
12
|
+
/** Sends URLs as they are, with none of an app's settings. */
|
|
13
|
+
const plainClient = createApiClient({
|
|
14
|
+
resolveUrl: () => {
|
|
15
|
+
throw new Error("sendApiRequest takes a URL; call an endpoint with api or serverApi.");
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Sends a body with `fetch` and reads the answer. Resolves with a failed result for a network error or a timeout;
|
|
20
|
+
* rejects only when the request is aborted. `ApiForm` sends through the app's `api` instead, with its headers.
|
|
21
|
+
*/
|
|
22
|
+
export async function sendApiRequest(request, body, options = {}) {
|
|
23
|
+
const result = await plainClient.send(request.url, apiRequestOptions(request, body, options));
|
|
24
|
+
if (result.aborted)
|
|
25
|
+
throw abortError();
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/** What an `ApiSend` function answered, read the same way: a `Response`, an `ApiResult`, or a body. */
|
|
29
|
+
export async function toApiResult(answer, read = readApiResponse) {
|
|
30
|
+
if (typeof Response !== "undefined" && answer instanceof Response)
|
|
31
|
+
return readFetchResponse(answer, read);
|
|
32
|
+
if (isApiResult(answer))
|
|
33
|
+
return answer;
|
|
34
|
+
return read(answer, undefined);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Puts a server's messages on the inputs they name. Names match without case, and a path matches its first part that is
|
|
38
|
+
* an input ("Addresses[1].City" and "$.addresses" go to "addresses"; "dto.Email" to "email"). Messages that match no
|
|
39
|
+
* input are returned in `others`, for the form's message.
|
|
40
|
+
*/
|
|
41
|
+
export function matchFieldErrors(errors, inputNames) {
|
|
42
|
+
const byName = new Map(inputNames.map(name => [name.toLowerCase(), name]));
|
|
43
|
+
const fields = {};
|
|
44
|
+
const others = [];
|
|
45
|
+
for (const [key, message] of Object.entries(errors ?? {})) {
|
|
46
|
+
if (!message)
|
|
47
|
+
continue;
|
|
48
|
+
const parts = key.replace(/^\$\.?/, "").split(/[.[\]]+/).filter(Boolean);
|
|
49
|
+
const name = parts.slice(0, 2).map(part => byName.get(part.toLowerCase())).find(Boolean);
|
|
50
|
+
if (name)
|
|
51
|
+
fields[name] = fields[name] && fields[name] !== message ? `${fields[name]} ${message}` : message;
|
|
52
|
+
else if (!others.includes(message))
|
|
53
|
+
others.push(message);
|
|
54
|
+
}
|
|
55
|
+
return { fields, others };
|
|
56
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { NexusModule, type ApiEndpoint, type ApiModuleInfo, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import { apiEndpoint, type ApiEndpointExtras, type ApiUrlParts } from "./ApiRoutes.ts";
|
|
3
|
+
/** Where the proxy route sits in an app: `app/api/app/[module]/[...path]/route.ts`. */
|
|
4
|
+
export declare const DEFAULT_PROXY_PATH = "/api/app";
|
|
5
|
+
/** An environment variable, read when it is needed (never inlined at build time), so one build serves every environment. */
|
|
6
|
+
export declare function readEnvironment(name: string | undefined): string | undefined;
|
|
7
|
+
export interface ApiModulesOptions<TModule extends ApiModuleValue> {
|
|
8
|
+
/** Browser calls go through the app's proxy route, which adds the user's token on the server. Default true. */
|
|
9
|
+
useProxy?: boolean;
|
|
10
|
+
/** Where the proxy route is. Default `/api/app`. */
|
|
11
|
+
proxyPath?: string;
|
|
12
|
+
/** Root URLs the browser calls when a module does not go through the proxy. Public: the browser sees them. */
|
|
13
|
+
browserRoots?: Partial<Record<TModule, string>>;
|
|
14
|
+
/** Root URLs the server calls, by module. They win over `serverRoot`. May be relative to the app's origin. */
|
|
15
|
+
serverRoots?: Partial<Record<TModule, string>>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The API modules of an app. Extend it with the app's own module enum:
|
|
19
|
+
*
|
|
20
|
+
* - `modules`: every module, with its key for proxy URLs and its label for messages.
|
|
21
|
+
* - `serverRoot(module)`: the module's root URL for server calls and the proxy (from the environment, a settings
|
|
22
|
+
* service, anything; may be async).
|
|
23
|
+
*
|
|
24
|
+
* Then, if the defaults do not suit: `useProxy` to call modules straight from the browser, `browserRoot(module)` for
|
|
25
|
+
* those roots, and `proxies(module)` to send only some modules through the proxy.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* enum HrModule { Staff = "staff", Payroll = "payroll" }
|
|
29
|
+
*
|
|
30
|
+
* class HrApiModules extends ApiModules<HrModule> {
|
|
31
|
+
* readonly modules = [
|
|
32
|
+
* { module: HrModule.Staff, key: "staff", label: "Staff", env: "STAFF_API_URL" },
|
|
33
|
+
* { module: HrModule.Payroll, key: "payroll", label: "Payroll", env: "PAYROLL_API_URL" },
|
|
34
|
+
* ];
|
|
35
|
+
* serverRoot(module: HrModule) {
|
|
36
|
+
* return this.readEnv(this.info(module)?.env);
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* export const MODULES = defineApiModules(new HrApiModules());
|
|
41
|
+
* export const EMPLOYEES = MODULES.controller(HrModule.Staff, "Employee", "employee");
|
|
42
|
+
*/
|
|
43
|
+
export declare abstract class ApiModules<TModule extends ApiModuleValue = ApiModuleValue> {
|
|
44
|
+
/** Every module the app calls. */
|
|
45
|
+
abstract readonly modules: readonly ApiModuleInfo<TModule>[];
|
|
46
|
+
/** A module's root URL for server calls and the proxy: absolute, or relative to the app's origin. `undefined` when it is not set. */
|
|
47
|
+
abstract serverRoot(module: TModule): string | undefined | Promise<string | undefined>;
|
|
48
|
+
/** Browser calls go through the proxy route. Change it here, in the options, or in a subclass. */
|
|
49
|
+
useProxy: boolean;
|
|
50
|
+
/** Where the proxy route is. */
|
|
51
|
+
proxyPath: string;
|
|
52
|
+
protected readonly options: ApiModulesOptions<TModule>;
|
|
53
|
+
constructor(options?: ApiModulesOptions<TModule>);
|
|
54
|
+
/** A module's root URL for browser calls that skip the proxy. Default: `browserRoots` from the options. */
|
|
55
|
+
browserRoot(module: TModule): string | undefined;
|
|
56
|
+
/** Whether the browser calls a module through the proxy. Default: `useProxy`, for every module. */
|
|
57
|
+
proxies(_module: TModule): boolean;
|
|
58
|
+
/** A module by its value, its key (any case), or its value as text ("2048", as older proxy URLs have it). */
|
|
59
|
+
info(module: TModule | string): ApiModuleInfo<TModule> | undefined;
|
|
60
|
+
/** The root the browser calls for a module: the proxy's path for it, or its own root. Throws when neither is set. */
|
|
61
|
+
browserUrl(module: TModule): string;
|
|
62
|
+
/** A module's root for server calls, as set: `serverRoots`, else `serverRoot`. Throws when it is not set. May be relative. */
|
|
63
|
+
serverUrl(module: TModule): Promise<string>;
|
|
64
|
+
/** The URL the browser calls for an endpoint, for links, images, and downloads: `/api/app/reservation/Room/Detail/5`. */
|
|
65
|
+
browserApiUrl(endpoint: ApiEndpoint<TModule>, parts?: ApiUrlParts): string;
|
|
66
|
+
/** An environment variable, read at call time; for `serverRoot`. */
|
|
67
|
+
protected readEnv(name: string | undefined): string | undefined;
|
|
68
|
+
/** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
|
|
69
|
+
readonly endpoint: Record<keyof typeof apiEndpoint, (module: TModule, path: string, extras?: ApiEndpointExtras) => ApiEndpoint<TModule>>;
|
|
70
|
+
/** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
|
|
71
|
+
controller(module: TModule, controller: string, subject?: string): {
|
|
72
|
+
module: TModule;
|
|
73
|
+
controller: string;
|
|
74
|
+
listing: ApiEndpoint<TModule>;
|
|
75
|
+
activeListing: ApiEndpoint<TModule>;
|
|
76
|
+
findListing: ApiEndpoint<TModule>;
|
|
77
|
+
pagination: ApiEndpoint<TModule>;
|
|
78
|
+
lists: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
|
|
79
|
+
pages: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
|
|
80
|
+
pins: ApiEndpoint<TModule>;
|
|
81
|
+
details: ApiEndpoint<TModule>;
|
|
82
|
+
create: ApiEndpoint<TModule>;
|
|
83
|
+
update: ApiEndpoint<TModule>;
|
|
84
|
+
delete: ApiEndpoint<TModule>;
|
|
85
|
+
trash: ApiEndpoint<TModule>;
|
|
86
|
+
trashFromDelete: ApiEndpoint<TModule>;
|
|
87
|
+
archive: ApiEndpoint<TModule>;
|
|
88
|
+
restore: ApiEndpoint<TModule>;
|
|
89
|
+
recover: ApiEndpoint<TModule>;
|
|
90
|
+
flag: ApiEndpoint<TModule>;
|
|
91
|
+
pin: ApiEndpoint<TModule>;
|
|
92
|
+
audit: ApiEndpoint<TModule>;
|
|
93
|
+
accessActions: ApiEndpoint<TModule>;
|
|
94
|
+
mass: {
|
|
95
|
+
description: ApiEndpoint<TModule>;
|
|
96
|
+
archive: ApiEndpoint<TModule>;
|
|
97
|
+
trash: ApiEndpoint<TModule>;
|
|
98
|
+
trashFromDelete: ApiEndpoint<TModule>;
|
|
99
|
+
flag: ApiEndpoint<TModule>;
|
|
100
|
+
pin: ApiEndpoint<TModule>;
|
|
101
|
+
delete: ApiEndpoint<TModule>;
|
|
102
|
+
recover: ApiEndpoint<TModule>;
|
|
103
|
+
restore: ApiEndpoint<TModule>;
|
|
104
|
+
};
|
|
105
|
+
action: (method: import("../Shared.Index.ts").HttpMethod, action: string, extras?: Partial<Omit<ApiEndpoint<number>, "path" | "method" | "module">> | undefined) => ApiEndpoint<TModule>;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */
|
|
109
|
+
export declare const NEXUS_MODULES: readonly ApiModuleInfo<NexusModule>[];
|
|
110
|
+
/**
|
|
111
|
+
* The Nexus modules: each module's root from its environment variable on the server (`API_RESERVATION=…/App/Api`),
|
|
112
|
+
* browser calls through the proxy, and `NexusModule.None` for the app's own routes at `/api/…`. Extend it to change one
|
|
113
|
+
* thing, such as a module's root.
|
|
114
|
+
*/
|
|
115
|
+
export declare class NexusApiModules extends ApiModules<NexusModule> {
|
|
116
|
+
readonly modules: readonly ApiModuleInfo<number>[];
|
|
117
|
+
serverRoot(module: NexusModule): string | undefined;
|
|
118
|
+
browserRoot(module: NexusModule): string | undefined;
|
|
119
|
+
proxies(module: NexusModule): boolean;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Makes an app's modules the ones every call uses, and returns them. Define endpoints through what it returns, so any
|
|
123
|
+
* file that calls an endpoint has registered the modules first, on the server and in the browser alike.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* export const MODULES = defineApiModules(new NexusApiModules({ useProxy: process.env.NEXT_PUBLIC_API_PROXY !== "false" }));
|
|
127
|
+
* export const ROOMS = MODULES.controller(NexusModule.Reservation, "Room", "room");
|
|
128
|
+
*/
|
|
129
|
+
export declare function defineApiModules<T extends ApiModules<any>>(modules: T): T;
|
|
130
|
+
/** The app's modules: the ones given to `defineApiModules`, else `NexusApiModules`. */
|
|
131
|
+
export declare function getApiModules(): ApiModules<any>;
|
|
132
|
+
/** The URL the browser calls for an endpoint, through the app's modules. See `ApiModules.browserApiUrl`. */
|
|
133
|
+
export declare function browserApiUrl(endpoint: ApiEndpoint, parts?: ApiUrlParts): string;
|
|
134
|
+
/** Names a call for logs and messages: "reservation/Room/NormalListing". */
|
|
135
|
+
export declare function describeEndpoint(endpoint: ApiEndpoint): string;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { NexusModule } from "../Interfaces/ApiInterfaces.js";
|
|
2
|
+
import { apiController, apiEndpoint, appendQuery, fillApiPath, joinUrl } from "./ApiRoutes.js";
|
|
3
|
+
import { notConfiguredMessage } from "./MessageBuilder.js";
|
|
4
|
+
/*
|
|
5
|
+
* An app's API modules: which modules it calls and where each one's root URL is. The package does not decide either.
|
|
6
|
+
* An app describes its own in a class that extends `ApiModules` (its module enum, its list, and how it finds a root)
|
|
7
|
+
* and registers it once with `defineApiModules`; the browser's `api`, the server's `serverApi`, and the proxy route all
|
|
8
|
+
* ask it. Nexus apps use `NexusApiModules`, which is also what runs when nothing is registered.
|
|
9
|
+
*/
|
|
10
|
+
/** Where the proxy route sits in an app: `app/api/app/[module]/[...path]/route.ts`. */
|
|
11
|
+
export const DEFAULT_PROXY_PATH = "/api/app";
|
|
12
|
+
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
13
|
+
/** An environment variable, read when it is needed (never inlined at build time), so one build serves every environment. */
|
|
14
|
+
export function readEnvironment(name) {
|
|
15
|
+
if (!name)
|
|
16
|
+
return undefined;
|
|
17
|
+
const env = globalThis.process?.env;
|
|
18
|
+
return env?.[name]?.trim() || undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The API modules of an app. Extend it with the app's own module enum:
|
|
22
|
+
*
|
|
23
|
+
* - `modules`: every module, with its key for proxy URLs and its label for messages.
|
|
24
|
+
* - `serverRoot(module)`: the module's root URL for server calls and the proxy (from the environment, a settings
|
|
25
|
+
* service, anything; may be async).
|
|
26
|
+
*
|
|
27
|
+
* Then, if the defaults do not suit: `useProxy` to call modules straight from the browser, `browserRoot(module)` for
|
|
28
|
+
* those roots, and `proxies(module)` to send only some modules through the proxy.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* enum HrModule { Staff = "staff", Payroll = "payroll" }
|
|
32
|
+
*
|
|
33
|
+
* class HrApiModules extends ApiModules<HrModule> {
|
|
34
|
+
* readonly modules = [
|
|
35
|
+
* { module: HrModule.Staff, key: "staff", label: "Staff", env: "STAFF_API_URL" },
|
|
36
|
+
* { module: HrModule.Payroll, key: "payroll", label: "Payroll", env: "PAYROLL_API_URL" },
|
|
37
|
+
* ];
|
|
38
|
+
* serverRoot(module: HrModule) {
|
|
39
|
+
* return this.readEnv(this.info(module)?.env);
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* export const MODULES = defineApiModules(new HrApiModules());
|
|
44
|
+
* export const EMPLOYEES = MODULES.controller(HrModule.Staff, "Employee", "employee");
|
|
45
|
+
*/
|
|
46
|
+
export class ApiModules {
|
|
47
|
+
/** Browser calls go through the proxy route. Change it here, in the options, or in a subclass. */
|
|
48
|
+
useProxy;
|
|
49
|
+
/** Where the proxy route is. */
|
|
50
|
+
proxyPath;
|
|
51
|
+
options;
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.useProxy = options.useProxy ?? true;
|
|
55
|
+
this.proxyPath = options.proxyPath ?? DEFAULT_PROXY_PATH;
|
|
56
|
+
}
|
|
57
|
+
/** A module's root URL for browser calls that skip the proxy. Default: `browserRoots` from the options. */
|
|
58
|
+
browserRoot(module) {
|
|
59
|
+
return this.options.browserRoots?.[module];
|
|
60
|
+
}
|
|
61
|
+
/** Whether the browser calls a module through the proxy. Default: `useProxy`, for every module. */
|
|
62
|
+
proxies(_module) {
|
|
63
|
+
return this.useProxy;
|
|
64
|
+
}
|
|
65
|
+
/** A module by its value, its key (any case), or its value as text ("2048", as older proxy URLs have it). */
|
|
66
|
+
info(module) {
|
|
67
|
+
const list = this.modules;
|
|
68
|
+
const exact = list.find(info => info.module === module);
|
|
69
|
+
if (exact || typeof module !== "string")
|
|
70
|
+
return exact;
|
|
71
|
+
const key = module.toLowerCase();
|
|
72
|
+
return list.find(info => info.key.toLowerCase() === key || String(info.module).toLowerCase() === key);
|
|
73
|
+
}
|
|
74
|
+
/** The root the browser calls for a module: the proxy's path for it, or its own root. Throws when neither is set. */
|
|
75
|
+
browserUrl(module) {
|
|
76
|
+
const info = this.info(module);
|
|
77
|
+
if (!info)
|
|
78
|
+
throw new Error(`"${String(module)}" is not one of the app's API modules.`);
|
|
79
|
+
if (this.proxies(module))
|
|
80
|
+
return joinUrl(this.proxyPath, info.key);
|
|
81
|
+
const root = this.browserRoot(module);
|
|
82
|
+
if (!root)
|
|
83
|
+
throw new Error(notConfiguredMessage(info.label, "browserRoot"));
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
/** A module's root for server calls, as set: `serverRoots`, else `serverRoot`. Throws when it is not set. May be relative. */
|
|
87
|
+
async serverUrl(module) {
|
|
88
|
+
const info = this.info(module);
|
|
89
|
+
if (!info)
|
|
90
|
+
throw new Error(`"${String(module)}" is not one of the app's API modules.`);
|
|
91
|
+
const root = this.options.serverRoots?.[module] ?? (await this.serverRoot(module));
|
|
92
|
+
if (!root)
|
|
93
|
+
throw new Error(notConfiguredMessage(info.label, info.env ?? "serverRoot"));
|
|
94
|
+
return root;
|
|
95
|
+
}
|
|
96
|
+
/** The URL the browser calls for an endpoint, for links, images, and downloads: `/api/app/reservation/Room/Detail/5`. */
|
|
97
|
+
browserApiUrl(endpoint, parts = {}) {
|
|
98
|
+
return appendQuery(joinUrl(this.browserUrl(endpoint.module), fillApiPath(endpoint.path, parts.params, parts.body)), endpoint.query, parts.query);
|
|
99
|
+
}
|
|
100
|
+
/** An environment variable, read at call time; for `serverRoot`. */
|
|
101
|
+
readEnv(name) {
|
|
102
|
+
return readEnvironment(name);
|
|
103
|
+
}
|
|
104
|
+
/** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
|
|
105
|
+
endpoint = apiEndpoint;
|
|
106
|
+
/** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
|
|
107
|
+
controller(module, controller, subject) {
|
|
108
|
+
return apiController(module, controller, subject);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */
|
|
112
|
+
export const NEXUS_MODULES = [
|
|
113
|
+
{ module: NexusModule.Configuration, key: "configuration", label: "Configuration", env: "API_CONFIGURATION" },
|
|
114
|
+
{ module: NexusModule.Identity, key: "identity", label: "Identity", env: "API_IDENTITY" },
|
|
115
|
+
{ module: NexusModule.Enterprise, key: "enterprise", label: "Enterprise", env: "API_ENTERPRISE" },
|
|
116
|
+
{ module: NexusModule.Drive, key: "drive", label: "Drive", env: "API_DRIVE" },
|
|
117
|
+
{ module: NexusModule.Message, key: "message", label: "Message", env: "API_MESSAGE" },
|
|
118
|
+
{ module: NexusModule.Accounting, key: "accounting", label: "Accounting", env: "API_ACCOUNTING" },
|
|
119
|
+
{ module: NexusModule.Report, key: "report", label: "Report", env: "API_REPORT" },
|
|
120
|
+
{ module: NexusModule.Requisition, key: "requisition", label: "Requisition", env: "API_REQUISITION" },
|
|
121
|
+
{ module: NexusModule.Tenant, key: "tenant", label: "Tenant", env: "API_TENANT" },
|
|
122
|
+
{ module: NexusModule.Subscription, key: "subscription", label: "Subscription", env: "API_SUBSCRIPTION" },
|
|
123
|
+
{ module: NexusModule.Reservation, key: "reservation", label: "Reservation", env: "API_RESERVATION" },
|
|
124
|
+
{ module: NexusModule.School, key: "school", label: "School", env: "API_SCHOOL" },
|
|
125
|
+
{ module: NexusModule.Pharmacy, key: "pharmacy", label: "Pharmacy", env: "API_PHARMACY" },
|
|
126
|
+
{ module: NexusModule.Staffing, key: "staffing", label: "Staffing", env: "API_STAFFING" },
|
|
127
|
+
{ module: NexusModule.Portfolio, key: "portfolio", label: "Portfolio", env: "API_PORTFOLIO" },
|
|
128
|
+
// The app's own API routes (/api/...), never through the proxy.
|
|
129
|
+
{ module: NexusModule.None, key: "app", label: "App" },
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* The Nexus modules: each module's root from its environment variable on the server (`API_RESERVATION=…/App/Api`),
|
|
133
|
+
* browser calls through the proxy, and `NexusModule.None` for the app's own routes at `/api/…`. Extend it to change one
|
|
134
|
+
* thing, such as a module's root.
|
|
135
|
+
*/
|
|
136
|
+
export class NexusApiModules extends ApiModules {
|
|
137
|
+
modules = NEXUS_MODULES;
|
|
138
|
+
serverRoot(module) {
|
|
139
|
+
if (module === NexusModule.None)
|
|
140
|
+
return "/api";
|
|
141
|
+
return this.readEnv(this.info(module)?.env);
|
|
142
|
+
}
|
|
143
|
+
browserRoot(module) {
|
|
144
|
+
return module === NexusModule.None ? "/api" : super.browserRoot(module);
|
|
145
|
+
}
|
|
146
|
+
proxies(module) {
|
|
147
|
+
return module !== NexusModule.None && super.proxies(module);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
let registered;
|
|
152
|
+
/** Checks a modules class: every module once, keys that fit in a URL. Throws on the first problem. */
|
|
153
|
+
function checkModules(modules) {
|
|
154
|
+
const keys = new Set();
|
|
155
|
+
const values = new Set();
|
|
156
|
+
for (const info of modules.modules) {
|
|
157
|
+
if (!KEY_PATTERN.test(info.key))
|
|
158
|
+
throw new Error(`API module key "${info.key}" can hold letters, digits, dots, dashes, and underscores only.`);
|
|
159
|
+
const key = info.key.toLowerCase();
|
|
160
|
+
if (keys.has(key))
|
|
161
|
+
throw new Error(`Two API modules have the key "${info.key}".`);
|
|
162
|
+
if (values.has(info.module))
|
|
163
|
+
throw new Error(`Two API modules have the value ${String(info.module)}.`);
|
|
164
|
+
keys.add(key);
|
|
165
|
+
values.add(info.module);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Makes an app's modules the ones every call uses, and returns them. Define endpoints through what it returns, so any
|
|
170
|
+
* file that calls an endpoint has registered the modules first, on the server and in the browser alike.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* export const MODULES = defineApiModules(new NexusApiModules({ useProxy: process.env.NEXT_PUBLIC_API_PROXY !== "false" }));
|
|
174
|
+
* export const ROOMS = MODULES.controller(NexusModule.Reservation, "Room", "room");
|
|
175
|
+
*/
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
|
+
export function defineApiModules(modules) {
|
|
178
|
+
checkModules(modules);
|
|
179
|
+
registered = modules;
|
|
180
|
+
return modules;
|
|
181
|
+
}
|
|
182
|
+
/** The app's modules: the ones given to `defineApiModules`, else `NexusApiModules`. */
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
+
export function getApiModules() {
|
|
185
|
+
return (registered ??= new NexusApiModules());
|
|
186
|
+
}
|
|
187
|
+
/** The URL the browser calls for an endpoint, through the app's modules. See `ApiModules.browserApiUrl`. */
|
|
188
|
+
export function browserApiUrl(endpoint, parts) {
|
|
189
|
+
return getApiModules().browserApiUrl(endpoint, parts);
|
|
190
|
+
}
|
|
191
|
+
/** Names a call for logs and messages: "reservation/Room/NormalListing". */
|
|
192
|
+
export function describeEndpoint(endpoint) {
|
|
193
|
+
return `${getApiModules().info(endpoint.module)?.key ?? String(endpoint.module)}/${endpoint.path}`;
|
|
194
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { MessageAction, MessageContext } from "../Interfaces/MessageInterfaces.ts";
|
|
3
|
+
/** Shown when nothing reached the server (the default wording; `messages.reason({ network: true })` follows `configureMessages`). */
|
|
4
|
+
export declare const NETWORK_ERROR_MESSAGE: string;
|
|
5
|
+
/** A message for a failed request that came without one of its own, by HTTP status and what the request did. Default action `save`. */
|
|
6
|
+
export declare function apiStatusMessage(status: number | undefined, action?: MessageAction): string;
|
|
7
|
+
/**
|
|
8
|
+
* Reads a server's answer as success or failure:
|
|
9
|
+
* - Nexus API responses, `{ isSuccess, message, messageCode, errorCode, inputName, result }`: `inputName` names the
|
|
10
|
+
* inputs the message is about, several joined with "|".
|
|
11
|
+
* - ASP.NET Core problem details and validation errors, `{ title, detail, errors: { Email: ["…"] } }`.
|
|
12
|
+
* - Anything else by its HTTP status; the body is the result.
|
|
13
|
+
*
|
|
14
|
+
* A failure without a message of its own gets one by status, worded for what the call did (`context`, default: a save).
|
|
15
|
+
*/
|
|
16
|
+
export declare function readApiResponse(body: unknown, status?: number, context?: MessageContext): ApiResult;
|
|
17
|
+
/** Reads a `fetch` response: its JSON (or text) through `read`. */
|
|
18
|
+
export declare function readFetchResponse(response: Response, read?: (body: unknown, status: number | undefined) => ApiResult): Promise<ApiResult>;
|
|
19
|
+
/** Whether an error is a request stopped on purpose (the form closed), which shows nothing. */
|
|
20
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
21
|
+
/** A thrown error as a message: its own, or the network message for a failed `fetch`. */
|
|
22
|
+
export declare function errorMessage(error: unknown, fallback?: string): string;
|
|
23
|
+
/** Values as form fields: lists of plain values repeat their name, objects and lists of objects go as JSON text. */
|
|
24
|
+
export declare function toFormData(values: unknown): FormData;
|
|
25
|
+
/** Whether a value is already a read answer (`{ ok, … }`), not a server's body. */
|
|
26
|
+
export declare function isApiResult(value: unknown): value is ApiResult;
|
|
27
|
+
/** A failed answer as an error, for code that throws: a server component's error boundary, a form's `loadValue`. */
|
|
28
|
+
export declare class ApiError extends Error {
|
|
29
|
+
readonly response: ApiResult;
|
|
30
|
+
constructor(response: ApiResult);
|
|
31
|
+
get status(): number | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What the server sent back, or an `ApiError` thrown for a failure (an `AbortError` for an aborted call, which forms and
|
|
35
|
+
* loaders ignore).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const rooms = unwrapApiResult(await serverApi.get<Room[]>(ROOMS.listing));
|
|
39
|
+
* <ApiForm loadValue={signal => api.get(ROOMS.details, { params: { id }, signal }).then(unwrapApiResult)} … />
|
|
40
|
+
*/
|
|
41
|
+
export declare function unwrapApiResult<T>(response: ApiResult<T>): T;
|