@jskit-ai/http-web 0.1.1
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/fixtures/crud-list-date-filters/App.vue +107 -0
- package/fixtures/crud-list-date-filters/index.html +12 -0
- package/fixtures/crud-list-date-filters/main.js +24 -0
- package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
- package/package.json +150 -0
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListDateFilterControl.vue +188 -0
- package/src/client/components/CrudListFilterSurface.vue +388 -0
- package/src/client/components/CrudListRecordActionMenu.vue +120 -0
- package/src/client/components/CrudListScreen.vue +501 -0
- package/src/client/components/CrudViewScreen.vue +197 -0
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
- package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
- package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
- package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
- package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/records/useAddEdit.js +224 -0
- package/src/client/composables/records/useCrudAddEdit.js +227 -0
- package/src/client/composables/records/useCrudList.js +101 -0
- package/src/client/composables/records/useCrudView.js +47 -0
- package/src/client/composables/records/useList.js +500 -0
- package/src/client/composables/records/useView.js +164 -0
- package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
- package/src/client/composables/runtime/listUiRuntime.js +123 -0
- package/src/client/composables/runtime/modelStateHelpers.js +90 -0
- package/src/client/composables/runtime/operationAdapters.js +34 -0
- package/src/client/composables/runtime/operationUiHelpers.js +125 -0
- package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
- package/src/client/composables/runtime/useAddEditCore.js +112 -0
- package/src/client/composables/runtime/useCommandCore.js +123 -0
- package/src/client/composables/runtime/useEndpointResource.js +199 -0
- package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
- package/src/client/composables/runtime/useListCore.js +112 -0
- package/src/client/composables/runtime/useUiFeedback.js +98 -0
- package/src/client/composables/runtime/useViewCore.js +93 -0
- package/src/client/composables/runtime/viewUiRuntime.js +96 -0
- package/src/client/composables/support/errorMessageHelpers.js +66 -0
- package/src/client/composables/support/listQueryParamSupport.js +473 -0
- package/src/client/composables/support/listSearchSupport.js +70 -0
- package/src/client/composables/support/refValueHelpers.js +19 -0
- package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
- package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
- package/src/client/composables/support/routeTemplateHelpers.js +285 -0
- package/src/client/composables/support/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useCommand.js +118 -0
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudDeleteAction.js +160 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListFilterLookups.js +146 -0
- package/src/client/composables/useCrudListFilters.js +339 -0
- package/src/client/composables/useCrudListParentTitle.js +151 -0
- package/src/client/composables/useCrudListRowActions.js +144 -0
- package/src/client/composables/useCrudListScreen.js +237 -0
- package/src/client/composables/useCrudViewScreen.js +77 -0
- package/src/client/composables/usePagedCollection.js +181 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
- package/src/client/composables/useScopeRuntime.js +135 -0
- package/src/client/filters.js +15 -0
- package/src/client/index.js +24 -0
- package/src/client/lib/httpClient.js +47 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/rowActions.js +47 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/crudListDateFilterSupport.js +91 -0
- package/test/addEditUiRuntime.test.js +110 -0
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudHttpClientSupport.test.js +77 -0
- package/test/crudJsonApiTransportSupport.test.js +179 -0
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListDateFilterSupport.test.js +92 -0
- package/test/crudListDateFilterSurface.browser.test.js +182 -0
- package/test/crudListFilterSurface.test.js +90 -0
- package/test/crudLookupFieldRuntime.test.js +264 -0
- package/test/crudScreenComponents.test.js +110 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +68 -0
- package/test/listQueryParamSupport.test.js +223 -0
- package/test/listUiRuntime.test.js +102 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/packageBoundary.test.js +31 -0
- package/test/permissions.test.js +35 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/requestTransportOptions.test.js +457 -0
- package/test/resourceLoadStateHelpers.test.js +189 -0
- package/test/routeTemplateHelpers.test.js +85 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +190 -0
- package/test/useCrudAddEdit.test.js +415 -0
- package/test/useCrudDeleteAction.test.js +139 -0
- package/test/useCrudListBulkActions.test.js +65 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +293 -0
- package/test/useCrudListParentTitle.test.js +262 -0
- package/test/useCrudListRowActions.test.js +77 -0
- package/test/useListRouteInitialization.test.js +337 -0
- package/test/useListSearchSupport.test.js +61 -0
- package/test/usePagedCollection.test.js +53 -0
- package/test/useViewRequestQueryParams.test.js +20 -0
- package/test/viewCoreLoading.test.js +44 -0
- package/test/viewUiRuntime.test.js +95 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, unref } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
screen: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true
|
|
8
|
+
},
|
|
9
|
+
resourceSingularTitle: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: "Record"
|
|
12
|
+
},
|
|
13
|
+
resourcePluralTitle: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: "Records"
|
|
16
|
+
},
|
|
17
|
+
description: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: ""
|
|
20
|
+
},
|
|
21
|
+
unavailableTitle: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: "Record unavailable"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const view = computed(() => props.screen?.view || {});
|
|
28
|
+
const recordTitle = computed(() => unref(props.screen?.recordTitle) || props.resourceSingularTitle);
|
|
29
|
+
const listLocation = computed(() => unref(props.screen?.listLocation) || null);
|
|
30
|
+
const editLocation = computed(() => unref(props.screen?.editLocation) || null);
|
|
31
|
+
const resolvedDescription = computed(() =>
|
|
32
|
+
String(props.description || `Review this ${props.resourceSingularTitle} record.`).trim()
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<section class="generated-ui-screen generated-ui-screen--operator ui-generator-view-element d-flex flex-column ga-4">
|
|
38
|
+
<header class="ui-generator-view-header">
|
|
39
|
+
<div class="ui-generator-view-header__copy">
|
|
40
|
+
<p class="text-overline text-medium-emphasis mb-1">{{ resourceSingularTitle }}</p>
|
|
41
|
+
<h1 class="ui-generator-view-header__title">{{ recordTitle }}</h1>
|
|
42
|
+
<p class="text-body-2 text-medium-emphasis mb-0">{{ resolvedDescription }}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="ui-generator-view-header__actions">
|
|
45
|
+
<v-btn
|
|
46
|
+
v-if="listLocation"
|
|
47
|
+
color="primary"
|
|
48
|
+
variant="outlined"
|
|
49
|
+
:to="listLocation"
|
|
50
|
+
>
|
|
51
|
+
Back to {{ resourcePluralTitle }}
|
|
52
|
+
</v-btn>
|
|
53
|
+
<slot name="actions" :screen="screen" :view="view" />
|
|
54
|
+
<v-btn
|
|
55
|
+
v-if="editLocation"
|
|
56
|
+
color="primary"
|
|
57
|
+
variant="flat"
|
|
58
|
+
:to="editLocation"
|
|
59
|
+
>
|
|
60
|
+
Edit
|
|
61
|
+
</v-btn>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<v-sheet rounded="lg" border class="ui-generator-view-panel">
|
|
66
|
+
<div v-if="view.loadError || view.isNotFound" class="ui-generator-view-state">
|
|
67
|
+
<h2 class="text-h6 mb-2">{{ unavailableTitle }}</h2>
|
|
68
|
+
<p class="text-body-2 text-medium-emphasis mb-4">
|
|
69
|
+
{{ view.loadError || `This ${resourceSingularTitle} could not be found.` }}
|
|
70
|
+
</p>
|
|
71
|
+
<div class="ui-generator-view-state__actions">
|
|
72
|
+
<v-btn
|
|
73
|
+
v-if="view.loadError"
|
|
74
|
+
color="primary"
|
|
75
|
+
variant="tonal"
|
|
76
|
+
:loading="view.isFetching"
|
|
77
|
+
@click="view.refresh"
|
|
78
|
+
>
|
|
79
|
+
Retry
|
|
80
|
+
</v-btn>
|
|
81
|
+
<v-btn
|
|
82
|
+
v-else-if="listLocation"
|
|
83
|
+
color="primary"
|
|
84
|
+
variant="tonal"
|
|
85
|
+
:to="listLocation"
|
|
86
|
+
>
|
|
87
|
+
Back to {{ resourcePluralTitle }}
|
|
88
|
+
</v-btn>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<template v-else-if="view.isLoading">
|
|
93
|
+
<div class="pa-4">
|
|
94
|
+
<v-skeleton-loader type="text@2, list-item-two-line@5" />
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<template v-else>
|
|
99
|
+
<v-progress-linear v-if="view.isRefetching" indeterminate />
|
|
100
|
+
<div class="pa-4">
|
|
101
|
+
<slot name="before-fields" :view="view" />
|
|
102
|
+
<v-row class="ui-generator-view-fields">
|
|
103
|
+
<slot name="fields" :view="view" />
|
|
104
|
+
</v-row>
|
|
105
|
+
<slot name="after-fields" :view="view" />
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
</v-sheet>
|
|
109
|
+
|
|
110
|
+
<div v-if="$slots['supporting-content']" class="ui-generator-view-supporting-content">
|
|
111
|
+
<slot name="supporting-content" :view="view" />
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.generated-ui-screen {
|
|
118
|
+
--generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
119
|
+
--generated-ui-screen-state-padding: 2.5rem 1.25rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.generated-ui-screen--operator {
|
|
123
|
+
--generated-ui-screen-state-padding: 2rem 1rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ui-generator-view-header {
|
|
127
|
+
align-items: flex-start;
|
|
128
|
+
display: flex;
|
|
129
|
+
gap: 1rem;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ui-generator-view-header__copy {
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ui-generator-view-header__title {
|
|
138
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
139
|
+
font-weight: 650;
|
|
140
|
+
letter-spacing: -0.02em;
|
|
141
|
+
line-height: 1.15;
|
|
142
|
+
margin: 0 0 0.35rem;
|
|
143
|
+
overflow-wrap: anywhere;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ui-generator-view-header__actions {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
gap: 0.5rem;
|
|
150
|
+
justify-content: flex-end;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ui-generator-view-panel {
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ui-generator-view-state {
|
|
158
|
+
margin-inline: auto;
|
|
159
|
+
max-width: 30rem;
|
|
160
|
+
padding: var(--generated-ui-screen-state-padding);
|
|
161
|
+
text-align: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.ui-generator-view-state__actions {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
gap: 0.5rem;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.ui-generator-view-fields :deep(.v-col) {
|
|
172
|
+
min-width: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.ui-generator-view-supporting-content {
|
|
176
|
+
min-width: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@media (max-width: 960px) {
|
|
180
|
+
.ui-generator-view-header {
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.ui-generator-view-header__actions {
|
|
185
|
+
width: 100%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.ui-generator-view-header__actions :deep(.v-btn) {
|
|
189
|
+
min-height: 48px;
|
|
190
|
+
flex: 1 1 10rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.ui-generator-view-state__actions :deep(.v-btn) {
|
|
194
|
+
min-height: 48px;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
4
|
+
|
|
5
|
+
const CRUD_BINDING_MODE_ROUTE = "route";
|
|
6
|
+
const CRUD_BINDING_MODE_MERGE = "merge";
|
|
7
|
+
const CRUD_BINDING_MODE_EXPLICIT = "explicit";
|
|
8
|
+
const CRUD_BINDING_MODE_NONE = "none";
|
|
9
|
+
|
|
10
|
+
function normalizeCrudBindingMode(value = "") {
|
|
11
|
+
const normalizedValue = normalizeText(value).toLowerCase();
|
|
12
|
+
if (normalizedValue === CRUD_BINDING_MODE_MERGE) {
|
|
13
|
+
return CRUD_BINDING_MODE_MERGE;
|
|
14
|
+
}
|
|
15
|
+
if (normalizedValue === CRUD_BINDING_MODE_EXPLICIT) {
|
|
16
|
+
return CRUD_BINDING_MODE_EXPLICIT;
|
|
17
|
+
}
|
|
18
|
+
if (normalizedValue === CRUD_BINDING_MODE_NONE) {
|
|
19
|
+
return CRUD_BINDING_MODE_NONE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return CRUD_BINDING_MODE_ROUTE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeCrudBindingConfig(binding = {}) {
|
|
26
|
+
const source = asPlainObject(unref(binding));
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
mode: normalizeCrudBindingMode(source.mode),
|
|
29
|
+
values: source.values ?? null
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCrudBindingValues(values, context = {}) {
|
|
34
|
+
if (typeof values === "function") {
|
|
35
|
+
return asPlainObject(values(context));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return asPlainObject(unref(values));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveCrudBoundValues({
|
|
42
|
+
binding = {},
|
|
43
|
+
routeValues = {},
|
|
44
|
+
context = {}
|
|
45
|
+
} = {}) {
|
|
46
|
+
const normalizedBinding = normalizeCrudBindingConfig(binding);
|
|
47
|
+
const normalizedRouteValues = asPlainObject(routeValues);
|
|
48
|
+
const explicitValues = resolveCrudBindingValues(normalizedBinding.values, context);
|
|
49
|
+
|
|
50
|
+
if (normalizedBinding.mode === CRUD_BINDING_MODE_NONE) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
if (normalizedBinding.mode === CRUD_BINDING_MODE_EXPLICIT) {
|
|
54
|
+
return explicitValues;
|
|
55
|
+
}
|
|
56
|
+
if (normalizedBinding.mode === CRUD_BINDING_MODE_MERGE) {
|
|
57
|
+
return {
|
|
58
|
+
...normalizedRouteValues,
|
|
59
|
+
...explicitValues
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalizedRouteValues;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
CRUD_BINDING_MODE_ROUTE,
|
|
68
|
+
CRUD_BINDING_MODE_MERGE,
|
|
69
|
+
CRUD_BINDING_MODE_EXPLICIT,
|
|
70
|
+
CRUD_BINDING_MODE_NONE,
|
|
71
|
+
normalizeCrudBindingMode,
|
|
72
|
+
normalizeCrudBindingConfig,
|
|
73
|
+
resolveCrudBindingValues,
|
|
74
|
+
resolveCrudBoundValues
|
|
75
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { getHttpWebClient } from "../../lib/httpClient.js";
|
|
3
|
+
|
|
4
|
+
const CRUD_API_ACCESS_AUTHENTICATED = "authenticated";
|
|
5
|
+
const CRUD_API_ACCESS_PUBLIC = "public";
|
|
6
|
+
const CRUD_API_ACCESS_VALUES = Object.freeze([
|
|
7
|
+
CRUD_API_ACCESS_AUTHENTICATED,
|
|
8
|
+
CRUD_API_ACCESS_PUBLIC
|
|
9
|
+
]);
|
|
10
|
+
const publicCrudClients = new WeakSet();
|
|
11
|
+
|
|
12
|
+
function normalizeCrudApiAccess(value = "") {
|
|
13
|
+
const normalized = normalizeText(value).toLowerCase() || CRUD_API_ACCESS_AUTHENTICATED;
|
|
14
|
+
if (CRUD_API_ACCESS_VALUES.includes(normalized)) {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`CRUD resource apiAccess must be one of: ${CRUD_API_ACCESS_VALUES.join(", ")}. ` +
|
|
20
|
+
`Received: ${String(value || "") || "(empty)"}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveCrudHttpClient(resource = null, { client = null } = {}) {
|
|
25
|
+
const activeClient = client || getHttpWebClient();
|
|
26
|
+
if (!activeClient || typeof activeClient.request !== "function") {
|
|
27
|
+
throw new TypeError("resolveCrudHttpClient requires a client with request().");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (normalizeCrudApiAccess(resource?.apiAccess) !== CRUD_API_ACCESS_PUBLIC) {
|
|
31
|
+
return activeClient;
|
|
32
|
+
}
|
|
33
|
+
if (publicCrudClients.has(activeClient)) {
|
|
34
|
+
return activeClient;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const publicClient = Object.freeze({
|
|
38
|
+
request(url, options = {}) {
|
|
39
|
+
const requestOptions = options && typeof options === "object" && !Array.isArray(options)
|
|
40
|
+
? options
|
|
41
|
+
: {};
|
|
42
|
+
return activeClient.request(url, {
|
|
43
|
+
...requestOptions,
|
|
44
|
+
csrf: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
publicCrudClients.add(publicClient);
|
|
49
|
+
return publicClient;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
CRUD_API_ACCESS_AUTHENTICATED,
|
|
54
|
+
CRUD_API_ACCESS_PUBLIC,
|
|
55
|
+
normalizeCrudApiAccess,
|
|
56
|
+
resolveCrudHttpClient
|
|
57
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeCrudLookupNamespace } from "@jskit-ai/resource-crud-core/shared/crudLookup";
|
|
3
|
+
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveSchemaFieldDefinitions(definition = null) {
|
|
9
|
+
const schema = definition?.schema;
|
|
10
|
+
if (!schema || typeof schema.getFieldDefinitions !== "function") {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const definitions = schema.getFieldDefinitions();
|
|
15
|
+
return isRecord(definitions) ? definitions : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveLookupFieldMap(resource = null) {
|
|
19
|
+
const outputDefinition =
|
|
20
|
+
resource?.operations?.view?.output ||
|
|
21
|
+
resource?.operations?.create?.output ||
|
|
22
|
+
resource?.operations?.patch?.output ||
|
|
23
|
+
null;
|
|
24
|
+
const mapping = {};
|
|
25
|
+
|
|
26
|
+
for (const [fieldKey, fieldDefinition] of Object.entries(resolveSchemaFieldDefinitions(outputDefinition))) {
|
|
27
|
+
const normalizedFieldDefinition = isRecord(fieldDefinition) ? fieldDefinition : {};
|
|
28
|
+
if (!normalizeText(normalizedFieldDefinition.belongsTo)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const relationshipName = normalizeText(normalizedFieldDefinition.as, {
|
|
33
|
+
fallback: fieldKey
|
|
34
|
+
});
|
|
35
|
+
const normalizedFieldKey = normalizeText(fieldKey);
|
|
36
|
+
if (!normalizedFieldKey || !relationshipName) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mapping[relationshipName] = normalizedFieldKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.freeze(mapping);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isJsonApiResourceTransport(transport = null) {
|
|
47
|
+
return normalizeText(transport?.kind).toLowerCase() === "jsonapi-resource";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveCrudJsonApiResourceType(resource = null) {
|
|
51
|
+
return normalizeText(resource?.namespace);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inferCrudLookupJsonApiTransport({ namespace = "", apiPath = "" } = {}) {
|
|
55
|
+
const resourceType =
|
|
56
|
+
normalizeCrudLookupNamespace(namespace) ||
|
|
57
|
+
normalizeCrudLookupNamespace(apiPath);
|
|
58
|
+
if (!resourceType) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
kind: "jsonapi-resource",
|
|
64
|
+
responseType: resourceType,
|
|
65
|
+
responseKind: "collection"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function inferCrudJsonApiTransport(resource = null, { mode = "", operationName = "" } = {}) {
|
|
70
|
+
const resourceType = resolveCrudJsonApiResourceType(resource);
|
|
71
|
+
if (!resourceType) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedMode = normalizeText(mode).toLowerCase();
|
|
76
|
+
const normalizedOperationName = normalizeText(operationName).toLowerCase();
|
|
77
|
+
if (normalizedMode === "list") {
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
kind: "jsonapi-resource",
|
|
80
|
+
responseType: resourceType,
|
|
81
|
+
responseKind: "collection"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalizedMode === "view") {
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
kind: "jsonapi-resource",
|
|
88
|
+
responseType: resourceType,
|
|
89
|
+
responseKind: "record"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (normalizedMode === "delete") {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
kind: "jsonapi-resource",
|
|
96
|
+
responseType: resourceType,
|
|
97
|
+
responseKind: "record"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (normalizedMode === "add-edit") {
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
kind: "jsonapi-resource",
|
|
104
|
+
...(normalizedOperationName ? { requestType: resourceType } : {}),
|
|
105
|
+
responseType: resourceType,
|
|
106
|
+
responseKind: "record"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveCrudJsonApiTransport(transport = null, resource = null, options = {}) {
|
|
114
|
+
if (transport != null) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
"CRUD hooks do not accept explicit transport. Derive JSON:API transport from the shared resource."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const baseTransport = inferCrudJsonApiTransport(resource, options);
|
|
121
|
+
if (!isJsonApiResourceTransport(baseTransport)) {
|
|
122
|
+
return baseTransport;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lookupFieldMap = resolveLookupFieldMap(resource);
|
|
126
|
+
const lookupContainerKey = normalizeText(resource?.contract?.lookup?.containerKey, {
|
|
127
|
+
fallback: "lookups"
|
|
128
|
+
});
|
|
129
|
+
if (Object.keys(lookupFieldMap).length < 1 && !lookupContainerKey) {
|
|
130
|
+
return baseTransport;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
...baseTransport,
|
|
135
|
+
...(Object.keys(lookupFieldMap).length > 0 ? { lookupFieldMap } : {}),
|
|
136
|
+
...(lookupContainerKey ? { lookupContainerKey } : {})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
inferCrudJsonApiTransport,
|
|
142
|
+
inferCrudLookupJsonApiTransport,
|
|
143
|
+
resolveCrudJsonApiTransport,
|
|
144
|
+
resolveLookupFieldMap
|
|
145
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeCrudLookupContainerKey } from "@jskit-ai/resource-crud-core/shared/crudLookup";
|
|
3
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
4
|
+
|
|
5
|
+
const LOOKUP_LABEL_COMPOSITION_CANDIDATES = Object.freeze([
|
|
6
|
+
Object.freeze(["name", "surname"]),
|
|
7
|
+
Object.freeze(["name", "lastName"]),
|
|
8
|
+
Object.freeze(["firstName", "surname"]),
|
|
9
|
+
Object.freeze(["firstName", "lastName"]),
|
|
10
|
+
Object.freeze(["name"]),
|
|
11
|
+
Object.freeze(["firstName"])
|
|
12
|
+
]);
|
|
13
|
+
const DEFAULT_RECORD_TITLE = "-";
|
|
14
|
+
|
|
15
|
+
function hasDisplayValue(value) {
|
|
16
|
+
if (value == null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
return normalizeText(value).length > 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveComposedLabel(source = {}, candidates = LOOKUP_LABEL_COMPOSITION_CANDIDATES) {
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
const parts = [];
|
|
29
|
+
for (const key of candidate) {
|
|
30
|
+
const part = normalizeText(source[key]);
|
|
31
|
+
if (!part) {
|
|
32
|
+
parts.length = 0;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
parts.push(part);
|
|
36
|
+
}
|
|
37
|
+
if (parts.length === candidate.length) {
|
|
38
|
+
return parts.join(" ");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveLookupItemLabel(item = {}, labelKey = "") {
|
|
46
|
+
const source = asPlainObject(item);
|
|
47
|
+
const composedLabel = resolveComposedLabel(source);
|
|
48
|
+
if (composedLabel) {
|
|
49
|
+
return composedLabel;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const normalizedLabelKey = normalizeText(labelKey);
|
|
53
|
+
if (!normalizedLabelKey) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return normalizeText(source[normalizedLabelKey]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveRecordTitle(record = {}, { fallbackKey = "", defaultValue = DEFAULT_RECORD_TITLE } = {}) {
|
|
61
|
+
const source = asPlainObject(record);
|
|
62
|
+
const composedLabel = resolveComposedLabel(source);
|
|
63
|
+
if (composedLabel) {
|
|
64
|
+
return composedLabel;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalizedFallbackKey = normalizeText(fallbackKey);
|
|
68
|
+
if (normalizedFallbackKey) {
|
|
69
|
+
const fallbackValue = normalizeText(source[normalizedFallbackKey]);
|
|
70
|
+
if (fallbackValue) {
|
|
71
|
+
return fallbackValue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedDefaultValue = normalizeText(defaultValue);
|
|
76
|
+
return normalizedDefaultValue || DEFAULT_RECORD_TITLE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveLookupFieldDescriptor(field = {}, relationKind = "", valueKey = "", labelKey = "") {
|
|
80
|
+
if (typeof field === "string") {
|
|
81
|
+
return {
|
|
82
|
+
key: normalizeText(field),
|
|
83
|
+
relation: {
|
|
84
|
+
kind: normalizeText(relationKind).toLowerCase(),
|
|
85
|
+
valueKey: normalizeText(valueKey) || "id",
|
|
86
|
+
labelKey: normalizeText(labelKey),
|
|
87
|
+
containerKey: ""
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sourceField = asPlainObject(field);
|
|
93
|
+
const relation = asPlainObject(sourceField.relation);
|
|
94
|
+
return {
|
|
95
|
+
key: normalizeText(sourceField.key),
|
|
96
|
+
relation: {
|
|
97
|
+
kind: normalizeText(relation.kind).toLowerCase(),
|
|
98
|
+
valueKey: normalizeText(relation.valueKey) || "id",
|
|
99
|
+
labelKey: normalizeText(relation.labelKey),
|
|
100
|
+
containerKey: normalizeText(relation.containerKey)
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveFallbackLookupKey(key = "") {
|
|
106
|
+
const normalizedKey = normalizeText(key);
|
|
107
|
+
if (!normalizedKey.endsWith("Id") || normalizedKey.length <= 2) {
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return normalizedKey.slice(0, -2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind = "", valueKey = "", labelKey = "") {
|
|
115
|
+
const sourceRecord = asPlainObject(record);
|
|
116
|
+
const descriptor = resolveLookupFieldDescriptor(field, relationKind, valueKey, labelKey);
|
|
117
|
+
const key = descriptor.key;
|
|
118
|
+
if (!key) {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (descriptor.relation.kind !== "lookup") {
|
|
123
|
+
return sourceRecord[key];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lookupContainerKey = normalizeCrudLookupContainerKey(descriptor.relation.containerKey, {
|
|
127
|
+
context: `lookup relation "${key}" containerKey`
|
|
128
|
+
});
|
|
129
|
+
const sourceLookups = asPlainObject(sourceRecord[lookupContainerKey]);
|
|
130
|
+
let lookupRecord = asPlainObject(sourceLookups[key]);
|
|
131
|
+
if (Object.keys(lookupRecord).length < 1) {
|
|
132
|
+
lookupRecord = asPlainObject(sourceLookups[resolveFallbackLookupKey(key)]);
|
|
133
|
+
}
|
|
134
|
+
const lookupLabel = resolveLookupItemLabel(lookupRecord, descriptor.relation.labelKey);
|
|
135
|
+
if (lookupLabel) {
|
|
136
|
+
return lookupLabel;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lookupValue = lookupRecord[descriptor.relation.valueKey];
|
|
140
|
+
if (hasDisplayValue(lookupValue)) {
|
|
141
|
+
return lookupValue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return sourceRecord[key];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
resolveLookupItemLabel,
|
|
149
|
+
resolveLookupFieldDisplayValue,
|
|
150
|
+
resolveRecordTitle
|
|
151
|
+
};
|