@ramathibodi/nuxt-commons 4.0.8 → 4.0.10
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/dist/module.json +1 -1
- package/dist/module.mjs +7 -0
- package/dist/runtime/composables/api.d.ts +35 -4
- package/dist/runtime/composables/api.js +22 -6
- package/dist/runtime/composables/lookupListMaster.js +6 -6
- package/dist/runtime/types/graphqlOperation.d.ts +3 -1
- package/package.json +1 -1
- package/templates/.codegen/plugin-schema-object.cjs +13 -5
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -48,6 +48,13 @@ const module$1 = defineNuxtModule({
|
|
|
48
48
|
...runtimeConfig,
|
|
49
49
|
..._options
|
|
50
50
|
};
|
|
51
|
+
const publicConfig = _nuxt.options.runtimeConfig.public;
|
|
52
|
+
if (publicConfig.IDEMPOTENCY_HEADER == null) {
|
|
53
|
+
publicConfig.IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
54
|
+
}
|
|
55
|
+
if (publicConfig.IDEMPOTENCY_ENABLED == null) {
|
|
56
|
+
publicConfig.IDEMPOTENCY_ENABLED = true;
|
|
57
|
+
}
|
|
51
58
|
_nuxt.options.vite.optimizeDeps ||= {};
|
|
52
59
|
_nuxt.options.vite.optimizeDeps.include ||= [];
|
|
53
60
|
_nuxt.options.vite.optimizeDeps.include.push("painterro");
|
|
@@ -21,6 +21,27 @@
|
|
|
21
21
|
* so users can see stale empty / outdated lists for the full TTL window even after
|
|
22
22
|
* creating data themselves.
|
|
23
23
|
*
|
|
24
|
+
* Idempotency: every authenticated POST (REST and GraphQL via useGraphQl()) automatically
|
|
25
|
+
* receives an `Idempotency-Key` HTTP header. The value is computed client-side as
|
|
26
|
+
*
|
|
27
|
+
* SHA-256(`${floor(Date.now()/1000)}|${username}|${stableStringify(body ?? {})}`)
|
|
28
|
+
*
|
|
29
|
+
* in lowercase hex. The recipe is intentionally identical to the backend's server-side
|
|
30
|
+
* fallback in rama-spring-starter's @IdempotentMutation aspect, so a header-present and
|
|
31
|
+
* a header-absent request from the same user with the same body within the same wall-clock
|
|
32
|
+
* second resolve to the same dedup signature.
|
|
33
|
+
*
|
|
34
|
+
* Per-call escape hatch: pass `{ idempotent: false }` in the options argument to skip
|
|
35
|
+
* injection for a single call (e.g. a long-running mutation the caller wants to retry
|
|
36
|
+
* intentionally with the same body). A caller-supplied `Idempotency-Key` in
|
|
37
|
+
* `options.headers` always wins and is never overwritten.
|
|
38
|
+
*
|
|
39
|
+
* Configuration (consumer's `nuxt.config` `runtimeConfig.public`, mirroring the
|
|
40
|
+
* unprefixed `WS_API` / `WS_GRAPHQL` precedent):
|
|
41
|
+
* - IDEMPOTENCY_HEADER (default 'Idempotency-Key') — must match the backend's
|
|
42
|
+
* `rama.idempotency.header-name` setting if the consumer overrides it there.
|
|
43
|
+
* - IDEMPOTENCY_ENABLED (default true) — global kill switch.
|
|
44
|
+
*
|
|
24
45
|
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
25
46
|
*/
|
|
26
47
|
import type { UseFetchOptions } from 'nuxt/app';
|
|
@@ -35,6 +56,16 @@ export type CacheOption = boolean | number | {
|
|
|
35
56
|
} | {
|
|
36
57
|
ttlMs: number;
|
|
37
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Per-call options accepted by useApi alongside Nuxt's UseFetchOptions.
|
|
61
|
+
*
|
|
62
|
+
* `idempotent: false` skips Idempotency-Key header injection for this single call.
|
|
63
|
+
* Default is `true`. See the useApi() docblock for the full idempotency contract.
|
|
64
|
+
*/
|
|
65
|
+
export interface ApiIdempotencyOption {
|
|
66
|
+
idempotent?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type ApiFetchOptions = UseFetchOptions<unknown> & ApiIdempotencyOption;
|
|
38
69
|
export declare function _resetLegacyHeuristicWarning(): void;
|
|
39
70
|
/**
|
|
40
71
|
* Resolve a cache option to seconds. Pure function, exported for tests.
|
|
@@ -60,10 +91,10 @@ export declare function resolveCacheTtlSeconds(cache: CacheOption | undefined |
|
|
|
60
91
|
declare function invalidateCache(prefix?: string): number;
|
|
61
92
|
export declare function useApi(): {
|
|
62
93
|
urlBuilder: (url: string | string[]) => string;
|
|
63
|
-
get: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
64
|
-
getPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
65
|
-
post: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
66
|
-
postPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
94
|
+
get: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
95
|
+
getPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
96
|
+
post: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
97
|
+
postPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
67
98
|
hashKey: (data: any) => Promise<string>;
|
|
68
99
|
invalidate: typeof invalidateCache;
|
|
69
100
|
};
|
|
@@ -7,6 +7,7 @@ import { useRuntimeConfig } from "#imports";
|
|
|
7
7
|
import { useAuthentication } from "../bridges/authentication.js";
|
|
8
8
|
const DEFAULT_TTL_SECONDS_FOR_TRUE = 5 * 60;
|
|
9
9
|
const CACHE_PREFIX = "api-cache-";
|
|
10
|
+
const IDEMPOTENCY_HEADER_DEFAULT = "Idempotency-Key";
|
|
10
11
|
let _legacyHeuristicWarned = false;
|
|
11
12
|
function warnLegacyHeuristic(value, resolvedSeconds) {
|
|
12
13
|
if (_legacyHeuristicWarned) return;
|
|
@@ -77,7 +78,7 @@ export function useApi() {
|
|
|
77
78
|
if (returnUrl.startsWith("http://") || returnUrl.startsWith("https://")) return returnUrl;
|
|
78
79
|
return trimEnd(config?.public.WS_API, "/") + "/" + trimStart(returnUrl, "/");
|
|
79
80
|
}
|
|
80
|
-
function optionBuilder(method, body, params, options = {}) {
|
|
81
|
+
async function optionBuilder(method, body, params, options = {}) {
|
|
81
82
|
const headers = {
|
|
82
83
|
"Content-Type": "application/json",
|
|
83
84
|
Accept: "application/json"
|
|
@@ -85,9 +86,24 @@ export function useApi() {
|
|
|
85
86
|
const auth = useAuthentication();
|
|
86
87
|
const token = auth.keycloak?.token || auth.token;
|
|
87
88
|
if (token) {
|
|
88
|
-
;
|
|
89
89
|
headers["Authorization"] = `Bearer ${token}`;
|
|
90
90
|
}
|
|
91
|
+
const callerHeaders = {
|
|
92
|
+
...options.headers || {}
|
|
93
|
+
};
|
|
94
|
+
const enabledConfig = config?.public?.IDEMPOTENCY_ENABLED;
|
|
95
|
+
const idempotencyEnabled = enabledConfig !== false;
|
|
96
|
+
const headerName = config?.public?.IDEMPOTENCY_HEADER || IDEMPOTENCY_HEADER_DEFAULT;
|
|
97
|
+
const callerProvidedHeader = Object.keys(callerHeaders).some(
|
|
98
|
+
(k) => k.toLowerCase() === headerName.toLowerCase()
|
|
99
|
+
);
|
|
100
|
+
if (method === "POST" && idempotencyEnabled && options.idempotent !== false && !callerProvidedHeader) {
|
|
101
|
+
const username = (typeof auth.getUsername === "function" ? auth.getUsername() : auth.userProfile?.username) ?? "";
|
|
102
|
+
const second = Math.floor(Date.now() / 1e3).toString();
|
|
103
|
+
const bodyJson = stableStringify(body ?? {});
|
|
104
|
+
headers[headerName] = await sha256(`${second}|${username}|${bodyJson}`);
|
|
105
|
+
}
|
|
106
|
+
const { idempotent: _omitIdempotent, ...passThroughOptions } = options;
|
|
91
107
|
const baseOptions = {
|
|
92
108
|
method,
|
|
93
109
|
body,
|
|
@@ -96,9 +112,9 @@ export function useApi() {
|
|
|
96
112
|
};
|
|
97
113
|
const finalHeaders = {
|
|
98
114
|
...headers,
|
|
99
|
-
...
|
|
115
|
+
...callerHeaders
|
|
100
116
|
};
|
|
101
|
-
Object.assign(baseOptions,
|
|
117
|
+
Object.assign(baseOptions, passThroughOptions);
|
|
102
118
|
return {
|
|
103
119
|
...baseOptions,
|
|
104
120
|
headers: finalHeaders
|
|
@@ -132,7 +148,7 @@ export function useApi() {
|
|
|
132
148
|
const builtUrl = urlBuilder(url);
|
|
133
149
|
const ttl = resolveCacheTtlSeconds(cache);
|
|
134
150
|
if (ttl === 0) {
|
|
135
|
-
return ofetch(builtUrl, optionBuilder(method, body, params, options));
|
|
151
|
+
return ofetch(builtUrl, await optionBuilder(method, body, params, options));
|
|
136
152
|
}
|
|
137
153
|
const keyData = { url: builtUrl, method, body, params, headers: options?.headers };
|
|
138
154
|
const key = CACHE_PREFIX + await hashKey(keyData);
|
|
@@ -140,7 +156,7 @@ export function useApi() {
|
|
|
140
156
|
if (cached !== null) {
|
|
141
157
|
return cached;
|
|
142
158
|
}
|
|
143
|
-
const result = await ofetch(builtUrl, optionBuilder(method, body, params, options));
|
|
159
|
+
const result = await ofetch(builtUrl, await optionBuilder(method, body, params, options));
|
|
144
160
|
ls.set(key, result, { ttl });
|
|
145
161
|
return result;
|
|
146
162
|
}
|
|
@@ -31,15 +31,15 @@ export function useLookupListMaster(props) {
|
|
|
31
31
|
);
|
|
32
32
|
});
|
|
33
33
|
const formatItemTitle = (item) => {
|
|
34
|
+
const raw = item?.raw ?? item ?? {};
|
|
35
|
+
const resolvedTitle = item?.title || raw?.[itemTitleField.value] || raw?.itemValue || raw?.itemCode;
|
|
34
36
|
if (props.meilisearch) {
|
|
35
|
-
const
|
|
36
|
-
const code = raw?.itemCode;
|
|
37
|
-
const title =
|
|
37
|
+
const formatted = raw?._formatted ?? {};
|
|
38
|
+
const code = formatted?.itemCode ?? raw?.itemCode;
|
|
39
|
+
const title = formatted?.[itemTitleField.value] || formatted?.itemValue || formatted?.itemCode || resolvedTitle;
|
|
38
40
|
return (props.showCode ? (code ?? "") + "-" : "") + (title ?? "");
|
|
39
41
|
} else {
|
|
40
|
-
|
|
41
|
-
const title = item.title ?? item?.itemValue ?? item?.itemCode;
|
|
42
|
-
return (props.showCode ? (code ?? "") + "-" : "") + (title ?? "");
|
|
42
|
+
return (props.showCode ? (raw?.itemCode ?? "") + "-" : "") + (resolvedTitle ?? "");
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
const computedNoDataText = computed(() => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface graphqlVariable {
|
|
2
2
|
name: string
|
|
3
|
-
|
|
3
|
+
// `true` for `[T]`, `[true]` for `[T!]` — matches gql-query-builder's
|
|
4
|
+
// VariableOptions shape so element non-null survives into the query string.
|
|
5
|
+
list?: boolean | [boolean]
|
|
4
6
|
required?: boolean
|
|
5
7
|
type?: string
|
|
6
8
|
value?: any
|
package/package.json
CHANGED
|
@@ -6,14 +6,21 @@ function capitalizeFirstLetter(string) {
|
|
|
6
6
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// `inList` flips to true the moment we descend into a ListType, so a NonNullType
|
|
10
|
+
// nested under a List is recognized as element-non-null (`[T!]`) rather than
|
|
11
|
+
// overwriting the outer list's `required` flag. The element-non-null is encoded
|
|
12
|
+
// as `list: [true]` — the shape gql-query-builder reads to emit `[T!]`.
|
|
13
|
+
function inputTypeToObject(inputType, result, inList = false) {
|
|
10
14
|
if (inputType.kind === 'NonNullType') {
|
|
11
|
-
result['
|
|
12
|
-
|
|
15
|
+
if (inList) result['list'] = [true]
|
|
16
|
+
else result['required'] = true
|
|
17
|
+
inputTypeToObject(inputType.type, result, inList)
|
|
18
|
+
return
|
|
13
19
|
}
|
|
14
20
|
if (inputType.kind === 'ListType') {
|
|
15
|
-
result['list'] = true
|
|
16
|
-
inputTypeToObject(inputType.type, result)
|
|
21
|
+
if (!Array.isArray(result['list'])) result['list'] = true
|
|
22
|
+
inputTypeToObject(inputType.type, result, true)
|
|
23
|
+
return
|
|
17
24
|
}
|
|
18
25
|
if (inputType.kind === 'NamedType') {
|
|
19
26
|
result['type'] = inputType.name.value
|
|
@@ -21,6 +28,7 @@ function inputTypeToObject(inputType, result) {
|
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
module.exports = {
|
|
31
|
+
inputTypeToObject,
|
|
24
32
|
plugin(schema, _documents, _config) {
|
|
25
33
|
const astNode = getCachedDocumentNodeFromSchema(schema) // Transforms the GraphQLSchema into ASTNode
|
|
26
34
|
|