@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 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, a form composable, helper utilities, optional icon defaults, Element Plus setup, and Pinia integration. Enable the pieces you need and keep the rest off.
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
  ```
@@ -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: NitroFetchOptions<NitroFetchRequest> = {}) {
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: NitroFetchOptions<NitroFetchRequest> = {}) {
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: UseFetchOptions<T> = {}) {
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: UseFetchOptions<T> = {}) {
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: NitroFetchOptions<NitroFetchRequest> = {}) {
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: NitroFetchOptions<NitroFetchRequest> = {}) {
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: NitroFetchOptions<NitroFetchRequest> = {}) {
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}/index`;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ciwergrp/nuxid",
3
3
  "configKey": "nuxid",
4
- "version": "1.1.3",
4
+ "version": "1.2.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
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
@@ -9,3 +9,5 @@ export * from '../dist/runtime/form.js'
9
9
  export * from '../dist/runtime/validator.js'
10
10
 
11
11
  export * from '../dist/runtime/helper/index.js'
12
+
13
+ export * from '../dist/runtime/fetcher/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciwergrp/nuxid",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "All-in-one essential modules for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",