@jskit-ai/http-web 0.1.92 → 0.1.93

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.
Files changed (51) hide show
  1. package/package.json +6 -6
  2. package/patterns/crud-screen-set/PATTERN.md +11 -1
  3. package/src/client/bulkActions.js +13 -40
  4. package/src/client/components/CrudAddEditScreen.vue +1 -9
  5. package/src/client/components/CrudListBulkActionSurface.vue +2 -5
  6. package/src/client/components/CrudListFilterControls.vue +175 -0
  7. package/src/client/components/CrudListFilterSurface.vue +5 -248
  8. package/src/client/components/CrudListScreen.vue +1 -28
  9. package/src/client/components/CrudViewScreen.vue +1 -9
  10. package/src/client/composables/crud/crudJsonApiTransportSupport.js +20 -42
  11. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +2 -13
  12. package/src/client/composables/crud/crudLookupFieldRuntime.js +2 -27
  13. package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -7
  14. package/src/client/composables/internal/crudListFilterLookupSupport.js +2 -1
  15. package/src/client/composables/internal/useOperationScope.js +5 -7
  16. package/src/client/composables/records/useAddEdit.js +12 -11
  17. package/src/client/composables/records/useCrudAddEdit.js +74 -22
  18. package/src/client/composables/records/useCrudList.js +0 -6
  19. package/src/client/composables/records/useCrudView.js +0 -6
  20. package/src/client/composables/records/useList.js +2 -8
  21. package/src/client/composables/records/useView.js +3 -8
  22. package/src/client/composables/runtime/addEditUiRuntime.js +12 -60
  23. package/src/client/composables/runtime/listUiRuntime.js +11 -36
  24. package/src/client/composables/runtime/useFieldErrorBag.js +3 -3
  25. package/src/client/composables/runtime/viewUiRuntime.js +14 -62
  26. package/src/client/composables/support/errorMessageHelpers.js +6 -48
  27. package/src/client/composables/support/resourceLoadStateHelpers.js +67 -156
  28. package/src/client/composables/support/routeTemplateHelpers.js +57 -0
  29. package/src/client/composables/useAccess.js +12 -17
  30. package/src/client/composables/useCommand.js +0 -1
  31. package/src/client/composables/useCrudDeleteAction.js +7 -5
  32. package/src/client/composables/useCrudListBulkActions.js +9 -21
  33. package/src/client/composables/useCrudListFilterLookups.js +1 -3
  34. package/src/client/composables/useCrudListParentTitle.js +0 -2
  35. package/src/client/composables/useCrudListRowActions.js +8 -24
  36. package/src/client/composables/useCrudListScreen.js +4 -5
  37. package/src/client/composables/useCrudViewScreen.js +2 -3
  38. package/src/client/composables/useRealtimeQueryInvalidation.js +3 -10
  39. package/src/client/composables/useScopeRuntime.js +2 -5
  40. package/src/client/rowActions.js +13 -40
  41. package/src/client/support/crudListActionSupport.js +54 -0
  42. package/test/crudAddEditActions.test.js +59 -19
  43. package/test/crudListFilterSurface.test.js +7 -4
  44. package/test/listUiRuntime.test.js +3 -1
  45. package/test/requestTransportOptions.test.js +24 -0
  46. package/test/resourceLoadStateHelpers.test.js +32 -0
  47. package/test/useAccess.test.js +62 -0
  48. package/test/useCrudDeleteAction.test.js +57 -2
  49. package/test/useCrudListScreen.test.js +45 -0
  50. package/test/useListRouteInitialization.test.js +5 -44
  51. package/src/client/composables/runtime/operationAdapters.js +0 -34
@@ -1,24 +1,11 @@
1
- import { computed, unref } from "vue";
1
+ import { computed } from "vue";
2
2
  import { asPlainObject } from "../support/scopeHelpers.js";
