@ciwergrp/nuxid 1.1.3 → 1.2.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/README.md +20 -1
- package/console/resource.mjs +8 -8
- package/dist/module.d.mts +11 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +30 -0
- package/dist/runtime/fetcher/cursor.d.ts +31 -0
- package/dist/runtime/fetcher/cursor.js +178 -0
- package/dist/runtime/fetcher/index.d.ts +1 -0
- package/dist/runtime/fetcher/index.js +1 -0
- package/dist/types.d.mts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nuxid – Nuxt Essentials
|
|
2
2
|
|
|
3
|
-
Nuxid bundles a set of productivity helpers for Nuxt projects: lodash auto-imports, Element Plus-friendly validation rules,
|
|
3
|
+
Nuxid bundles a set of productivity helpers for Nuxt projects: lodash auto-imports, Element Plus-friendly validation rules, form and cursor fetch composables, helper utilities, optional icon defaults, Element Plus setup, and Pinia integration. Enable the pieces you need and keep the rest off.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -32,6 +32,7 @@ export default defineNuxtConfig({
|
|
|
32
32
|
- **Validator helpers** (enabled by default): Element Plus friendly `createValidationRules` plus `ValidationRule` and `ValidationOptions` types. Auto-imported when enabled.
|
|
33
33
|
- **Form composable** (enabled by default): `useHttp` wraps `$fetch`, handles `processing`, `errors`, `response`, and builds `FormData` automatically (or always when `alwaysFormData: true`). Auto-imported when enabled.
|
|
34
34
|
- **Helper utilities** (enabled by default): array/object/number/string helpers with configurable factory (`number().abbreviate()`) or prefixed (`NumberAbbreviate()`) styles, locale lookups, and currency defaults.
|
|
35
|
+
- **Cursor fetch composable** (enabled by default): `useCursorFetch` wraps `$fetch`, supports cursor pagination, polling, reactive params, custom fetchers, and configurable cursor/meta keys. Auto-imported when enabled.
|
|
35
36
|
- **Pinia integration** (enabled by default): injects Pinia, auto-imports core helpers (`defineStore`, `storeToRefs`, etc.), and auto-imports stores from `stores` by default.
|
|
36
37
|
- **Icon defaults** (disabled by default): installs `@nuxt/icon` with default component name `KIcon`, size `1.25em`, base class `align-middle inline-block text-current`, mode `svg`. Configure via `nuxid.icon.config`.
|
|
37
38
|
- **Element Plus module** (disabled by default): installs `@element-plus/nuxt` with your provided config.
|
|
@@ -75,6 +76,21 @@ const price = number().currency(4200)
|
|
|
75
76
|
const slug = string().slug('Hello World')
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
**Cursor fetch example**
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const { data, loadMore, refresh } = useCursorFetch('/api/messages', {
|
|
83
|
+
fetchOptions: { query: { perPage: 20 } },
|
|
84
|
+
pollInterval: 5000,
|
|
85
|
+
itemKey: 'code',
|
|
86
|
+
cursorParam: 'after',
|
|
87
|
+
meta: {
|
|
88
|
+
cursorKey: 'next_cursor',
|
|
89
|
+
hasMoreKey: 'has_next_page',
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
78
94
|
**Pinia example**
|
|
79
95
|
|
|
80
96
|
```ts
|
|
@@ -135,6 +151,9 @@ export default defineNuxtConfig({
|
|
|
135
151
|
enabled: true,
|
|
136
152
|
storesDirs: ['stores'],
|
|
137
153
|
},
|
|
154
|
+
fetcher: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
},
|
|
138
157
|
},
|
|
139
158
|
})
|
|
140
159
|
```
|
package/console/resource.mjs
CHANGED
|
@@ -225,31 +225,31 @@ function getCrudTemplate(moduleName, caseStyle) {
|
|
|
225
225
|
import type { UseFetchOptions } from '#app';
|
|
226
226
|
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
|
|
227
227
|
|
|
228
|
-
export async function index(options
|
|
228
|
+
export async function index(options?: NitroFetchOptions<NitroFetchRequest>) {
|
|
229
229
|
return $fetch('/api/${moduleName}', options);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export async function show(id: string, options
|
|
232
|
+
export async function show(id: string, options?: NitroFetchOptions<NitroFetchRequest>) {
|
|
233
233
|
return $fetch(\`/api/${moduleName}/\${id}\`, options);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
export function ${useIndexName}<T = any>(options
|
|
236
|
+
export function ${useIndexName}<T = any>(options?: UseFetchOptions<T>) {
|
|
237
237
|
return useFetch<T>('/api/${moduleName}', options);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
export function ${useShowName}<T = any>(id: string, options
|
|
240
|
+
export function ${useShowName}<T = any>(id: string, options?: UseFetchOptions<T>) {
|
|
241
241
|
return useFetch<T>(\`/api/${moduleName}/\${id}\`, options);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
export async function store(form: UseHttp, options
|
|
244
|
+
export async function store(form: UseHttp, options?: NitroFetchOptions<NitroFetchRequest>) {
|
|
245
245
|
return form.post('/api/${moduleName}', options);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
export async function update(form: UseHttp, id: string, options
|
|
248
|
+
export async function update(form: UseHttp, id: string, options?: NitroFetchOptions<NitroFetchRequest>) {
|
|
249
249
|
return form.patch(\`/api/${moduleName}/\${id}\`, options);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
export async function destroy(id: string, options
|
|
252
|
+
export async function destroy(id: string, options?: NitroFetchOptions<NitroFetchRequest>) {
|
|
253
253
|
return $fetch(\`/api/${moduleName}/\${id}\`, { method: 'DELETE', ...options });
|
|
254
254
|
}
|
|
255
255
|
`;
|
|
@@ -283,7 +283,7 @@ function reindexApiModules(baseDir, caseStyle) {
|
|
|
283
283
|
const folderName = path.basename(dir);
|
|
284
284
|
const exportName = toCase(folderName, caseStyle);
|
|
285
285
|
const safeAlias = isValidIdentifier(exportName) ? exportName : toSafeIdentifier(folderName);
|
|
286
|
-
const modulePath = `./modules/${folderName}
|
|
286
|
+
const modulePath = `./modules/${folderName}`;
|
|
287
287
|
|
|
288
288
|
if (safeAlias) {
|
|
289
289
|
imports.push(`import * as ${safeAlias} from '${modulePath}';`);
|
package/dist/module.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ export * from '../dist/runtime/form.js';
|
|
|
3
3
|
export { default as useHttp } from '../dist/runtime/form.js';
|
|
4
4
|
export * from '../dist/runtime/validator.js';
|
|
5
5
|
export * from '../dist/runtime/helper/index.js';
|
|
6
|
+
export * from '../dist/runtime/fetcher/index.js';
|
|
6
7
|
|
|
7
8
|
interface LodashOptions {
|
|
8
9
|
/**
|
|
@@ -196,6 +197,15 @@ interface VueUseOptions {
|
|
|
196
197
|
}
|
|
197
198
|
type VueUseFeatureInput = boolean | Partial<VueUseOptions> | undefined;
|
|
198
199
|
|
|
200
|
+
interface FetcherOptions {
|
|
201
|
+
/**
|
|
202
|
+
* Enable cursor fetch composable auto-import
|
|
203
|
+
* @default true
|
|
204
|
+
*/
|
|
205
|
+
enabled: boolean;
|
|
206
|
+
}
|
|
207
|
+
type FetcherFeatureInput = boolean | Partial<FetcherOptions> | undefined;
|
|
208
|
+
|
|
199
209
|
interface ModuleOptions {
|
|
200
210
|
lodash?: LodashFeatureInput;
|
|
201
211
|
validator?: ValidatorFeatureInput;
|
|
@@ -205,6 +215,7 @@ interface ModuleOptions {
|
|
|
205
215
|
helper?: HelperFeatureInput;
|
|
206
216
|
pinia?: PiniaFeatureInput;
|
|
207
217
|
vueuse?: VueUseFeatureInput;
|
|
218
|
+
fetcher?: FetcherFeatureInput;
|
|
208
219
|
}
|
|
209
220
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
210
221
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -5,6 +5,7 @@ export * from '../dist/runtime/form.js';
|
|
|
5
5
|
export { default as useHttp } from '../dist/runtime/form.js';
|
|
6
6
|
export * from '../dist/runtime/validator.js';
|
|
7
7
|
export * from '../dist/runtime/helper/index.js';
|
|
8
|
+
export * from '../dist/runtime/fetcher/index.js';
|
|
8
9
|
|
|
9
10
|
const lodashExcludes = [
|
|
10
11
|
"wrapperValue",
|
|
@@ -447,6 +448,29 @@ function registerVueUseFeature(options, nuxt) {
|
|
|
447
448
|
});
|
|
448
449
|
}
|
|
449
450
|
|
|
451
|
+
const fetcherDefaults = {
|
|
452
|
+
enabled: false
|
|
453
|
+
};
|
|
454
|
+
function resolveFetcherOptions(input) {
|
|
455
|
+
if (input === false) {
|
|
456
|
+
return { ...fetcherDefaults, enabled: false };
|
|
457
|
+
}
|
|
458
|
+
const overrides = typeof input === "boolean" || input === void 0 ? {} : input;
|
|
459
|
+
return {
|
|
460
|
+
...fetcherDefaults,
|
|
461
|
+
...overrides,
|
|
462
|
+
enabled: overrides?.enabled ?? true
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function registerFetcherFeature(options, { from }) {
|
|
466
|
+
if (!options.enabled) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
addImports({ name: "useCursorFetch", as: "useCursorFetch", from });
|
|
470
|
+
addImports({ name: "APIResponseCursor", as: "APIResponseCursor", from, type: true });
|
|
471
|
+
addImports({ name: "CursorFetchOptions", as: "CursorFetchOptions", from, type: true });
|
|
472
|
+
}
|
|
473
|
+
|
|
450
474
|
const module$1 = defineNuxtModule({
|
|
451
475
|
meta: {
|
|
452
476
|
name: "@ciwergrp/nuxid",
|
|
@@ -470,6 +494,7 @@ const module$1 = defineNuxtModule({
|
|
|
470
494
|
const helperOptions = resolveHelperOptions(options.helper);
|
|
471
495
|
const piniaOptions = resolvePiniaOptions(options.pinia);
|
|
472
496
|
const vueuseOptions = resolveVueUseOptions(options.vueuse);
|
|
497
|
+
const fetcherOptions = resolveFetcherOptions(options.fetcher);
|
|
473
498
|
if (iconOptions.enabled) {
|
|
474
499
|
await installModule("@nuxt/icon", iconOptions.config);
|
|
475
500
|
}
|
|
@@ -494,6 +519,11 @@ const module$1 = defineNuxtModule({
|
|
|
494
519
|
from: resolver.resolve("./runtime/helper")
|
|
495
520
|
});
|
|
496
521
|
}
|
|
522
|
+
if (fetcherOptions.enabled) {
|
|
523
|
+
registerFetcherFeature(fetcherOptions, {
|
|
524
|
+
from: resolver.resolve("./runtime/fetcher")
|
|
525
|
+
});
|
|
526
|
+
}
|
|
497
527
|
if (piniaOptions.enabled) {
|
|
498
528
|
const piniaRuntimeDir = fileURLToPath(new URL("./runtime/pinia", import.meta.url));
|
|
499
529
|
registerPiniaFeature(piniaOptions, nuxt, {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
|
|
2
|
+
export interface APIResponseCursor<T> {
|
|
3
|
+
data: T[];
|
|
4
|
+
meta: {
|
|
5
|
+
afterCursor?: string | null;
|
|
6
|
+
hasMore?: boolean;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface CursorMetaConfig {
|
|
11
|
+
cursorKey?: string;
|
|
12
|
+
hasMoreKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CursorFetchOptions<TRequest extends NitroFetchRequest = NitroFetchRequest> {
|
|
15
|
+
lazy?: boolean;
|
|
16
|
+
pollInterval?: number;
|
|
17
|
+
fetcher?: typeof $fetch;
|
|
18
|
+
fetchOptions?: NitroFetchOptions<TRequest>;
|
|
19
|
+
meta?: CursorMetaConfig;
|
|
20
|
+
itemKey?: string;
|
|
21
|
+
cursorParam?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function useCursorFetch<T extends Record<string, string>, TResponse extends APIResponseCursor<T> = APIResponseCursor<T>, TRequest extends NitroFetchRequest = NitroFetchRequest>(url: TRequest, options?: CursorFetchOptions<TRequest>): {
|
|
24
|
+
data: import("vue").Ref<TResponse | undefined, TResponse | undefined>;
|
|
25
|
+
loading: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
26
|
+
error: Readonly<import("vue").Ref<Error | null, Error | null>>;
|
|
27
|
+
hasNextPage: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
28
|
+
isLoadMoreTriggered: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
29
|
+
loadMore: () => Promise<void>;
|
|
30
|
+
refresh: () => Promise<void>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { isRef, onUnmounted, readonly, ref, shallowRef, watch } from "vue";
|
|
2
|
+
export function useCursorFetch(url, options) {
|
|
3
|
+
function findReactiveSources(obj) {
|
|
4
|
+
const sources = [];
|
|
5
|
+
if (!obj || typeof obj !== "object") {
|
|
6
|
+
return sources;
|
|
7
|
+
}
|
|
8
|
+
for (const key in obj) {
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
10
|
+
const value = obj[key];
|
|
11
|
+
if (isRef(value)) {
|
|
12
|
+
sources.push(value);
|
|
13
|
+
} else if (typeof value === "object") {
|
|
14
|
+
sources.push(...findReactiveSources(value));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return sources;
|
|
19
|
+
}
|
|
20
|
+
function unwrapReactiveObject(obj) {
|
|
21
|
+
if (!obj || typeof obj !== "object") {
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
const unwrapped = Array.isArray(obj) ? [] : {};
|
|
25
|
+
for (const key in obj) {
|
|
26
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
27
|
+
const value = obj[key];
|
|
28
|
+
if (isRef(value)) {
|
|
29
|
+
unwrapped[key] = value.value;
|
|
30
|
+
} else if (typeof value === "object") {
|
|
31
|
+
unwrapped[key] = unwrapReactiveObject(value);
|
|
32
|
+
} else {
|
|
33
|
+
unwrapped[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return unwrapped;
|
|
38
|
+
}
|
|
39
|
+
const {
|
|
40
|
+
lazy = false,
|
|
41
|
+
pollInterval,
|
|
42
|
+
fetcher,
|
|
43
|
+
fetchOptions,
|
|
44
|
+
meta,
|
|
45
|
+
itemKey = "id",
|
|
46
|
+
cursorParam = "cursor"
|
|
47
|
+
} = options ?? {};
|
|
48
|
+
const initialUrl = url;
|
|
49
|
+
const data = ref();
|
|
50
|
+
const loading = ref(false);
|
|
51
|
+
const error = ref(null);
|
|
52
|
+
const nextCursor = ref(null);
|
|
53
|
+
const hasNextPage = ref(true);
|
|
54
|
+
const isLoadMoreTriggered = ref(false);
|
|
55
|
+
const currentParams = shallowRef(
|
|
56
|
+
unwrapReactiveObject(fetchOptions)
|
|
57
|
+
);
|
|
58
|
+
let pollTimer = null;
|
|
59
|
+
const fetcherFn = fetcher ?? globalThis.$fetch;
|
|
60
|
+
if (!fetcherFn) {
|
|
61
|
+
throw new Error("Nuxt $fetch is not available in the current context");
|
|
62
|
+
}
|
|
63
|
+
const normalizeKey = (value, fallback) => {
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
69
|
+
};
|
|
70
|
+
const cursorKey = normalizeKey(meta?.cursorKey, "afterCursor");
|
|
71
|
+
const hasMoreKey = normalizeKey(meta?.hasMoreKey, "hasMore");
|
|
72
|
+
const resolvedItemKey = normalizeKey(itemKey, "id");
|
|
73
|
+
const resolvedCursorParam = normalizeKey(cursorParam, "cursor");
|
|
74
|
+
const normalizeCursorValue = (value) => {
|
|
75
|
+
if (value === null || value === void 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
81
|
+
}
|
|
82
|
+
return String(value);
|
|
83
|
+
};
|
|
84
|
+
const fetchData = async (fetchUrl, params) => {
|
|
85
|
+
if (loading.value) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!hasNextPage.value && data.value) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
loading.value = true;
|
|
92
|
+
error.value = null;
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetcherFn(fetchUrl, params);
|
|
95
|
+
data.value = {
|
|
96
|
+
...response,
|
|
97
|
+
data: [...data.value?.data ?? [], ...response.data]
|
|
98
|
+
};
|
|
99
|
+
nextCursor.value = normalizeCursorValue(response.meta?.[cursorKey]);
|
|
100
|
+
hasNextPage.value = response.meta?.[hasMoreKey] ?? false;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
error.value = e;
|
|
103
|
+
} finally {
|
|
104
|
+
loading.value = false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const pollData = async () => {
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetcherFn(initialUrl, currentParams.value);
|
|
110
|
+
if (data.value) {
|
|
111
|
+
const existingIds = new Set(
|
|
112
|
+
data.value.data.map((item) => item[resolvedItemKey])
|
|
113
|
+
);
|
|
114
|
+
const newItems = response.data.filter(
|
|
115
|
+
(item) => !existingIds.has(item[resolvedItemKey])
|
|
116
|
+
);
|
|
117
|
+
if (newItems.length > 0) {
|
|
118
|
+
data.value.data.unshift(...newItems);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
data.value = response;
|
|
122
|
+
nextCursor.value = normalizeCursorValue(response.meta?.[cursorKey]);
|
|
123
|
+
hasNextPage.value = response.meta?.[hasMoreKey] ?? false;
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error("Polling error:", e);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const loadMore = async () => {
|
|
130
|
+
if (!hasNextPage.value) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
isLoadMoreTriggered.value = true;
|
|
134
|
+
const params = nextCursor.value ? {
|
|
135
|
+
...currentParams.value,
|
|
136
|
+
query: { ...currentParams.value?.query, [resolvedCursorParam]: nextCursor.value }
|
|
137
|
+
} : currentParams.value;
|
|
138
|
+
await fetchData(initialUrl, params);
|
|
139
|
+
};
|
|
140
|
+
const refresh = async () => {
|
|
141
|
+
data.value = void 0;
|
|
142
|
+
nextCursor.value = null;
|
|
143
|
+
hasNextPage.value = true;
|
|
144
|
+
isLoadMoreTriggered.value = false;
|
|
145
|
+
await fetchData(initialUrl, currentParams.value);
|
|
146
|
+
};
|
|
147
|
+
const reactiveSources = findReactiveSources(fetchOptions);
|
|
148
|
+
if (reactiveSources.length > 0) {
|
|
149
|
+
watch(
|
|
150
|
+
reactiveSources,
|
|
151
|
+
() => {
|
|
152
|
+
currentParams.value = unwrapReactiveObject(fetchOptions);
|
|
153
|
+
refresh();
|
|
154
|
+
},
|
|
155
|
+
{ flush: "sync" }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (pollInterval) {
|
|
159
|
+
pollTimer = setInterval(pollData, pollInterval);
|
|
160
|
+
}
|
|
161
|
+
onUnmounted(() => {
|
|
162
|
+
if (pollTimer) {
|
|
163
|
+
clearInterval(pollTimer);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (!lazy) {
|
|
167
|
+
void fetchData(initialUrl, currentParams.value);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
data,
|
|
171
|
+
loading: readonly(loading),
|
|
172
|
+
error: readonly(error),
|
|
173
|
+
hasNextPage: readonly(hasNextPage),
|
|
174
|
+
isLoadMoreTriggered: readonly(isLoadMoreTriggered),
|
|
175
|
+
loadMore,
|
|
176
|
+
refresh
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cursor.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./cursor.js";
|
package/dist/types.d.mts
CHANGED