@entifix/next-shell 0.1.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 +7 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/back-office/account-menu.d.ts +29 -0
- package/dist/lib/back-office/account-menu.d.ts.map +1 -0
- package/dist/lib/back-office/account-menu.js +70 -0
- package/dist/lib/back-office/account-menu.js.map +1 -0
- package/dist/lib/back-office/back-office-shell.d.ts +50 -0
- package/dist/lib/back-office/back-office-shell.d.ts.map +1 -0
- package/dist/lib/back-office/back-office-shell.js +128 -0
- package/dist/lib/back-office/back-office-shell.js.map +1 -0
- package/dist/lib/back-office/breadcrumbs.d.ts +16 -0
- package/dist/lib/back-office/breadcrumbs.d.ts.map +1 -0
- package/dist/lib/back-office/breadcrumbs.js +53 -0
- package/dist/lib/back-office/breadcrumbs.js.map +1 -0
- package/dist/lib/back-office/group-by-screen-type.d.ts +21 -0
- package/dist/lib/back-office/group-by-screen-type.d.ts.map +1 -0
- package/dist/lib/back-office/group-by-screen-type.js +39 -0
- package/dist/lib/back-office/group-by-screen-type.js.map +1 -0
- package/dist/lib/back-office/index.d.ts +6 -0
- package/dist/lib/back-office/index.d.ts.map +1 -0
- package/dist/lib/back-office/index.js +7 -0
- package/dist/lib/back-office/index.js.map +1 -0
- package/dist/lib/back-office/nav.d.ts +39 -0
- package/dist/lib/back-office/nav.d.ts.map +1 -0
- package/dist/lib/back-office/nav.js +3 -0
- package/dist/lib/back-office/nav.js.map +1 -0
- package/dist/lib/back-office/sidebar-nav.d.ts +26 -0
- package/dist/lib/back-office/sidebar-nav.d.ts.map +1 -0
- package/dist/lib/back-office/sidebar-nav.js +181 -0
- package/dist/lib/back-office/sidebar-nav.js.map +1 -0
- package/dist/lib/command-palette/command-palette-host.d.ts +41 -0
- package/dist/lib/command-palette/command-palette-host.d.ts.map +1 -0
- package/dist/lib/command-palette/command-palette-host.js +196 -0
- package/dist/lib/command-palette/command-palette-host.js.map +1 -0
- package/dist/lib/command-palette/index.d.ts +9 -0
- package/dist/lib/command-palette/index.d.ts.map +1 -0
- package/dist/lib/command-palette/index.js +10 -0
- package/dist/lib/command-palette/index.js.map +1 -0
- package/dist/lib/command-palette/palette-command.d.ts +32 -0
- package/dist/lib/command-palette/palette-command.d.ts.map +1 -0
- package/dist/lib/command-palette/palette-command.js +19 -0
- package/dist/lib/command-palette/palette-command.js.map +1 -0
- package/dist/lib/command-palette/use-command-recency.d.ts +28 -0
- package/dist/lib/command-palette/use-command-recency.d.ts.map +1 -0
- package/dist/lib/command-palette/use-command-recency.js +67 -0
- package/dist/lib/command-palette/use-command-recency.js.map +1 -0
- package/dist/lib/command-palette/use-command-source.d.ts +17 -0
- package/dist/lib/command-palette/use-command-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-command-source.js +70 -0
- package/dist/lib/command-palette/use-command-source.js.map +1 -0
- package/dist/lib/command-palette/use-nav-source.d.ts +21 -0
- package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-nav-source.js +78 -0
- package/dist/lib/command-palette/use-nav-source.js.map +1 -0
- package/dist/lib/command-palette/use-records-source.d.ts +23 -0
- package/dist/lib/command-palette/use-records-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-records-source.js +199 -0
- package/dist/lib/command-palette/use-records-source.js.map +1 -0
- package/dist/lib/command-palette/use-tabs-source.d.ts +20 -0
- package/dist/lib/command-palette/use-tabs-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-tabs-source.js +52 -0
- package/dist/lib/command-palette/use-tabs-source.js.map +1 -0
- package/dist/lib/command-palette/use-use-cases-source.d.ts +22 -0
- package/dist/lib/command-palette/use-use-cases-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-use-cases-source.js +93 -0
- package/dist/lib/command-palette/use-use-cases-source.js.map +1 -0
- package/dist/lib/config/config-route.d.ts +51 -0
- package/dist/lib/config/config-route.d.ts.map +1 -0
- package/dist/lib/config/config-route.js +65 -0
- package/dist/lib/config/config-route.js.map +1 -0
- package/dist/lib/config/service-proxy-route.d.ts +28 -0
- package/dist/lib/config/service-proxy-route.d.ts.map +1 -0
- package/dist/lib/config/service-proxy-route.js +125 -0
- package/dist/lib/config/service-proxy-route.js.map +1 -0
- package/dist/lib/config/service-token.d.ts +5 -0
- package/dist/lib/config/service-token.d.ts.map +1 -0
- package/dist/lib/config/service-token.js +18 -0
- package/dist/lib/config/service-token.js.map +1 -0
- package/dist/lib/crud/entity-crud-form.d.ts +49 -0
- package/dist/lib/crud/entity-crud-form.d.ts.map +1 -0
- package/dist/lib/crud/entity-crud-form.js +133 -0
- package/dist/lib/crud/entity-crud-form.js.map +1 -0
- package/dist/lib/crud/hand-off-write.d.ts +35 -0
- package/dist/lib/crud/hand-off-write.d.ts.map +1 -0
- package/dist/lib/crud/hand-off-write.js +27 -0
- package/dist/lib/crud/hand-off-write.js.map +1 -0
- package/dist/lib/crud/index.d.ts +9 -0
- package/dist/lib/crud/index.d.ts.map +1 -0
- package/dist/lib/crud/index.js +10 -0
- package/dist/lib/crud/index.js.map +1 -0
- package/dist/lib/crud/make-entity-crud.d.ts +28 -0
- package/dist/lib/crud/make-entity-crud.d.ts.map +1 -0
- package/dist/lib/crud/make-entity-crud.js +318 -0
- package/dist/lib/crud/make-entity-crud.js.map +1 -0
- package/dist/lib/crud/make-entity-crud.types.d.ts +237 -0
- package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -0
- package/dist/lib/crud/make-entity-crud.types.js +6 -0
- package/dist/lib/crud/make-entity-crud.types.js.map +1 -0
- package/dist/lib/crud/pending-notice.d.ts +31 -0
- package/dist/lib/crud/pending-notice.d.ts.map +1 -0
- package/dist/lib/crud/pending-notice.js +72 -0
- package/dist/lib/crud/pending-notice.js.map +1 -0
- package/dist/lib/crud/slug.d.ts +13 -0
- package/dist/lib/crud/slug.d.ts.map +1 -0
- package/dist/lib/crud/slug.js +13 -0
- package/dist/lib/crud/slug.js.map +1 -0
- package/dist/lib/crud/use-entity-affordances.d.ts +20 -0
- package/dist/lib/crud/use-entity-affordances.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-affordances.js +22 -0
- package/dist/lib/crud/use-entity-affordances.js.map +1 -0
- package/dist/lib/crud/use-entity-bulk.d.ts +43 -0
- package/dist/lib/crud/use-entity-bulk.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-bulk.js +106 -0
- package/dist/lib/crud/use-entity-bulk.js.map +1 -0
- package/dist/lib/crud/use-entity-link-sources.d.ts +25 -0
- package/dist/lib/crud/use-entity-link-sources.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-link-sources.js +37 -0
- package/dist/lib/crud/use-entity-link-sources.js.map +1 -0
- package/dist/lib/health/health-routes.d.ts +37 -0
- package/dist/lib/health/health-routes.d.ts.map +1 -0
- package/dist/lib/health/health-routes.js +90 -0
- package/dist/lib/health/health-routes.js.map +1 -0
- package/dist/lib/i18n/catalog/en.d.ts +4 -0
- package/dist/lib/i18n/catalog/en.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/en.js +347 -0
- package/dist/lib/i18n/catalog/en.js.map +1 -0
- package/dist/lib/i18n/catalog/es.d.ts +397 -0
- package/dist/lib/i18n/catalog/es.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/es.js +407 -0
- package/dist/lib/i18n/catalog/es.js.map +1 -0
- package/dist/lib/i18n/catalog/index.d.ts +671 -0
- package/dist/lib/i18n/catalog/index.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/index.js +28 -0
- package/dist/lib/i18n/catalog/index.js.map +1 -0
- package/dist/lib/i18n/index.d.ts +2 -0
- package/dist/lib/i18n/index.d.ts.map +1 -0
- package/dist/lib/i18n/index.js +3 -0
- package/dist/lib/i18n/index.js.map +1 -0
- package/dist/lib/i18n/locale-link.d.ts +23 -0
- package/dist/lib/i18n/locale-link.d.ts.map +1 -0
- package/dist/lib/i18n/locale-link.js +43 -0
- package/dist/lib/i18n/locale-link.js.map +1 -0
- package/dist/lib/i18n/spec-catalog.d.ts +2 -0
- package/dist/lib/i18n/spec-catalog.d.ts.map +1 -0
- package/dist/lib/i18n/spec-catalog.js +84 -0
- package/dist/lib/i18n/spec-catalog.js.map +1 -0
- package/dist/lib/search/record-search-route.d.ts +44 -0
- package/dist/lib/search/record-search-route.d.ts.map +1 -0
- package/dist/lib/search/record-search-route.js +158 -0
- package/dist/lib/search/record-search-route.js.map +1 -0
- package/dist/lib/search/record-search-source.d.ts +59 -0
- package/dist/lib/search/record-search-source.d.ts.map +1 -0
- package/dist/lib/search/record-search-source.js +118 -0
- package/dist/lib/search/record-search-source.js.map +1 -0
- package/dist/lib/search/record-search.types.d.ts +85 -0
- package/dist/lib/search/record-search.types.d.ts.map +1 -0
- package/dist/lib/search/record-search.types.js +29 -0
- package/dist/lib/search/record-search.types.js.map +1 -0
- package/dist/lib/search/search-records.d.ts +25 -0
- package/dist/lib/search/search-records.d.ts.map +1 -0
- package/dist/lib/search/search-records.js +36 -0
- package/dist/lib/search/search-records.js.map +1 -0
- package/dist/lib/session/account-links.d.ts +46 -0
- package/dist/lib/session/account-links.d.ts.map +1 -0
- package/dist/lib/session/account-links.js +41 -0
- package/dist/lib/session/account-links.js.map +1 -0
- package/dist/lib/session/bearer.d.ts +25 -0
- package/dist/lib/session/bearer.d.ts.map +1 -0
- package/dist/lib/session/bearer.js +31 -0
- package/dist/lib/session/bearer.js.map +1 -0
- package/dist/lib/session/cookies.d.ts +14 -0
- package/dist/lib/session/cookies.d.ts.map +1 -0
- package/dist/lib/session/cookies.js +33 -0
- package/dist/lib/session/cookies.js.map +1 -0
- package/dist/lib/session/device.d.ts +31 -0
- package/dist/lib/session/device.d.ts.map +1 -0
- package/dist/lib/session/device.js +80 -0
- package/dist/lib/session/device.js.map +1 -0
- package/dist/lib/session/index.d.ts +3 -0
- package/dist/lib/session/index.d.ts.map +1 -0
- package/dist/lib/session/index.js +12 -0
- package/dist/lib/session/index.js.map +1 -0
- package/dist/lib/session/refresh-route.d.ts +19 -0
- package/dist/lib/session/refresh-route.d.ts.map +1 -0
- package/dist/lib/session/refresh-route.js +85 -0
- package/dist/lib/session/refresh-route.js.map +1 -0
- package/dist/lib/session/session-keepalive.d.ts +52 -0
- package/dist/lib/session/session-keepalive.d.ts.map +1 -0
- package/dist/lib/session/session-keepalive.js +72 -0
- package/dist/lib/session/session-keepalive.js.map +1 -0
- package/dist/lib/session/use-session-refresh.d.ts +43 -0
- package/dist/lib/session/use-session-refresh.d.ts.map +1 -0
- package/dist/lib/session/use-session-refresh.js +120 -0
- package/dist/lib/session/use-session-refresh.js.map +1 -0
- package/dist/lib/wizard/index.d.ts +4 -0
- package/dist/lib/wizard/index.d.ts.map +1 -0
- package/dist/lib/wizard/index.js +5 -0
- package/dist/lib/wizard/index.js.map +1 -0
- package/dist/lib/wizard/use-wizard-draft.d.ts +22 -0
- package/dist/lib/wizard/use-wizard-draft.d.ts.map +1 -0
- package/dist/lib/wizard/use-wizard-draft.js +39 -0
- package/dist/lib/wizard/use-wizard-draft.js.map +1 -0
- package/dist/lib/wizard/use-wizard-step-param.d.ts +60 -0
- package/dist/lib/wizard/use-wizard-step-param.d.ts.map +1 -0
- package/dist/lib/wizard/use-wizard-step-param.js +94 -0
- package/dist/lib/wizard/use-wizard-step-param.js.map +1 -0
- package/dist/lib/wizard/wizard-tab-kind.d.ts +31 -0
- package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -0
- package/dist/lib/wizard/wizard-tab-kind.js +33 -0
- package/dist/lib/wizard/wizard-tab-kind.js.map +1 -0
- package/dist/lib/workspace/drafts-state.d.ts +88 -0
- package/dist/lib/workspace/drafts-state.d.ts.map +1 -0
- package/dist/lib/workspace/drafts-state.js +128 -0
- package/dist/lib/workspace/drafts-state.js.map +1 -0
- package/dist/lib/workspace/entity-nav.d.ts +26 -0
- package/dist/lib/workspace/entity-nav.d.ts.map +1 -0
- package/dist/lib/workspace/entity-nav.js +56 -0
- package/dist/lib/workspace/entity-nav.js.map +1 -0
- package/dist/lib/workspace/entity-tab-kind.d.ts +51 -0
- package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -0
- package/dist/lib/workspace/entity-tab-kind.js +47 -0
- package/dist/lib/workspace/entity-tab-kind.js.map +1 -0
- package/dist/lib/workspace/idb-state-storage.d.ts +9 -0
- package/dist/lib/workspace/idb-state-storage.d.ts.map +1 -0
- package/dist/lib/workspace/idb-state-storage.js +32 -0
- package/dist/lib/workspace/idb-state-storage.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +13 -0
- package/dist/lib/workspace/index.d.ts.map +1 -0
- package/dist/lib/workspace/index.js +14 -0
- package/dist/lib/workspace/index.js.map +1 -0
- package/dist/lib/workspace/pending-state.d.ts +84 -0
- package/dist/lib/workspace/pending-state.d.ts.map +1 -0
- package/dist/lib/workspace/pending-state.js +155 -0
- package/dist/lib/workspace/pending-state.js.map +1 -0
- package/dist/lib/workspace/pending-transactions.d.ts +44 -0
- package/dist/lib/workspace/pending-transactions.d.ts.map +1 -0
- package/dist/lib/workspace/pending-transactions.js +114 -0
- package/dist/lib/workspace/pending-transactions.js.map +1 -0
- package/dist/lib/workspace/tab-kind.d.ts +48 -0
- package/dist/lib/workspace/tab-kind.d.ts.map +1 -0
- package/dist/lib/workspace/tab-kind.js +47 -0
- package/dist/lib/workspace/tab-kind.js.map +1 -0
- package/dist/lib/workspace/tab-state.d.ts +36 -0
- package/dist/lib/workspace/tab-state.d.ts.map +1 -0
- package/dist/lib/workspace/tab-state.js +81 -0
- package/dist/lib/workspace/tab-state.js.map +1 -0
- package/dist/lib/workspace/tabs-state.d.ts +46 -0
- package/dist/lib/workspace/tabs-state.d.ts.map +1 -0
- package/dist/lib/workspace/tabs-state.js +56 -0
- package/dist/lib/workspace/tabs-state.js.map +1 -0
- package/dist/lib/workspace/use-entity-draft.d.ts +25 -0
- package/dist/lib/workspace/use-entity-draft.d.ts.map +1 -0
- package/dist/lib/workspace/use-entity-draft.js +38 -0
- package/dist/lib/workspace/use-entity-draft.js.map +1 -0
- package/dist/lib/workspace/workspace-scope.d.ts +51 -0
- package/dist/lib/workspace/workspace-scope.d.ts.map +1 -0
- package/dist/lib/workspace/workspace-scope.js +42 -0
- package/dist/lib/workspace/workspace-scope.js.map +1 -0
- package/dist/lib/workspace/workspace-shell.d.ts +33 -0
- package/dist/lib/workspace/workspace-shell.d.ts.map +1 -0
- package/dist/lib/workspace/workspace-shell.js +206 -0
- package/dist/lib/workspace/workspace-shell.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +41 -0
- package/dist/server.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { emptySelection } from '@entifix/core';
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* The code a row carries when the *request* failed rather than the row.
|
|
6
|
+
*
|
|
7
|
+
* `unexpected` is the shared fallback the `errors` catalog already ships, and
|
|
8
|
+
* it is the honest label: what went wrong is not something this row did.
|
|
9
|
+
*/ const BULK_REQUEST_FAILED = 'unexpected';
|
|
10
|
+
/**
|
|
11
|
+
* Owns a listing's selection and the result of the last bulk run.
|
|
12
|
+
*
|
|
13
|
+
* The selection lives here rather than inside `EntityTable` because it has to
|
|
14
|
+
* survive pagination, and the page owns the pager — a selection reset by the
|
|
15
|
+
* very navigation used to add to it would make "select rows across pages"
|
|
16
|
+
* impossible.
|
|
17
|
+
*
|
|
18
|
+
* Two behaviours worth not re-deriving. **The selection survives the action**
|
|
19
|
+
* (#121): after a run the rows stay ticked, because the operator's next act is
|
|
20
|
+
* usually to retry the failures or to run a second verb on the same rows, and
|
|
21
|
+
* re-selecting forty rows to do it is the whole cost the bulk bar exists to
|
|
22
|
+
* remove. And **a retry re-runs only the failures**, as an `ids` selection
|
|
23
|
+
* built from the outcomes — never the original selection, which would redo the
|
|
24
|
+
* successful rows and, for a `matching` selection, re-resolve a filter whose
|
|
25
|
+
* answer has just changed underneath it.
|
|
26
|
+
*/ export function useEntityBulk({ run, onCompleted, onError }) {
|
|
27
|
+
const [selection, setSelection] = useState(()=>emptySelection());
|
|
28
|
+
const [outcomes, setOutcomes] = useState(undefined);
|
|
29
|
+
const [isRunning, setRunning] = useState(false);
|
|
30
|
+
const execute = useCallback(async (key, target)=>{
|
|
31
|
+
// Unreachable through the UI: the handlers that call this are only
|
|
32
|
+
// exposed when `run` exists. It is here because a `useCallback` cannot
|
|
33
|
+
// be declared conditionally and TypeScript still has to narrow.
|
|
34
|
+
/* v8 ignore next */ if (!run) return;
|
|
35
|
+
setRunning(true);
|
|
36
|
+
// The previous result is cleared as the new run starts: leaving it up
|
|
37
|
+
// would show last run's failures beside this run's spinner, and they
|
|
38
|
+
// are not the same rows.
|
|
39
|
+
setOutcomes(undefined);
|
|
40
|
+
try {
|
|
41
|
+
setOutcomes(await run(key, target));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// The *request* failed — the network, a `403`, a `500` — which is not a
|
|
44
|
+
// per-row outcome. Caught rather than left to reject: `void execute(…)`
|
|
45
|
+
// at the call site would otherwise make every failed bulk run an
|
|
46
|
+
// unhandled promise rejection, which is invisible in the UI and noisy
|
|
47
|
+
// in the console.
|
|
48
|
+
//
|
|
49
|
+
// When the rows are known it is reported as every row failing, because
|
|
50
|
+
// that is what happened, and it reuses the surface that already
|
|
51
|
+
// attributes failures per row. A `matching` selection has no id list to
|
|
52
|
+
// attribute to — that is the whole point of the mode — so it reports
|
|
53
|
+
// nothing here and the error goes to the caller instead.
|
|
54
|
+
if (target.mode === 'ids') {
|
|
55
|
+
setOutcomes(// `Array.from`, never a spread — see `toWireSelection`: the loose
|
|
56
|
+
// SWC helper wraps a `Set` instead of iterating it.
|
|
57
|
+
Array.from(target.ids).map((id)=>({
|
|
58
|
+
id,
|
|
59
|
+
ok: false,
|
|
60
|
+
code: BULK_REQUEST_FAILED
|
|
61
|
+
})));
|
|
62
|
+
}
|
|
63
|
+
onError == null ? void 0 : onError(error);
|
|
64
|
+
} finally{
|
|
65
|
+
setRunning(false);
|
|
66
|
+
onCompleted == null ? void 0 : onCompleted();
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
run,
|
|
70
|
+
onCompleted,
|
|
71
|
+
onError
|
|
72
|
+
]);
|
|
73
|
+
const [lastKey, setLastKey] = useState(undefined);
|
|
74
|
+
const onBulkUseCase = useCallback((key, target)=>{
|
|
75
|
+
setLastKey(key);
|
|
76
|
+
void execute(key, target);
|
|
77
|
+
}, [
|
|
78
|
+
execute
|
|
79
|
+
]);
|
|
80
|
+
const onBulkRetry = useCallback((ids)=>{
|
|
81
|
+
if (lastKey === undefined) return;
|
|
82
|
+
void execute(lastKey, {
|
|
83
|
+
mode: 'ids',
|
|
84
|
+
ids: new Set(ids)
|
|
85
|
+
});
|
|
86
|
+
}, [
|
|
87
|
+
execute,
|
|
88
|
+
lastKey
|
|
89
|
+
]);
|
|
90
|
+
if (!run) return {
|
|
91
|
+
tableProps: {}
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
tableProps: {
|
|
95
|
+
selection,
|
|
96
|
+
onSelectionChange: setSelection,
|
|
97
|
+
onBulkUseCase,
|
|
98
|
+
bulkOutcomes: outcomes,
|
|
99
|
+
onBulkDismiss: ()=>setOutcomes(undefined),
|
|
100
|
+
onBulkRetry,
|
|
101
|
+
isBulkRunning: isRunning
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=use-entity-bulk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/use-entity-bulk.ts"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n emptySelection,\n type Entity,\n type EntityId,\n type EntitySelection,\n} from '@entifix/core';\nimport { useCallback, useState } from 'react';\n\n/**\n * The code a row carries when the *request* failed rather than the row.\n *\n * `unexpected` is the shared fallback the `errors` catalog already ships, and\n * it is the honest label: what went wrong is not something this row did.\n */\nconst BULK_REQUEST_FAILED = 'unexpected';\n\nexport interface UseEntityBulkOptions<TEntity extends Entity> {\n /**\n * Runs one `collection`-bound verb. Absent means this listing has no bulk\n * actions, and no selection column is rendered at all.\n */\n readonly run?: (\n key: string,\n selection: EntitySelection<TEntity>,\n ) => Promise<readonly BulkOutcome[]>;\n /** Called after a run, so the listing can re-read the rows it just changed. */\n readonly onCompleted?: () => void;\n /** The whole request failed — a transport error, not a per-row outcome. */\n readonly onError?: (error: unknown) => void;\n}\n\n/** The props {@link useEntityBulk} contributes to `EntityTable`. */\nexport interface EntityBulkTableProps<TEntity extends Entity> {\n selection?: EntitySelection<TEntity>;\n onSelectionChange?: (selection: EntitySelection<TEntity>) => void;\n onBulkUseCase?: (key: string, selection: EntitySelection<TEntity>) => void;\n bulkOutcomes?: readonly BulkOutcome[];\n onBulkDismiss?: () => void;\n onBulkRetry?: (ids: EntityId[]) => void;\n isBulkRunning?: boolean;\n}\n\n/**\n * Owns a listing's selection and the result of the last bulk run.\n *\n * The selection lives here rather than inside `EntityTable` because it has to\n * survive pagination, and the page owns the pager — a selection reset by the\n * very navigation used to add to it would make \"select rows across pages\"\n * impossible.\n *\n * Two behaviours worth not re-deriving. **The selection survives the action**\n * (#121): after a run the rows stay ticked, because the operator's next act is\n * usually to retry the failures or to run a second verb on the same rows, and\n * re-selecting forty rows to do it is the whole cost the bulk bar exists to\n * remove. And **a retry re-runs only the failures**, as an `ids` selection\n * built from the outcomes — never the original selection, which would redo the\n * successful rows and, for a `matching` selection, re-resolve a filter whose\n * answer has just changed underneath it.\n */\nexport function useEntityBulk<TEntity extends Entity>({\n run,\n onCompleted,\n onError,\n}: UseEntityBulkOptions<TEntity>): {\n tableProps: EntityBulkTableProps<TEntity>;\n} {\n const [selection, setSelection] = useState<EntitySelection<TEntity>>(() =>\n emptySelection<TEntity>(),\n );\n const [outcomes, setOutcomes] = useState<readonly BulkOutcome[] | undefined>(\n undefined,\n );\n const [isRunning, setRunning] = useState(false);\n\n const execute = useCallback(\n async (key: string, target: EntitySelection<TEntity>) => {\n // Unreachable through the UI: the handlers that call this are only\n // exposed when `run` exists. It is here because a `useCallback` cannot\n // be declared conditionally and TypeScript still has to narrow.\n /* v8 ignore next */\n if (!run) return;\n setRunning(true);\n // The previous result is cleared as the new run starts: leaving it up\n // would show last run's failures beside this run's spinner, and they\n // are not the same rows.\n setOutcomes(undefined);\n try {\n setOutcomes(await run(key, target));\n } catch (error) {\n // The *request* failed — the network, a `403`, a `500` — which is not a\n // per-row outcome. Caught rather than left to reject: `void execute(…)`\n // at the call site would otherwise make every failed bulk run an\n // unhandled promise rejection, which is invisible in the UI and noisy\n // in the console.\n //\n // When the rows are known it is reported as every row failing, because\n // that is what happened, and it reuses the surface that already\n // attributes failures per row. A `matching` selection has no id list to\n // attribute to — that is the whole point of the mode — so it reports\n // nothing here and the error goes to the caller instead.\n if (target.mode === 'ids') {\n setOutcomes(\n // `Array.from`, never a spread — see `toWireSelection`: the loose\n // SWC helper wraps a `Set` instead of iterating it.\n Array.from(target.ids).map(id => ({\n id,\n ok: false,\n code: BULK_REQUEST_FAILED,\n })),\n );\n }\n onError?.(error);\n } finally {\n setRunning(false);\n onCompleted?.();\n }\n },\n [run, onCompleted, onError],\n );\n\n const [lastKey, setLastKey] = useState<string | undefined>(undefined);\n\n const onBulkUseCase = useCallback(\n (key: string, target: EntitySelection<TEntity>) => {\n setLastKey(key);\n void execute(key, target);\n },\n [execute],\n );\n\n const onBulkRetry = useCallback(\n (ids: EntityId[]) => {\n if (lastKey === undefined) return;\n void execute(lastKey, { mode: 'ids', ids: new Set(ids) });\n },\n [execute, lastKey],\n );\n\n if (!run) return { tableProps: {} };\n\n return {\n tableProps: {\n selection,\n onSelectionChange: setSelection,\n onBulkUseCase,\n bulkOutcomes: outcomes,\n onBulkDismiss: () => setOutcomes(undefined),\n onBulkRetry,\n isBulkRunning: isRunning,\n },\n };\n}\n"],"names":["emptySelection","useCallback","useState","BULK_REQUEST_FAILED","useEntityBulk","run","onCompleted","onError","selection","setSelection","outcomes","setOutcomes","undefined","isRunning","setRunning","execute","key","target","error","mode","Array","from","ids","map","id","ok","code","lastKey","setLastKey","onBulkUseCase","onBulkRetry","Set","tableProps","onSelectionChange","bulkOutcomes","onBulkDismiss","isBulkRunning"],"mappings":"AAAA;AAEA,SAEEA,cAAc,QAIT,gBAAgB;AACvB,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAQ;AAE9C;;;;;CAKC,GACD,MAAMC,sBAAsB;AA4B5B;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,cAAsC,EACpDC,GAAG,EACHC,WAAW,EACXC,OAAO,EACuB;IAG9B,MAAM,CAACC,WAAWC,aAAa,GAAGP,SAAmC,IACnEF;IAEF,MAAM,CAACU,UAAUC,YAAY,GAAGT,SAC9BU;IAEF,MAAM,CAACC,WAAWC,WAAW,GAAGZ,SAAS;IAEzC,MAAMa,UAAUd,YACd,OAAOe,KAAaC;QAClB,mEAAmE;QACnE,uEAAuE;QACvE,gEAAgE;QAChE,kBAAkB,GAClB,IAAI,CAACZ,KAAK;QACVS,WAAW;QACX,sEAAsE;QACtE,qEAAqE;QACrE,yBAAyB;QACzBH,YAAYC;QACZ,IAAI;YACFD,YAAY,MAAMN,IAAIW,KAAKC;QAC7B,EAAE,OAAOC,OAAO;YACd,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,sEAAsE;YACtE,kBAAkB;YAClB,EAAE;YACF,uEAAuE;YACvE,gEAAgE;YAChE,wEAAwE;YACxE,qEAAqE;YACrE,yDAAyD;YACzD,IAAID,OAAOE,IAAI,KAAK,OAAO;gBACzBR,YACE,kEAAkE;gBAClE,oDAAoD;gBACpDS,MAAMC,IAAI,CAACJ,OAAOK,GAAG,EAAEC,GAAG,CAACC,CAAAA,KAAO,CAAA;wBAChCA;wBACAC,IAAI;wBACJC,MAAMvB;oBACR,CAAA;YAEJ;YACAI,2BAAAA,QAAUW;QACZ,SAAU;YACRJ,WAAW;YACXR,+BAAAA;QACF;IACF,GACA;QAACD;QAAKC;QAAaC;KAAQ;IAG7B,MAAM,CAACoB,SAASC,WAAW,GAAG1B,SAA6BU;IAE3D,MAAMiB,gBAAgB5B,YACpB,CAACe,KAAaC;QACZW,WAAWZ;QACX,KAAKD,QAAQC,KAAKC;IACpB,GACA;QAACF;KAAQ;IAGX,MAAMe,cAAc7B,YAClB,CAACqB;QACC,IAAIK,YAAYf,WAAW;QAC3B,KAAKG,QAAQY,SAAS;YAAER,MAAM;YAAOG,KAAK,IAAIS,IAAIT;QAAK;IACzD,GACA;QAACP;QAASY;KAAQ;IAGpB,IAAI,CAACtB,KAAK,OAAO;QAAE2B,YAAY,CAAC;IAAE;IAElC,OAAO;QACLA,YAAY;YACVxB;YACAyB,mBAAmBxB;YACnBoB;YACAK,cAAcxB;YACdyB,eAAe,IAAMxB,YAAYC;YACjCkB;YACAM,eAAevB;QACjB;IACF;AACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Entity, type EntityDraft, type EntityLinkSelection, type EntityLinkSource } from '@entifix/core';
|
|
2
|
+
import type { EntityCrudLinkSource } from './make-entity-crud.types.js';
|
|
3
|
+
export interface UseEntityLinkSourcesOptions {
|
|
4
|
+
/** The form's current draft — ids are the truth. */
|
|
5
|
+
readonly values: EntityDraft;
|
|
6
|
+
/** The picked-instance sidecar, so a chosen name needs no round trip. */
|
|
7
|
+
readonly selection: EntityLinkSelection;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Turns a fixed list of pickers into the `linkSources` map `EntityForm` takes.
|
|
11
|
+
*
|
|
12
|
+
* **Why the rule is disabled below.** `links` is built once, at factory time,
|
|
13
|
+
* and closed over by the generated form — it is the same array object on every
|
|
14
|
+
* render of a given component, so the hook count is fixed. That is exactly the
|
|
15
|
+
* invariant `react-hooks/rules-of-hooks` exists to protect and exactly the one
|
|
16
|
+
* it cannot see, because it reasons syntactically about the loop rather than
|
|
17
|
+
* about the array's provenance. The loop is isolated in this module so the
|
|
18
|
+
* disable is written once, with its reason, instead of once per generated form.
|
|
19
|
+
*
|
|
20
|
+
* The alternative — one child component per picker, reporting its source
|
|
21
|
+
* upward — would put the sources one render behind the draft that feeds them,
|
|
22
|
+
* which is a stale suggestion list rather than a lint error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useEntityLinkSources(links: readonly EntityCrudLinkSource[], { values, selection }: UseEntityLinkSourcesOptions): Record<string, EntityLinkSource<Entity>>;
|
|
25
|
+
//# sourceMappingURL=use-entity-link-sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-entity-link-sources.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/use-entity-link-sources.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,MAAM,WAAW,2BAA2B;IAC1C,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;CACzC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,oBAAoB,EAAE,EACtC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,2BAA2B,GACjD,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAiB1C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { readDraftString } from '@entifix/core';
|
|
3
|
+
import { useEntityLinkSource } from '@entifix/react-integration';
|
|
4
|
+
/**
|
|
5
|
+
* Turns a fixed list of pickers into the `linkSources` map `EntityForm` takes.
|
|
6
|
+
*
|
|
7
|
+
* **Why the rule is disabled below.** `links` is built once, at factory time,
|
|
8
|
+
* and closed over by the generated form — it is the same array object on every
|
|
9
|
+
* render of a given component, so the hook count is fixed. That is exactly the
|
|
10
|
+
* invariant `react-hooks/rules-of-hooks` exists to protect and exactly the one
|
|
11
|
+
* it cannot see, because it reasons syntactically about the loop rather than
|
|
12
|
+
* about the array's provenance. The loop is isolated in this module so the
|
|
13
|
+
* disable is written once, with its reason, instead of once per generated form.
|
|
14
|
+
*
|
|
15
|
+
* The alternative — one child component per picker, reporting its source
|
|
16
|
+
* upward — would put the sources one render behind the draft that feeds them,
|
|
17
|
+
* which is a stale suggestion list rather than a lint error.
|
|
18
|
+
*/ export function useEntityLinkSources(links, { values, selection }) {
|
|
19
|
+
const sources = {};
|
|
20
|
+
for (const link of links){
|
|
21
|
+
// Read as a string: a relation's draft value is a foreign key, and a
|
|
22
|
+
// member that came back from storage in any other shape must read as unset
|
|
23
|
+
// rather than be handed to the picker as an id.
|
|
24
|
+
const held = readDraftString(values, link.field);
|
|
25
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- fixed-length loop over a factory-time array; see the note above.
|
|
26
|
+
sources[link.field] = useEntityLinkSource(link.config, {
|
|
27
|
+
descriptor: link.descriptor,
|
|
28
|
+
// An empty draft entry is "unset", not an id. Nothing enforces the
|
|
29
|
+
// reference across a store boundary, so `''` would be a dangling id.
|
|
30
|
+
selectedId: held === '' ? undefined : held,
|
|
31
|
+
selectedEntity: selection[link.field]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return sources;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=use-entity-link-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/crud/use-entity-link-sources.ts"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityDraft,\n type EntityLinkSelection,\n type EntityLinkSource,\n readDraftString,\n} from '@entifix/core';\nimport { useEntityLinkSource } from '@entifix/react-integration';\n\nimport type { EntityCrudLinkSource } from './make-entity-crud.types.js';\n\nexport interface UseEntityLinkSourcesOptions {\n /** The form's current draft — ids are the truth. */\n readonly values: EntityDraft;\n /** The picked-instance sidecar, so a chosen name needs no round trip. */\n readonly selection: EntityLinkSelection;\n}\n\n/**\n * Turns a fixed list of pickers into the `linkSources` map `EntityForm` takes.\n *\n * **Why the rule is disabled below.** `links` is built once, at factory time,\n * and closed over by the generated form — it is the same array object on every\n * render of a given component, so the hook count is fixed. That is exactly the\n * invariant `react-hooks/rules-of-hooks` exists to protect and exactly the one\n * it cannot see, because it reasons syntactically about the loop rather than\n * about the array's provenance. The loop is isolated in this module so the\n * disable is written once, with its reason, instead of once per generated form.\n *\n * The alternative — one child component per picker, reporting its source\n * upward — would put the sources one render behind the draft that feeds them,\n * which is a stale suggestion list rather than a lint error.\n */\nexport function useEntityLinkSources(\n links: readonly EntityCrudLinkSource[],\n { values, selection }: UseEntityLinkSourcesOptions,\n): Record<string, EntityLinkSource<Entity>> {\n const sources: Record<string, EntityLinkSource<Entity>> = {};\n for (const link of links) {\n // Read as a string: a relation's draft value is a foreign key, and a\n // member that came back from storage in any other shape must read as unset\n // rather than be handed to the picker as an id.\n const held = readDraftString(values, link.field);\n // eslint-disable-next-line react-hooks/rules-of-hooks -- fixed-length loop over a factory-time array; see the note above.\n sources[link.field] = useEntityLinkSource(link.config, {\n descriptor: link.descriptor,\n // An empty draft entry is \"unset\", not an id. Nothing enforces the\n // reference across a store boundary, so `''` would be a dangling id.\n selectedId: held === '' ? undefined : held,\n selectedEntity: selection[link.field],\n });\n }\n return sources;\n}\n"],"names":["readDraftString","useEntityLinkSource","useEntityLinkSources","links","values","selection","sources","link","held","field","config","descriptor","selectedId","undefined","selectedEntity"],"mappings":"AAAA;AAEA,SAKEA,eAAe,QACV,gBAAgB;AACvB,SAASC,mBAAmB,QAAQ,6BAA6B;AAWjE;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,qBACdC,KAAsC,EACtC,EAAEC,MAAM,EAAEC,SAAS,EAA+B;IAElD,MAAMC,UAAoD,CAAC;IAC3D,KAAK,MAAMC,QAAQJ,MAAO;QACxB,qEAAqE;QACrE,2EAA2E;QAC3E,gDAAgD;QAChD,MAAMK,OAAOR,gBAAgBI,QAAQG,KAAKE,KAAK;QAC/C,0HAA0H;QAC1HH,OAAO,CAACC,KAAKE,KAAK,CAAC,GAAGR,oBAAoBM,KAAKG,MAAM,EAAE;YACrDC,YAAYJ,KAAKI,UAAU;YAC3B,mEAAmE;YACnE,qEAAqE;YACrEC,YAAYJ,SAAS,KAAKK,YAAYL;YACtCM,gBAAgBT,SAAS,CAACE,KAAKE,KAAK,CAAC;QACvC;IACF;IACA,OAAOH;AACT"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe route handlers for the Next apps, mirroring what
|
|
3
|
+
* `@entifix/service-shell` mounts on the backends.
|
|
4
|
+
*
|
|
5
|
+
* They ship from `@entifix/next-shell/server` because a route handler must
|
|
6
|
+
* not come from the `"use client"` bundle.
|
|
7
|
+
*/
|
|
8
|
+
export interface HealthRouteOptions {
|
|
9
|
+
/** The app's package name, echoed back so a probe says what it reached. */
|
|
10
|
+
readonly app: string;
|
|
11
|
+
/** Base URL of config-service, e.g. `http://localhost:3190`. */
|
|
12
|
+
readonly configApiUrl: string;
|
|
13
|
+
/** The app's config key, i.e. the `:service` in `GET /api/config/:service`. */
|
|
14
|
+
readonly configKey: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build the `GET` handlers for `/api/health`, `/api/health/live` and
|
|
18
|
+
* `/api/health/ready`.
|
|
19
|
+
*
|
|
20
|
+
* **Liveness** answers from the process alone — no config-service, no backend,
|
|
21
|
+
* no session. That is what makes it usable as Playwright's `readyPath` and what
|
|
22
|
+
* keeps a dependency blip from getting the app killed by Kubernetes later.
|
|
23
|
+
*
|
|
24
|
+
* **Readiness** checks the one dependency the app cannot render without: its
|
|
25
|
+
* configuration. It deliberately does *not* chain to the domain backend —
|
|
26
|
+
* cascading readiness turns one degraded service into a fleet-wide outage, and
|
|
27
|
+
* a page that renders with a degraded backend is still worth serving.
|
|
28
|
+
*/
|
|
29
|
+
export declare const createHealthRoutes: (options: HealthRouteOptions) => {
|
|
30
|
+
/** `GET /api/health` — the original endpoint, unchanged. */
|
|
31
|
+
health: () => Response;
|
|
32
|
+
/** `GET /api/health/live` */
|
|
33
|
+
live: () => Response;
|
|
34
|
+
/** `GET /api/health/ready` */
|
|
35
|
+
ready: () => Promise<Response>;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=health-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../../../src/lib/health/health-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAiCH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,kBAAkB;IAuB1D,4DAA4D;;IAG5D,6BAA6B;;IAQ7B,8BAA8B;;CAejC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe route handlers for the Next apps, mirroring what
|
|
3
|
+
* `@entifix/service-shell` mounts on the backends.
|
|
4
|
+
*
|
|
5
|
+
* They ship from `@entifix/next-shell/server` because a route handler must
|
|
6
|
+
* not come from the `"use client"` bundle.
|
|
7
|
+
*/ import { SERVICE_TOKEN_HEADER, serviceToken } from '../config/service-token.js';
|
|
8
|
+
/** How long a readiness result is reused, so probes cannot hammer config-service. */ const READY_CACHE_MS = 1000;
|
|
9
|
+
/** A readiness check that hangs is a readiness check that failed. */ const READY_TIMEOUT_MS = 2000;
|
|
10
|
+
/**
|
|
11
|
+
* Which build is answering: `next dev` or `next start`.
|
|
12
|
+
*
|
|
13
|
+
* Liveness carries it because an e2e run needs to know *which artifact* it
|
|
14
|
+
* attached to. Playwright reuses whatever already listens on the app's port, so
|
|
15
|
+
* a suite started while a dev server is up tests a different bundle against a
|
|
16
|
+
* different backend than the one it claims to — see `assertExpectedServer` in
|
|
17
|
+
* `@entifix/testing-e2e/playwright`.
|
|
18
|
+
*
|
|
19
|
+
* On liveness rather than readiness because liveness is the one endpoint that
|
|
20
|
+
* answers from the process alone, before config-service or any backend is up.
|
|
21
|
+
*
|
|
22
|
+
* Not an information leak worth avoiding: a dev build already announces itself
|
|
23
|
+
* in its asset paths long before anything asks this endpoint.
|
|
24
|
+
*/ const buildMode = ()=>process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
|
25
|
+
/**
|
|
26
|
+
* Build the `GET` handlers for `/api/health`, `/api/health/live` and
|
|
27
|
+
* `/api/health/ready`.
|
|
28
|
+
*
|
|
29
|
+
* **Liveness** answers from the process alone — no config-service, no backend,
|
|
30
|
+
* no session. That is what makes it usable as Playwright's `readyPath` and what
|
|
31
|
+
* keeps a dependency blip from getting the app killed by Kubernetes later.
|
|
32
|
+
*
|
|
33
|
+
* **Readiness** checks the one dependency the app cannot render without: its
|
|
34
|
+
* configuration. It deliberately does *not* chain to the domain backend —
|
|
35
|
+
* cascading readiness turns one degraded service into a fleet-wide outage, and
|
|
36
|
+
* a page that renders with a degraded backend is still worth serving.
|
|
37
|
+
*/ export const createHealthRoutes = (options)=>{
|
|
38
|
+
let cached = null;
|
|
39
|
+
const checkConfig = async ()=>{
|
|
40
|
+
try {
|
|
41
|
+
// The fleet token is not optional here: `GET /api/config/:service` serves
|
|
42
|
+
// real credentials and answers `401` without it, which a readiness probe
|
|
43
|
+
// reads as "degraded" — permanently, for an app that is in fact fine.
|
|
44
|
+
const response = await fetch(`${options.configApiUrl}/api/config/${options.configKey}`, {
|
|
45
|
+
signal: AbortSignal.timeout(READY_TIMEOUT_MS),
|
|
46
|
+
cache: 'no-store',
|
|
47
|
+
headers: {
|
|
48
|
+
[SERVICE_TOKEN_HEADER]: serviceToken()
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return response.ok;
|
|
52
|
+
} catch (unused) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
/** `GET /api/health` — the original endpoint, unchanged. */ health: ()=>Response.json({
|
|
58
|
+
status: 'ok',
|
|
59
|
+
app: options.app
|
|
60
|
+
}),
|
|
61
|
+
/** `GET /api/health/live` */ live: ()=>Response.json({
|
|
62
|
+
status: 'live',
|
|
63
|
+
app: options.app,
|
|
64
|
+
mode: buildMode()
|
|
65
|
+
}),
|
|
66
|
+
/** `GET /api/health/ready` */ ready: async ()=>{
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (cached === null || now - cached.at >= READY_CACHE_MS) {
|
|
69
|
+
cached = {
|
|
70
|
+
at: now,
|
|
71
|
+
ready: await checkConfig()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return cached.ready ? Response.json({
|
|
75
|
+
status: 'ready',
|
|
76
|
+
app: options.app
|
|
77
|
+
}) : Response.json({
|
|
78
|
+
status: 'degraded',
|
|
79
|
+
app: options.app,
|
|
80
|
+
failing: [
|
|
81
|
+
'config'
|
|
82
|
+
]
|
|
83
|
+
}, {
|
|
84
|
+
status: 503
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=health-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/health/health-routes.ts"],"sourcesContent":["/**\n * Probe route handlers for the Next apps, mirroring what\n * `@entifix/service-shell` mounts on the backends.\n *\n * They ship from `@entifix/next-shell/server` because a route handler must\n * not come from the `\"use client\"` bundle.\n */\n\nimport { SERVICE_TOKEN_HEADER, serviceToken } from '../config/service-token.js';\n\n/** How long a readiness result is reused, so probes cannot hammer config-service. */\nconst READY_CACHE_MS = 1_000;\n\n/** A readiness check that hangs is a readiness check that failed. */\nconst READY_TIMEOUT_MS = 2_000;\n\n/**\n * Which build is answering: `next dev` or `next start`.\n *\n * Liveness carries it because an e2e run needs to know *which artifact* it\n * attached to. Playwright reuses whatever already listens on the app's port, so\n * a suite started while a dev server is up tests a different bundle against a\n * different backend than the one it claims to — see `assertExpectedServer` in\n * `@entifix/testing-e2e/playwright`.\n *\n * On liveness rather than readiness because liveness is the one endpoint that\n * answers from the process alone, before config-service or any backend is up.\n *\n * Not an information leak worth avoiding: a dev build already announces itself\n * in its asset paths long before anything asks this endpoint.\n */\nconst buildMode = (): 'development' | 'production' =>\n process.env.NODE_ENV === 'production' ? 'production' : 'development';\n\ninterface ReadyState {\n readonly at: number;\n readonly ready: boolean;\n}\n\nexport interface HealthRouteOptions {\n /** The app's package name, echoed back so a probe says what it reached. */\n readonly app: string;\n /** Base URL of config-service, e.g. `http://localhost:3190`. */\n readonly configApiUrl: string;\n /** The app's config key, i.e. the `:service` in `GET /api/config/:service`. */\n readonly configKey: string;\n}\n\n/**\n * Build the `GET` handlers for `/api/health`, `/api/health/live` and\n * `/api/health/ready`.\n *\n * **Liveness** answers from the process alone — no config-service, no backend,\n * no session. That is what makes it usable as Playwright's `readyPath` and what\n * keeps a dependency blip from getting the app killed by Kubernetes later.\n *\n * **Readiness** checks the one dependency the app cannot render without: its\n * configuration. It deliberately does *not* chain to the domain backend —\n * cascading readiness turns one degraded service into a fleet-wide outage, and\n * a page that renders with a degraded backend is still worth serving.\n */\nexport const createHealthRoutes = (options: HealthRouteOptions) => {\n let cached: ReadyState | null = null;\n\n const checkConfig = async (): Promise<boolean> => {\n try {\n // The fleet token is not optional here: `GET /api/config/:service` serves\n // real credentials and answers `401` without it, which a readiness probe\n // reads as \"degraded\" — permanently, for an app that is in fact fine.\n const response = await fetch(\n `${options.configApiUrl}/api/config/${options.configKey}`,\n {\n signal: AbortSignal.timeout(READY_TIMEOUT_MS),\n cache: 'no-store',\n headers: { [SERVICE_TOKEN_HEADER]: serviceToken() },\n },\n );\n return response.ok;\n } catch {\n return false;\n }\n };\n\n return {\n /** `GET /api/health` — the original endpoint, unchanged. */\n health: () => Response.json({ status: 'ok', app: options.app }),\n\n /** `GET /api/health/live` */\n live: () =>\n Response.json({\n status: 'live',\n app: options.app,\n mode: buildMode(),\n }),\n\n /** `GET /api/health/ready` */\n ready: async () => {\n const now = Date.now();\n if (cached === null || now - cached.at >= READY_CACHE_MS) {\n cached = { at: now, ready: await checkConfig() };\n }\n\n return cached.ready\n ? Response.json({ status: 'ready', app: options.app })\n : Response.json(\n { status: 'degraded', app: options.app, failing: ['config'] },\n { status: 503 },\n );\n },\n };\n};\n"],"names":["SERVICE_TOKEN_HEADER","serviceToken","READY_CACHE_MS","READY_TIMEOUT_MS","buildMode","process","env","NODE_ENV","createHealthRoutes","options","cached","checkConfig","response","fetch","configApiUrl","configKey","signal","AbortSignal","timeout","cache","headers","ok","health","Response","json","status","app","live","mode","ready","now","Date","at","failing"],"mappings":"AAAA;;;;;;CAMC,GAED,SAASA,oBAAoB,EAAEC,YAAY,QAAQ,6BAA6B;AAEhF,mFAAmF,GACnF,MAAMC,iBAAiB;AAEvB,mEAAmE,GACnE,MAAMC,mBAAmB;AAEzB;;;;;;;;;;;;;;CAcC,GACD,MAAMC,YAAY,IAChBC,QAAQC,GAAG,CAACC,QAAQ,KAAK,eAAe,eAAe;AAgBzD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAACC;IACjC,IAAIC,SAA4B;IAEhC,MAAMC,cAAc;QAClB,IAAI;YACF,0EAA0E;YAC1E,yEAAyE;YACzE,sEAAsE;YACtE,MAAMC,WAAW,MAAMC,MACrB,GAAGJ,QAAQK,YAAY,CAAC,YAAY,EAAEL,QAAQM,SAAS,EAAE,EACzD;gBACEC,QAAQC,YAAYC,OAAO,CAACf;gBAC5BgB,OAAO;gBACPC,SAAS;oBAAE,CAACpB,qBAAqB,EAAEC;gBAAe;YACpD;YAEF,OAAOW,SAASS,EAAE;QACpB,EAAE,eAAM;YACN,OAAO;QACT;IACF;IAEA,OAAO;QACL,0DAA0D,GAC1DC,QAAQ,IAAMC,SAASC,IAAI,CAAC;gBAAEC,QAAQ;gBAAMC,KAAKjB,QAAQiB,GAAG;YAAC;QAE7D,2BAA2B,GAC3BC,MAAM,IACJJ,SAASC,IAAI,CAAC;gBACZC,QAAQ;gBACRC,KAAKjB,QAAQiB,GAAG;gBAChBE,MAAMxB;YACR;QAEF,4BAA4B,GAC5ByB,OAAO;YACL,MAAMC,MAAMC,KAAKD,GAAG;YACpB,IAAIpB,WAAW,QAAQoB,MAAMpB,OAAOsB,EAAE,IAAI9B,gBAAgB;gBACxDQ,SAAS;oBAAEsB,IAAIF;oBAAKD,OAAO,MAAMlB;gBAAc;YACjD;YAEA,OAAOD,OAAOmB,KAAK,GACfN,SAASC,IAAI,CAAC;gBAAEC,QAAQ;gBAASC,KAAKjB,QAAQiB,GAAG;YAAC,KAClDH,SAASC,IAAI,CACX;gBAAEC,QAAQ;gBAAYC,KAAKjB,QAAQiB,GAAG;gBAAEO,SAAS;oBAAC;iBAAS;YAAC,GAC5D;gBAAER,QAAQ;YAAI;QAEtB;IACF;AACF,EAAE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/lib/i18n/catalog/en.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,cAAc,SAAS,EAAE,KAAK,CAAC;AAE9C,eAAO,MAAM,KAAK,EAAE,OAsWnB,CAAC"}
|