use-l5 0.0.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/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # use-l5
2
+
3
+ Nuxt-модуль с типизированным `useL5`-композаблом для парсинга фильтров, синхронизации с query в роуте и сборки параметров для API/URL.
4
+
5
+ ## Возможности
6
+
7
+ - типизированная схема фильтров на основе конструкторов `String/Number/Boolean`
8
+ - двусторонняя синхронизация с `route.query` (опционально)
9
+ - генерация параметров для API и URL
10
+ - базовые параметры пагинации и сортировки
11
+
12
+ ## Установка
13
+
14
+ ```bash
15
+ npx nuxi module add use-l5
16
+ ```
17
+
18
+ или
19
+
20
+ ```bash
21
+ pnpm add use-l5
22
+ ```
23
+
24
+ ## Настройка модуля
25
+
26
+ `nuxt.config.ts`:
27
+
28
+ ```ts
29
+ export default defineNuxtConfig({
30
+ modules: ['use-l5'],
31
+ useL5: {
32
+ syncWithRoute: true,
33
+ urlUpdateStrategy: 'replace',
34
+ boolToNumber: true
35
+ }
36
+ })
37
+ ```
38
+
39
+ ## Базовое использование
40
+
41
+ ```ts
42
+ import { useL5 } from '#imports'
43
+
44
+ const schema = {
45
+ q: String,
46
+ category: [String],
47
+ inStock: Boolean,
48
+ price: Number
49
+ }
50
+
51
+ const { filters, queryForApi, updateFilters, updateDefaults } = useL5(schema, {
52
+ defaults: {
53
+ category: [],
54
+ inStock: false
55
+ },
56
+ syncWithRoute: true,
57
+ urlUpdateStrategy: 'push'
58
+ })
59
+
60
+ updateFilters({ q: 'mac', category: ['laptops'] })
61
+ ```
62
+
63
+ ## Параметры `useL5`
64
+
65
+ ```ts
66
+ interface Options<S> {
67
+ defaults?: Partial<InferFromL5Schema<S>>
68
+ syncWithRoute?: boolean
69
+ excludeFromSearch?: (keyof S)[]
70
+ apiIncludes?: string[]
71
+ excludeFromQueryBuilder?: (keyof S)[]
72
+ boolToNumber?: boolean
73
+ queryAliases?: Partial<Record<keyof S, string>>
74
+ transformInput?: (query: Partial<S>) => Partial<S>
75
+ transformOutput?: (filters: Filters<S>) => Record<keyof S & keyof BaseParams, unknown>
76
+ urlUpdateStrategy?: 'replace' | 'push'
77
+ }
78
+ ```
79
+
80
+ ## Базовые параметры (BaseParams)
81
+
82
+ Всегда доступны и участвуют в сборке запросов:
83
+
84
+ - `page` (по умолчанию 1)
85
+ - `limit` (по умолчанию 10)
86
+ - `sortedBy` (по умолчанию `id`)
87
+ - `orderBy` (по умолчанию `desc`)
88
+ - `searchJoin` (по умолчанию `and`)
89
+ - `searchFields` (по умолчанию `null`)
90
+ - `search` (по умолчанию `null`)
91
+
92
+ ## Утилиты
93
+
94
+ Модуль автоматически добавляет импорты:
95
+
96
+ ```ts
97
+ import { buildQueryForApi, parseFiltersFromQuery } from '#imports'
98
+ ```
99
+
100
+ - `buildQueryForApi(filters, options)` — сборка параметров для API
101
+ - `parseFiltersFromQuery(schema, query, { defaults })` — парсинг query в фильтры
102
+
103
+ ## Возврат `useL5`
104
+
105
+ ```ts
106
+ interface UseL5Return<S> {
107
+ filters: Ref<Filters<S>>
108
+ queryForApi: ShallowRef<Record<string, unknown>>
109
+ updateFilters: (newFilters: Partial<Filters<S>>, options?: { urlUpdateStrategy?: 'replace' | 'push' }) => void
110
+ updateDefaults: (newDefaults: Partial<InferFromL5Schema<S>>) => void
111
+ defaultsRef: Ref<Partial<InferFromL5Schema<S>>>
112
+ }
113
+ ```
114
+
115
+ ## Лицензия
116
+
117
+ MIT
@@ -0,0 +1,12 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ export { BaseParams, Filters, InferFromL5Schema, InferL5, L5Node, Options, Primitive, SchemaDefinition } from '../dist/runtime/types/index.js';
3
+
4
+ interface ModuleOptions {
5
+ syncWithRoute?: boolean;
6
+ urlUpdateStrategy?: 'replace' | 'push';
7
+ boolToNumber?: boolean;
8
+ }
9
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
10
+
11
+ export { _default as default };
12
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "use-l5",
3
+ "configKey": "useL5",
4
+ "compatibility": {
5
+ "nuxt": ">=3.16"
6
+ },
7
+ "version": "0.0.1",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.2",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,39 @@
1
+ import { defineNuxtModule, createResolver, addImportsDir, addImports } from '@nuxt/kit';
2
+
3
+ const module$1 = defineNuxtModule({
4
+ meta: {
5
+ name: "use-l5",
6
+ configKey: "useL5",
7
+ compatibility: {
8
+ nuxt: ">=3.16"
9
+ }
10
+ },
11
+ // Default configuration options of the Nuxt module
12
+ defaults: {},
13
+ setup(_options, _nuxt) {
14
+ const publicConfig = _nuxt.options.runtimeConfig.public ?? {};
15
+ const existing = publicConfig.useL5 ?? {};
16
+ const mergedOptions = {
17
+ ...existing,
18
+ ..._options
19
+ };
20
+ _nuxt.options.runtimeConfig.public = {
21
+ ...publicConfig,
22
+ useL5: mergedOptions
23
+ };
24
+ const resolver = createResolver(import.meta.url);
25
+ addImportsDir(resolver.resolve("./runtime/composables"));
26
+ addImports({
27
+ name: "buildQueryForApi",
28
+ as: "buildQueryForApi",
29
+ from: resolver.resolve("./runtime/utils/buildQueryForApi")
30
+ });
31
+ addImports({
32
+ name: "parseFiltersFromQuery",
33
+ as: "parseFiltersFromQuery",
34
+ from: resolver.resolve("./runtime/utils/parseFiltersFromQuery")
35
+ });
36
+ }
37
+ });
38
+
39
+ export { module$1 as default };
@@ -0,0 +1,10 @@
1
+ import type { Ref, ShallowRef } from 'vue';
2
+ import type { Filters, InferFromL5Schema, Options, SchemaDefinition } from '../types/index.js';
3
+ export interface UseL5Return<S extends SchemaDefinition> {
4
+ filters: Ref<Filters<S>>;
5
+ queryForApi: ShallowRef<Record<string, unknown>>;
6
+ updateFilters: (newFilters: Partial<Filters<S>>, _options?: Partial<Pick<Options<S>, 'urlUpdateStrategy'>>) => void;
7
+ updateDefaults: (newDefaults: Partial<InferFromL5Schema<S>>) => void;
8
+ defaultsRef: Ref<Partial<InferFromL5Schema<S>>>;
9
+ }
10
+ export declare function useL5<S extends SchemaDefinition>(scheme: S, options?: Options<S>): UseL5Return<S>;
@@ -0,0 +1,77 @@
1
+ import {
2
+ shallowRef,
3
+ ref,
4
+ useRoute,
5
+ useRouter,
6
+ watch,
7
+ useRuntimeConfig
8
+ } from "#imports";
9
+ import { parseFiltersFromQuery } from "../utils/parseFiltersFromQuery.js";
10
+ import { buildQueryForApi } from "../utils/buildQueryForApi.js";
11
+ import { buildQueryForUrl } from "../utils/buildQueryForUrl.js";
12
+ export function useL5(scheme, options = {}) {
13
+ const config = useRuntimeConfig();
14
+ const moduleOptions = config.public?.useL5 ?? {};
15
+ const route = useRoute();
16
+ const router = useRouter();
17
+ const mergedOptions = { ...moduleOptions, ...options };
18
+ const { syncWithRoute = false, urlUpdateStrategy = "push" } = mergedOptions;
19
+ const defaultsRef = ref(mergedOptions.defaults ?? {});
20
+ const query = syncWithRoute ? route.query : {};
21
+ const filters = ref(
22
+ parseFiltersFromQuery(scheme, query, {
23
+ defaults: defaultsRef.value
24
+ })
25
+ );
26
+ const queryForApi = shallowRef(
27
+ buildQueryForApi(filters.value, mergedOptions)
28
+ );
29
+ function updateFilters(newFilters, _options = {}) {
30
+ const {
31
+ urlUpdateStrategy: localUrlUpdateStrategy = urlUpdateStrategy
32
+ } = _options;
33
+ for (const key in newFilters) {
34
+ filters.value[key] = newFilters[key];
35
+ }
36
+ if (!syncWithRoute) {
37
+ queryForApi.value = buildQueryForApi(filters.value, mergedOptions);
38
+ return;
39
+ }
40
+ const query2 = buildQueryForUrl(filters.value, {
41
+ ...mergedOptions,
42
+ defaults: defaultsRef.value
43
+ });
44
+ const method = localUrlUpdateStrategy === "replace" ? router.replace : router.push;
45
+ method.call(router, { query: query2 });
46
+ }
47
+ function updateDefaults(newDefaults) {
48
+ defaultsRef.value = {
49
+ ...defaultsRef.value,
50
+ ...newDefaults
51
+ };
52
+ }
53
+ function startWatchingForRoute() {
54
+ watch(
55
+ () => route.query,
56
+ (newQuery) => {
57
+ filters.value = parseFiltersFromQuery(scheme, newQuery, {
58
+ defaults: defaultsRef.value
59
+ });
60
+ queryForApi.value = buildQueryForApi(
61
+ filters.value,
62
+ mergedOptions
63
+ );
64
+ }
65
+ );
66
+ }
67
+ if (syncWithRoute) {
68
+ startWatchingForRoute();
69
+ }
70
+ return {
71
+ filters,
72
+ queryForApi,
73
+ updateFilters,
74
+ updateDefaults,
75
+ defaultsRef
76
+ };
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { BaseParams, L5Node } from '../types/index.js';
2
+ export declare const BASE_PARAMS_DEFAULTS: BaseParams;
3
+ export declare const BASE_PARAMS_DEFAULTS_TYPE_MAP: Record<keyof BaseParams, L5Node>;
@@ -0,0 +1,18 @@
1
+ export const BASE_PARAMS_DEFAULTS = {
2
+ page: 1,
3
+ limit: 10,
4
+ sortedBy: "id",
5
+ orderBy: "desc",
6
+ searchJoin: "and",
7
+ searchFields: null,
8
+ search: null
9
+ };
10
+ export const BASE_PARAMS_DEFAULTS_TYPE_MAP = {
11
+ page: Number,
12
+ limit: Number,
13
+ sortedBy: String,
14
+ orderBy: String,
15
+ searchJoin: String,
16
+ searchFields: String,
17
+ search: String
18
+ };
@@ -0,0 +1,33 @@
1
+ export type Primitive = string | number | boolean | null | undefined;
2
+ export type L5Node = StringConstructor | NumberConstructor | BooleanConstructor | [L5Node] | {
3
+ [k: string]: L5Node;
4
+ };
5
+ export type InferL5<T> = T extends StringConstructor ? string | null : T extends NumberConstructor ? number | null : T extends BooleanConstructor ? boolean : T extends [infer U] ? InferL5<U>[] : T extends Record<string, unknown> ? {
6
+ [K in keyof T]: InferL5<T[K]>;
7
+ } : never;
8
+ export type SchemaDefinition = Record<string, L5Node>;
9
+ export type InferFromL5Schema<S extends SchemaDefinition> = {
10
+ [K in keyof S]: InferL5<S[K]>;
11
+ };
12
+ export type Filters<S extends SchemaDefinition> = InferL5<S> & BaseParams;
13
+ export interface Options<S extends SchemaDefinition> {
14
+ defaults?: Partial<InferFromL5Schema<S>>;
15
+ syncWithRoute?: boolean;
16
+ excludeFromSearch?: (keyof S)[];
17
+ apiIncludes?: string[];
18
+ excludeFromQueryBuilder?: (keyof S)[];
19
+ boolToNumber?: boolean;
20
+ queryAliases?: Partial<Record<keyof S, string>>;
21
+ transformInput?: (query: Partial<S>) => Partial<S>;
22
+ transformOutput?: (filters: Filters<S>) => Record<keyof S & keyof BaseParams, unknown>;
23
+ urlUpdateStrategy?: 'replace' | 'push';
24
+ }
25
+ export interface BaseParams {
26
+ page: number;
27
+ limit: number;
28
+ sortedBy: string;
29
+ orderBy: string;
30
+ searchJoin: string;
31
+ searchFields: string | null;
32
+ search: string | null;
33
+ }
File without changes
@@ -0,0 +1,2 @@
1
+ import type { Filters, SchemaDefinition, Options } from '../types/index.js';
2
+ export declare function buildQueryForApi<S extends SchemaDefinition>(_filters: Filters<S>, options: Options<S>): Record<string, unknown>;
@@ -0,0 +1,45 @@
1
+ import { BASE_PARAMS_DEFAULTS } from "../constant/baseParams.const.js";
2
+ import { pick } from "es-toolkit";
3
+ const BASE_PARAMS_KEYS = Object.keys(BASE_PARAMS_DEFAULTS);
4
+ export function buildQueryForApi(_filters, options) {
5
+ let filters = { ..._filters };
6
+ const result = pick(filters, BASE_PARAMS_KEYS);
7
+ const {
8
+ excludeFromSearch = [],
9
+ apiIncludes = [],
10
+ excludeFromQueryBuilder = [],
11
+ queryAliases,
12
+ transformOutput
13
+ } = options;
14
+ if (transformOutput) {
15
+ filters = transformOutput(_filters);
16
+ }
17
+ const allExcludeKeys = [...BASE_PARAMS_KEYS, ...excludeFromSearch];
18
+ if (options.boolToNumber) {
19
+ Object.entries(filters).forEach(([key, value]) => {
20
+ if (typeof value === "boolean") {
21
+ filters[key] = Number(value);
22
+ }
23
+ });
24
+ }
25
+ allExcludeKeys.forEach((key) => {
26
+ const alias = queryAliases?.[key] ?? key;
27
+ result[alias] = filters[key];
28
+ });
29
+ result.search = Object.entries(filters).filter(([key, value]) => {
30
+ if (BASE_PARAMS_KEYS.includes(key)) return false;
31
+ if (excludeFromSearch.includes(key)) return false;
32
+ if (excludeFromQueryBuilder.includes(key)) return false;
33
+ if (Array.isArray(value)) {
34
+ return value.length > 0;
35
+ }
36
+ return value !== null;
37
+ }).map(([key, value]) => {
38
+ const alias = queryAliases?.[key] ?? key;
39
+ return `${alias}:${Array.isArray(value) ? value.join(",") : value}`;
40
+ }).join(";");
41
+ if (apiIncludes.length) {
42
+ result.include = apiIncludes.join(",");
43
+ }
44
+ return result;
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { Filters, Options, SchemaDefinition } from '../types/index.js';
2
+ import type { LocationQuery } from 'vue-router';
3
+ export declare function buildQueryForUrl<S extends SchemaDefinition>(filters: Filters<S>, options: Options<S>): LocationQuery;
@@ -0,0 +1,17 @@
1
+ import { BASE_PARAMS_DEFAULTS } from "../constant/baseParams.const.js";
2
+ import { toMerged } from "es-toolkit/object";
3
+ import { isEqualArray } from "./isEqualArray.js";
4
+ export function buildQueryForUrl(filters, options) {
5
+ const defaults = toMerged(BASE_PARAMS_DEFAULTS, options.defaults ?? {});
6
+ return Object.entries(filters).filter(([key, value]) => {
7
+ if (value === null || value === void 0) return false;
8
+ const defaultValue = defaults?.[key];
9
+ if (Array.isArray(value) && Array.isArray(defaultValue)) {
10
+ return !isEqualArray(value, defaultValue);
11
+ }
12
+ return value !== defaultValue;
13
+ }).reduce((acc, [k, v]) => {
14
+ acc[k] = v;
15
+ return acc;
16
+ }, {});
17
+ }
@@ -0,0 +1 @@
1
+ export declare function isEqualArray(first: unknown[], second: unknown[]): boolean;
@@ -0,0 +1,8 @@
1
+ import { isEqual } from "es-toolkit/predicate";
2
+ export function isEqualArray(first, second) {
3
+ if (first === second) return true;
4
+ if (first.length !== second.length) return false;
5
+ const normalizedFirst = [...first].sort();
6
+ const normalizedSecond = [...second].sort();
7
+ return isEqual(normalizedFirst, normalizedSecond);
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { Filters, SchemaDefinition, Options } from '../types/index.js';
2
+ import type { LocationQuery } from 'vue-router';
3
+ export declare function parseFiltersFromQuery<S extends SchemaDefinition>(scheme: S, query: LocationQuery, options: Required<Pick<Options<S>, 'defaults'>>): Filters<S>;
@@ -0,0 +1,19 @@
1
+ import { parseValueFromQuery } from "./parseValueFromQuery.js";
2
+ import { BASE_PARAMS_DEFAULTS, BASE_PARAMS_DEFAULTS_TYPE_MAP } from "../constant/baseParams.const.js";
3
+ export function parseFiltersFromQuery(scheme, query, options) {
4
+ const result = {};
5
+ Object.keys(scheme).forEach((key) => {
6
+ if (!scheme[key]) return;
7
+ let value = query[key];
8
+ value ??= options.defaults[key];
9
+ result[key] = parseValueFromQuery(value, scheme[key]);
10
+ });
11
+ for (const key in BASE_PARAMS_DEFAULTS_TYPE_MAP) {
12
+ const typedKey = key;
13
+ let value = query[typedKey];
14
+ value ??= options.defaults[key];
15
+ value ??= BASE_PARAMS_DEFAULTS[typedKey];
16
+ result[key] = parseValueFromQuery(value, BASE_PARAMS_DEFAULTS_TYPE_MAP[typedKey]);
17
+ }
18
+ return result;
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { L5Node, Primitive } from '../types/index.js';
2
+ import type { LocationQueryValue } from 'vue-router';
3
+ export declare function parseValueFromQuery(value: LocationQueryValue | LocationQueryValue[] | Primitive | Primitive[], node: L5Node): Primitive | Primitive[];
@@ -0,0 +1,34 @@
1
+ function asArray(value) {
2
+ if (!value) return [];
3
+ if (Array.isArray(value)) return value;
4
+ return [value];
5
+ }
6
+ export function parseValueFromQuery(value, node) {
7
+ let isArray = false;
8
+ if (Array.isArray(node)) {
9
+ node = node[0];
10
+ isArray = true;
11
+ }
12
+ switch (node) {
13
+ case String:
14
+ if (isArray) {
15
+ return asArray(value).map((i) => String(i));
16
+ }
17
+ if (!value) return null;
18
+ return String(value);
19
+ case Number:
20
+ if (isArray) {
21
+ return asArray(value).map((i) => {
22
+ i = Number(i);
23
+ return Number.isNaN(i) ? null : i;
24
+ }).filter((i) => i);
25
+ }
26
+ if (!value) return null;
27
+ value = Number(value);
28
+ return Number.isNaN(value) ? null : value;
29
+ case Boolean:
30
+ if (!value) return false;
31
+ return ["1", "true"].includes(value.toString());
32
+ default:
33
+ }
34
+ }
@@ -0,0 +1,5 @@
1
+ export { type BaseParams, type Filters, type InferFromL5Schema, type InferL5, type L5Node, type Options, type Primitive, type SchemaDefinition } from '../dist/runtime/types/index.js'
2
+
3
+ export { default } from './module.mjs'
4
+
5
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "use-l5",
3
+ "version": "0.0.1",
4
+ "description": "Nuxt-модуль с типизированным useL5-композаблом для парсинга фильтров, синхронизации с query в роуте и сборки параметров для API/URL",
5
+ "author": {
6
+ "name": "Kirill Merkulov",
7
+ "url": "https://github.com/merkulovka"
8
+ },
9
+ "homepage": "https://github.com/merkulovka/use-l5#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/merkulovka/use-l5.git"
13
+ },
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/types.d.mts",
19
+ "import": "./dist/module.mjs"
20
+ },
21
+ "./runtime/*": {
22
+ "types": "./dist/runtime/*",
23
+ "import": "./dist/runtime/*"
24
+ }
25
+ },
26
+ "main": "./dist/module.mjs",
27
+ "typesVersions": {
28
+ "*": {
29
+ ".": [
30
+ "./dist/types.d.mts"
31
+ ],
32
+ "runtime/*": [
33
+ "./dist/runtime/*"
34
+ ]
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "scripts": {
41
+ "prepack": "nuxt-module-build build",
42
+ "dev": "npm run dev:prepare && nuxi dev playground",
43
+ "dev:build": "nuxi build playground",
44
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
45
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
46
+ "lint": "eslint .",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest watch",
49
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
50
+ },
51
+ "dependencies": {
52
+ "@nuxt/kit": "^4.2.1",
53
+ "es-toolkit": "^1.42.0"
54
+ },
55
+ "devDependencies": {
56
+ "@nuxt/devtools": "^3.1.0",
57
+ "@nuxt/eslint-config": "^1.10.0",
58
+ "@nuxt/module-builder": "^1.0.2",
59
+ "@nuxt/schema": "^4.2.1",
60
+ "@nuxt/test-utils": "^3.20.1",
61
+ "@types/node": "latest",
62
+ "changelogen": "^0.6.2",
63
+ "eslint": "^9.39.1",
64
+ "nuxt": "^4.2.1",
65
+ "typescript": "~5.9.3",
66
+ "vitest": "^4.0.10",
67
+ "vue-tsc": "^3.1.4"
68
+ },
69
+ "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
70
+ }