@ciwergrp/nuxid 1.1.4 → 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
  ```
@@ -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.4",
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.4",
3
+ "version": "1.2.0",
4
4
  "description": "All-in-one essential modules for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",