3
3
  import {
4
+ createRouteTemplateResolver,
4
5
  normalizeRouteParamName,
5
- resolveRouteParamsSource,
6
- resolveScopedRoutePathname,
7
- resolveRouteTemplateLocation,
8
6
  toRouteParamValue
9
7
  } from "../support/routeTemplateHelpers.js";
10
8
 
11
- function toResolvedRecordId({ routeParams, recordIdParam, routeRecordId }) {
12
- const explicitRecordId = toRouteParamValue(
13
- typeof routeRecordId === "function" ? routeRecordId() : unref(routeRecordId)
14
- );
15
- if (explicitRecordId) {
16
- return explicitRecordId;
17
- }
18
-
19
- return toRouteParamValue(routeParams[recordIdParam]);
20
- }
21
-
22
9
  function resolveSavedRecordId(payload, saveRecordIdSelector) {
23
10
  const sourcePayload = asPlainObject(payload);
24
11
  if (typeof saveRecordIdSelector === "function") {
@@ -46,56 +33,21 @@ function createAddEditUiRuntime({
46
33
  const normalizedViewUrlTemplate = String(viewUrlTemplate || "").trim();
47
34
  const normalizedListUrlTemplate = String(listUrlTemplate || "").trim();
48
35
 
49
- function resolveTemplatePath(urlTemplate = "", extraParams = {}) {
50
- const normalizedTemplate = String(urlTemplate || "").trim();
51
- if (!normalizedTemplate) {
52
- return "";
53
- }
54
-
55
- const currentRouteParams = resolveRouteParamsSource(routeParams);
56
- const sourceParams = {
57
- ...currentRouteParams,
58
- ...asPlainObject(extraParams)
59
- };
60
- const currentRouteRecordId = toResolvedRecordId({
61
- routeParams: currentRouteParams,
62
- recordIdParam: normalizedRecordIdParam,
63
- routeRecordId
64
- });
65
- const resolvedRecordId = toRouteParamValue(sourceParams[normalizedRecordIdParam]) ||
66
- currentRouteRecordId;
67
- sourceParams[normalizedRecordIdParam] = resolvedRecordId;
68
- const currentPathname = resolveScopedRoutePathname({
69
- currentPathname: routePath,
70
- params: currentRouteParams,
71
- orderedParamNames: routeParamNames,
72
- anchorParamName: normalizedRecordIdParam,
73
- anchorParamValue: currentRouteRecordId,
74
- anchorMode: "after"
75
- });
76
-
77
- return resolveRouteTemplateLocation(normalizedTemplate, {
78
- params: sourceParams,
79
- currentPathname
80
- });
81
- }
36
+ const { resolveRecordId, resolveTemplatePath } = createRouteTemplateResolver({
37
+ recordIdParam: normalizedRecordIdParam,
38
+ routeParams,
39
+ routeParamNames,
40
+ routePath,
41
+ routeRecordId,
42
+ anchorMode: "after"
43
+ });
82
44
 
83
- const recordId = computed(() =>
84
- toResolvedRecordId({
85
- routeParams: resolveRouteParamsSource(routeParams),
86
- recordIdParam: normalizedRecordIdParam,
87
- routeRecordId
88
- })
89
- );
45
+ const recordId = computed(() => resolveRecordId());
90
46
 
91
47
  const apiSuffix = computed(() => resolveTemplatePath(normalizedApiUrlTemplate));
92
48
  const listUrl = computed(() => resolveTemplatePath(normalizedListUrlTemplate));
93
49
  const cancelUrl = computed(() => resolveTemplatePath(normalizedViewUrlTemplate) || listUrl.value);
94
50
 
95
- function resolveParams(urlTemplate = "", extraParams = {}) {
96
- return resolveTemplatePath(urlTemplate, extraParams);
97
- }
98
-
99
51
  function resolveViewUrl(recordIdLike = "") {
100
52
  const targetRecordId = toRouteParamValue(recordIdLike);
101
53
  if (!targetRecordId) {
@@ -121,7 +73,7 @@ function createAddEditUiRuntime({
121
73
  apiSuffix,
122
74
  listUrl,
123
75
  cancelUrl,
124
- resolveParams,
76
+ resolveParams: resolveTemplatePath,
125
77
  resolveViewUrl,
126
78
  resolveSavedViewUrl
127
79
  });
@@ -1,11 +1,9 @@
1
1
  import { computed } from "vue";
2
2
  import { asPlainObject } from "../support/scopeHelpers.js";
3
3
  import {
4
+ createRouteTemplateResolver,
4
5
  normalizeRouteParamName,
5
- toRouteParamValue,
6
- resolveRouteParamsSource,
7
- resolveScopedRoutePathname,
8
- resolveRouteTemplateLocation
6
+ toRouteParamValue
9
7
  } from "../support/routeTemplateHelpers.js";
10
8
 
11
9
  function resolveRecordId(record, recordIdSelector) {
@@ -35,39 +33,17 @@ function createListUiRuntime({
35
33
  const normalizedEditUrlTemplate = String(editUrlTemplate || "").trim();
36
34
  const hasViewUrl = Boolean(normalizedViewUrlTemplate);
37
35
  const hasEditUrl = Boolean(normalizedEditUrlTemplate);
38
- const actionColumnCount = (hasViewUrl ? 1 : 0) + (hasEditUrl ? 1 : 0);
39
36
  const normalizedItems = computed(() => (Array.isArray(items?.value) ? items.value : []));
40
37
  const showListSkeleton = computed(() => Boolean(isInitialLoading?.value && normalizedItems.value.length < 1));
41
38
 
42
- function resolveTemplatePath(urlTemplate = "", extraParams = {}) {
43
- const normalizedTemplate = String(urlTemplate || "").trim();
44
- if (!normalizedTemplate) {
45
- return "";
46
- }
47
-
48
- const currentRouteParams = resolveRouteParamsSource(routeParams);
49
- const sourceParams = {
50
- ...currentRouteParams,
51
- ...asPlainObject(extraParams)
52
- };
53
- const currentPathname = resolveScopedRoutePathname({
54
- currentPathname: routePath,
55
- params: currentRouteParams,
56
- orderedParamNames: routeParamNames,
57
- anchorParamName: normalizedRecordIdParam,
58
- anchorParamValue: currentRouteParams[normalizedRecordIdParam],
59
- anchorMode: "before"
60
- });
61
-
62
- return resolveRouteTemplateLocation(normalizedTemplate, {
63
- params: sourceParams,
64
- currentPathname
65
- });
66
- }
67
-
68
- function resolveParams(urlTemplate = "", extraParams = {}) {
69
- return resolveTemplatePath(urlTemplate, extraParams);
70
- }
39
+ const { resolveTemplatePath } = createRouteTemplateResolver({
40
+ recordIdParam: normalizedRecordIdParam,
41
+ routeParams,
42
+ routeParamNames,
43
+ routePath,
44
+ anchorMode: "before",
45
+ fillRecordId: false
46
+ });
71
47
 
72
48
  function resolveRowKey(record, index) {
73
49
  const recordId = resolveRecordId(record, recordIdSelector);
@@ -111,9 +87,8 @@ function createListUiRuntime({
111
87
  return Object.freeze({
112
88
  hasViewUrl,
113
89
  hasEditUrl,
114
- actionColumnCount,
115
90
  showListSkeleton,
116
- resolveParams,
91
+ resolveParams: resolveTemplatePath,
117
92
  resolveRowKey,
118
93
  resolveViewUrl,
119
94
  resolveEditUrl
@@ -1,13 +1,13 @@
1
1
  import { reactive } from "vue";
2
2
 
3
- function normalizeKeys(keys) {
3
+ function normalizeFieldErrorKeys(keys) {
4
4
  return Array.isArray(keys)
5
5
  ? keys.map((entry) => String(entry || "").trim()).filter(Boolean)
6
6
  : [];
7
7
  }
8
8
 
9
9
  function useFieldErrorBag(keys = []) {
10
- const normalizedKeys = normalizeKeys(keys);
10
+ const normalizedKeys = normalizeFieldErrorKeys(keys);
11
11
  const hasFixedKeys = normalizedKeys.length > 0;
12
12
  const errors = reactive(
13
13
  Object.fromEntries(normalizedKeys.map((key) => [key, ""]))
@@ -58,4 +58,4 @@ function useFieldErrorBag(keys = []) {
58
58
  });
59
59
  }
60
60
 
61
- export { useFieldErrorBag };
61
+ export { normalizeFieldErrorKeys, useFieldErrorBag };
@@ -1,24 +1,9 @@
1
- import { computed, unref } from "vue";
2
- import { asPlainObject } from "../support/scopeHelpers.js";
1
+ import { computed } from "vue";
3
2
  import {
4
- normalizeRouteParamName,
5
- resolveRouteParamsSource,
6
- resolveScopedRoutePathname,
7
- resolveRouteTemplateLocation,
8
- toRouteParamValue
3
+ createRouteTemplateResolver,
4
+ normalizeRouteParamName
9
5
  } from "../support/routeTemplateHelpers.js";
10
6
 
11
- function resolveRecordId({ routeParams, recordIdParam, routeRecordId }) {
12
- const explicitRecordId = toRouteParamValue(
13
- typeof routeRecordId === "function" ? routeRecordId() : unref(routeRecordId)
14
- );
15
- if (explicitRecordId) {
16
- return explicitRecordId;
17
- }
18
-
19
- return toRouteParamValue(routeParams[recordIdParam]);
20
- }
21
-
22
7
  function createViewUiRuntime({
23
8
  recordIdParam = "recordId",
24
9
  routeParams = null,
@@ -36,60 +21,27 @@ function createViewUiRuntime({
36
21
  const normalizedListUrlTemplate = String(listUrlTemplate || "").trim();
37
22
  const normalizedEditUrlTemplate = String(editUrlTemplate || "").trim();
38
23
 
39
- function resolveTemplatePath(urlTemplate = "", extraParams = {}) {
40
- const normalizedTemplate = String(urlTemplate || "").trim();
41
- if (!normalizedTemplate) {
42
- return "";
43
- }
44
-
45
- const currentRouteParams = resolveRouteParamsSource(routeParams);
46
- const sourceParams = {
47
- ...currentRouteParams,
48
- ...asPlainObject(extraParams)
49
- };
50
- const resolvedRecordId = toRouteParamValue(sourceParams[normalizedRecordIdParam]) ||
51
- resolveRecordId({
52
- routeParams: currentRouteParams,
53
- recordIdParam: normalizedRecordIdParam,
54
- routeRecordId
55
- });
56
- sourceParams[normalizedRecordIdParam] = resolvedRecordId;
57
- const currentPathname = resolveScopedRoutePathname({
58
- currentPathname: routePath,
59
- params: currentRouteParams,
60
- orderedParamNames: routeParamNames,
61
- anchorParamName: normalizedRecordIdParam,
62
- anchorParamValue: resolvedRecordId,
63
- anchorMode: "at"
64
- });
65
-
66
- return resolveRouteTemplateLocation(normalizedTemplate, {
67
- params: sourceParams,
68
- currentPathname
69
- });
70
- }
24
+ const { resolveRecordId, resolveTemplatePath } = createRouteTemplateResolver({
25
+ recordIdParam: normalizedRecordIdParam,
26
+ routeParams,
27
+ routeParamNames,
28
+ routePath,
29
+ routeRecordId,
30
+ anchorMode: "at",
31
+ anchorOnTargetRecord: true
32
+ });
71
33
 
72
- const recordId = computed(() =>
73
- resolveRecordId({
74
- routeParams: resolveRouteParamsSource(routeParams),
75
- recordIdParam: normalizedRecordIdParam,
76
- routeRecordId
77
- })
78
- );
34
+ const recordId = computed(() => resolveRecordId());
79
35
  const apiSuffix = computed(() => resolveTemplatePath(normalizedApiUrlTemplate));
80
36
  const listUrl = computed(() => resolveTemplatePath(normalizedListUrlTemplate));
81
37
  const editUrl = computed(() => resolveTemplatePath(normalizedEditUrlTemplate));
82
38
 
83
- function resolveParams(urlTemplate = "", extraParams = {}) {
84
- return resolveTemplatePath(urlTemplate, extraParams);
85
- }
86
-
87
39
  return Object.freeze({
88
40
  recordId,
89
41
  apiSuffix,
90
42
  listUrl,
91
43
  editUrl,
92
- resolveParams
44
+ resolveParams: resolveTemplatePath
93
45
  });
94
46
  }
95
47
 
@@ -1,55 +1,9 @@
1
- function normalizeErrorStatus(error) {
2
- if (error == null || typeof error !== "object") {
3
- return null;
4
- }
5
-
6
- const hasStatus = Object.prototype.hasOwnProperty.call(error, "status");
7
- const hasStatusCode = Object.prototype.hasOwnProperty.call(error, "statusCode");
8
- if (!hasStatus && !hasStatusCode) {
9
- return null;
10
- }
11
-
12
- const status = Number(hasStatus ? error.status : error.statusCode);
13
- return Number.isInteger(status) ? status : null;
14
- }
15
-
16
- function isGenericTransportMessage(error) {
17
- const normalizedMessage = String(error?.message || "").trim();
18
- if (!normalizedMessage) {
19
- return false;
20
- }
21
-
22
- const status = normalizeErrorStatus(error);
23
- if (status === 0) {
24
- return true;
25
- }
26
-
27
- return status != null && status >= 400 && normalizedMessage === `Request failed with status ${status}.`;
28
- }
29
-
30
- function toQueryErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
31
- if (!error) {
32
- return "";
33
- }
34
-
35
- const normalizedFallback = String(fallbackMessage || "").trim();
36
- const normalizedMessage = String(error?.message || "").trim();
37
- if (normalizedMessage && !isGenericTransportMessage(error)) {
38
- return normalizedMessage;
39
- }
40
- if (normalizedFallback) {
41
- return normalizedFallback;
42
- }
43
- if (normalizedMessage) {
44
- return normalizedMessage;
45
- }
46
- return String(defaultMessage || "Request failed.").trim();
47
- }
1
+ import { isGenericHttpErrorMessage } from "@jskit-ai/http-runtime/client";
48
2
 
49
3
  function toUiErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
50
4
  const normalizedFallback = String(fallbackMessage || "").trim();
51
5
  const normalizedMessage = String(error?.message || "").trim();
52
- if (normalizedMessage && !isGenericTransportMessage(error)) {
6
+ if (normalizedMessage && !isGenericHttpErrorMessage(error)) {
53
7
  return normalizedMessage;
54
8
  }
55
9
  if (normalizedFallback) {
@@ -62,6 +16,10 @@ function toUiErrorMessage(error, fallbackMessage = "", defaultMessage = "Request
62
16
  return String(defaultMessage || "Request failed.").trim();
63
17
  }
64
18
 
19
+ function toQueryErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
20
+ return error ? toUiErrorMessage(error, fallbackMessage, defaultMessage) : "";
21
+ }
22
+
65
23
  export {
66
24
  toQueryErrorMessage,
67
25
  toUiErrorMessage
@@ -1,4 +1,5 @@
1
1
  import { unref } from "vue";
2
+ import { hasValue, isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
3
 
3
4
  function hasResolvedQueryData({ query = null, data = null } = {}) {
4
5
  const querySucceeded = Boolean(unref(query?.isSuccess));
@@ -8,20 +9,8 @@ function hasResolvedQueryData({ query = null, data = null } = {}) {
8
9
  }
9
10
 
10
11
  function mergeQueryMeta(queryOptions = null, meta = {}) {
11
- const sourceOptions =
12
- queryOptions && typeof queryOptions === "object" && !Array.isArray(queryOptions) ? queryOptions : {};
13
- const sourceMeta =
14
- sourceOptions.meta && typeof sourceOptions.meta === "object" && !Array.isArray(sourceOptions.meta)
15
- ? sourceOptions.meta
16
- : {};
17
- const sourceJskitMeta =
18
- sourceMeta.jskit && typeof sourceMeta.jskit === "object" && !Array.isArray(sourceMeta.jskit)
19
- ? sourceMeta.jskit
20
- : {};
21
- const nextJskitMeta =
22
- meta.jskit && typeof meta.jskit === "object" && !Array.isArray(meta.jskit)
23
- ? meta.jskit
24
- : {};
12
+ const sourceOptions = isRecord(queryOptions) ? queryOptions : {};
13
+ const sourceMeta = isRecord(sourceOptions.meta) ? sourceOptions.meta : {};
25
14
 
26
15
  return {
27
16
  ...sourceOptions,
@@ -29,175 +18,97 @@ function mergeQueryMeta(queryOptions = null, meta = {}) {
29
18
  ...sourceMeta,
30
19
  ...meta,
31
20
  jskit: {
32
- ...sourceJskitMeta,
33
- ...nextJskitMeta
21
+ ...(isRecord(sourceMeta.jskit) ? sourceMeta.jskit : {}),
22
+ ...(isRecord(meta.jskit) ? meta.jskit : {})
34
23
  }
35
24
  }
36
25
  };
37
26
  }
38
27
 
39
- function normalizeText(value = "") {
40
- return String(value || "").trim();
41
- }
28
+ // Each requestRecovery option and the query meta key shell-web's request
29
+ // recovery reads for it.
30
+ const REQUEST_RECOVERY_META_KEYS = Object.freeze({
31
+ label: "requestRecoveryLabel",
32
+ source: "requestRecoverySource",
33
+ dedupeKey: "requestRecoveryDedupeKey",
34
+ method: "requestRecoveryMethod",
35
+ dedupeWindowMs: "requestRecoveryDedupeWindowMs"
36
+ });
37
+ const DISABLED_REQUEST_RECOVERY_META = Object.freeze({
38
+ jskit: Object.freeze({ requestRecovery: false })
39
+ });
42
40
 
43
- function firstNormalizedText(...values) {
44
- for (const value of values) {
45
- const normalized = normalizeText(value);
46
- if (normalized) {
47
- return normalized;
48
- }
41
+ function normalizeRequestRecoveryInput(requestRecovery = null) {
42
+ if (typeof requestRecovery === "string") {
43
+ return { label: requestRecovery };
49
44
  }
50
- return "";
51
- }
52
-
53
- function firstNonNegativeFiniteNumber(...values) {
54
- for (const value of values) {
55
- const numberValue = Number(value);
56
- if (Number.isFinite(numberValue)) {
57
- return Math.max(0, numberValue);
45
+ const source = isRecord(requestRecovery) ? requestRecovery : {};
46
+ for (const key of Object.keys(source)) {
47
+ if (key !== "enabled" && !Object.hasOwn(REQUEST_RECOVERY_META_KEYS, key)) {
48
+ throw new TypeError(
49
+ `requestRecovery.${key} is not supported; use ${Object.keys(REQUEST_RECOVERY_META_KEYS).join(", ")} or enabled.`
50
+ );
58
51
  }
59
52
  }
60
- return null;
61
- }
62
-
63
- function normalizePlainObject(value = null) {
64
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
53
+ return source;
65
54
  }
66
55
 
67
- function normalizeRequestRecoveryInput(requestRecovery = null) {
68
- if (typeof requestRecovery === "string") {
69
- return {
70
- label: requestRecovery
71
- };
56
+ function normalizeRequestRecoveryValue(name, value) {
57
+ if (name === "dedupeWindowMs") {
58
+ const numberValue = value == null || value === "" ? Number.NaN : Number(value);
59
+ return Number.isFinite(numberValue) ? Math.max(0, numberValue) : null;
72
60
  }
73
- return normalizePlainObject(requestRecovery);
74
- }
75
-
76
- function normalizeRequestRecoveryMethod(value = "") {
77
- return normalizeText(value).toUpperCase();
78
- }
79
-
80
- function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}) {
81
- return sourceJskitMeta.requestRecovery === false;
82
- }
83
-
84
- function hasUsableMetaValue(source = {}, key = "") {
85
- return Object.hasOwn(source, key) && source[key] !== undefined && source[key] !== null && source[key] !== "";
61
+ const text = normalizeText(value);
62
+ if (!text) {
63
+ return null;
64
+ }
65
+ return name === "method" ? text.toUpperCase() : text;
86
66
  }
87
67
 
68
+ // requestRecovery is `false`, a label string, or an object of the short option
69
+ // names above (plus `enabled: false`); missing values come from `defaults`.
88
70
  function buildRequestRecoveryMeta(requestRecovery = null, defaults = {}) {
89
71
  if (requestRecovery === false) {
90
- return {
91
- jskit: {
92
- requestRecovery: false
93
- }
94
- };
72
+ return DISABLED_REQUEST_RECOVERY_META;
95
73
  }
96
-
97
- const fallback = normalizePlainObject(defaults);
98
74
  const source = normalizeRequestRecoveryInput(requestRecovery);
99
- if (source.requestRecovery === false || source.enabled === false) {
100
- return {
101
- jskit: {
102
- requestRecovery: false
103
- }
104
- };
75
+ if (source.enabled === false) {
76
+ return DISABLED_REQUEST_RECOVERY_META;
105
77
  }
106
78
 
107
- const label = firstNormalizedText(
108
- source.requestRecoveryLabel,
109
- source.label,
110
- fallback.requestRecoveryLabel,
111
- fallback.label
112
- );
113
- const recoverySource = firstNormalizedText(
114
- source.requestRecoverySource,
115
- source.source,
116
- fallback.requestRecoverySource,
117
- fallback.source
118
- );
119
- const dedupeKey = firstNormalizedText(
120
- source.requestRecoveryDedupeKey,
121
- source.dedupeKey,
122
- fallback.requestRecoveryDedupeKey,
123
- fallback.dedupeKey
124
- );
125
- const method = normalizeRequestRecoveryMethod(
126
- firstNormalizedText(
127
- source.requestRecoveryMethod,
128
- source.method,
129
- fallback.requestRecoveryMethod,
130
- fallback.method
131
- )
132
- );
133
- const dedupeWindowMs = firstNonNegativeFiniteNumber(
134
- source.requestRecoveryDedupeWindowMs,
135
- source.dedupeWindowMs,
136
- fallback.requestRecoveryDedupeWindowMs,
137
- fallback.dedupeWindowMs
138
- );
139
-
79
+ const fallback = isRecord(defaults) ? defaults : {};
140
80
  const jskit = {};
141
- if (label) {
142
- jskit.requestRecoveryLabel = label;
143
- }
144
- if (recoverySource) {
145
- jskit.requestRecoverySource = recoverySource;
146
- }
147
- if (dedupeKey) {
148
- jskit.requestRecoveryDedupeKey = dedupeKey;
149
- }
150
- if (method) {
151
- jskit.requestRecoveryMethod = method;
152
- }
153
- if (dedupeWindowMs !== null) {
154
- jskit.requestRecoveryDedupeWindowMs = dedupeWindowMs;
81
+ for (const [name, metaKey] of Object.entries(REQUEST_RECOVERY_META_KEYS)) {
82
+ const value = normalizeRequestRecoveryValue(name, source[name]) ??
83
+ normalizeRequestRecoveryValue(name, fallback[name]);
84
+ if (value !== null) {
85
+ jskit[metaKey] = value;
86
+ }
155
87
  }
156
-
157
88
  return Object.keys(jskit).length > 0 ? { jskit } : {};
158
89
  }
159
90
 
160
- function resolveRequestRecoveryDefaults(queryOptions = null, defaults = {}) {
161
- const sourceOptions = normalizePlainObject(queryOptions);
162
- const sourceMeta = normalizePlainObject(sourceOptions.meta);
163
- const sourceJskitMeta = normalizePlainObject(sourceMeta.jskit);
164
- const fallback = normalizePlainObject(defaults);
165
- if (isRequestRecoveryMetaDisabled(sourceJskitMeta)) {
166
- return {};
167
- }
168
-
169
- const hasExplicitLabel = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryLabel");
170
- const hasExplicitSource = hasUsableMetaValue(sourceJskitMeta, "requestRecoverySource");
171
- const hasExplicitDedupeKey = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeKey");
172
- const hasExplicitDedupeWindowMs = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeWindowMs");
173
- const hasExplicitMethod = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryMethod");
174
-
175
- return {
176
- ...fallback,
177
- requestRecoveryLabel: hasExplicitLabel ? "" : fallback.requestRecoveryLabel,
178
- label: hasExplicitLabel ? "" : fallback.label,
179
- requestRecoverySource: hasExplicitSource ? "" : fallback.requestRecoverySource,
180
- source: hasExplicitSource ? "" : fallback.source,
181
- requestRecoveryDedupeKey: hasExplicitDedupeKey ? "" : fallback.requestRecoveryDedupeKey,
182
- dedupeKey: hasExplicitDedupeKey ? "" : fallback.dedupeKey,
183
- requestRecoveryDedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.requestRecoveryDedupeWindowMs,
184
- dedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.dedupeWindowMs,
185
- requestRecoveryMethod: hasExplicitMethod ? "" : fallback.requestRecoveryMethod,
186
- method: hasExplicitMethod ? "" : fallback.method
187
- };
188
- }
189
-
190
91
  function mergeRequestRecoveryQueryMeta(queryOptions = null, requestRecovery = null, defaults = {}) {
191
- const sourceOptions = normalizePlainObject(queryOptions);
192
- const effectiveDefaults =
193
- requestRecovery == null
194
- ? resolveRequestRecoveryDefaults(sourceOptions, defaults)
195
- : normalizePlainObject(defaults);
196
- const meta = buildRequestRecoveryMeta(requestRecovery, effectiveDefaults);
197
- if (Object.keys(meta).length < 1) {
198
- return sourceOptions;
92
+ const sourceOptions = isRecord(queryOptions) ? queryOptions : {};
93
+ let meta;
94
+ if (requestRecovery == null) {
95
+ const presetMeta = isRecord(sourceOptions.meta?.jskit) ? sourceOptions.meta.jskit : {};
96
+ if (presetMeta.requestRecovery === false) {
97
+ return sourceOptions;
98
+ }
99
+ // Composable defaults never replace a hint the query options already set.
100
+ const fallback = isRecord(defaults) ? defaults : {};
101
+ const unsetDefaults = {};
102
+ for (const [name, metaKey] of Object.entries(REQUEST_RECOVERY_META_KEYS)) {
103
+ if (!hasValue(presetMeta[metaKey])) {
104
+ unsetDefaults[name] = fallback[name];
105
+ }
106
+ }
107
+ meta = buildRequestRecoveryMeta(null, unsetDefaults);
108
+ } else {
109
+ meta = buildRequestRecoveryMeta(requestRecovery, defaults);
199
110
  }
200
- return mergeQueryMeta(sourceOptions, meta);
111
+ return Object.keys(meta).length > 0 ? mergeQueryMeta(sourceOptions, meta) : sourceOptions;
201
112
  }
202
113
 
203
114
  export {