@eienjs/adonisjs-api-query 1.1.1 → 3.0.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/{LICENSE.md → LICENSE} +1 -1
- package/README.md +166 -2
- package/build/configure.d.ts +2 -2
- package/build/configure.js +10 -0
- package/build/index.d.ts +38 -31
- package/build/index.js +38 -154
- package/build/providers/api_query_provider.d.ts +48 -37
- package/build/providers/api_query_provider.js +26 -27
- package/build/src/allowed_filter.d.ts +44 -36
- package/build/src/allowed_filter.js +135 -0
- package/build/src/allowed_include.d.ts +15 -16
- package/build/src/allowed_include.js +47 -0
- package/build/src/allowed_sort.d.ts +17 -17
- package/build/src/allowed_sort.js +49 -0
- package/build/src/api_query_builder_request.d.ts +55 -30
- package/build/src/api_query_builder_request.js +200 -0
- package/build/src/base_filterable.d.ts +45 -0
- package/build/src/base_filterable.js +172 -0
- package/build/src/bindings/api_query_request.js +13 -0
- package/build/src/bindings/filterable_query.js +51 -0
- package/build/src/bindings/filters_query.js +38 -0
- package/build/src/bindings/includes_query.js +50 -0
- package/build/src/bindings/sorts_query.js +42 -0
- package/build/src/bindings/validation_query.js +27 -0
- package/build/src/decorators/filterable.d.ts +20 -0
- package/build/src/decorators/filterable.js +24 -0
- package/build/src/define_config.d.ts +3 -3
- package/build/src/define_config.js +25 -0
- package/build/src/enums/filter_operator.d.ts +8 -9
- package/build/src/enums/filter_operator.js +11 -0
- package/build/src/enums/sort_direction.d.ts +3 -4
- package/build/src/enums/sort_direction.js +6 -0
- package/build/src/exceptions/allowed_fields_must_be_called_before_allowed_includes.d.ts +5 -5
- package/build/src/exceptions/allowed_fields_must_be_called_before_allowed_includes.js +10 -0
- package/build/src/exceptions/filterable_not_configured.d.ts +5 -0
- package/build/src/exceptions/filterable_not_configured.js +12 -0
- package/build/src/exceptions/invalid_append_query.d.ts +6 -4
- package/build/src/exceptions/invalid_append_query.js +17 -0
- package/build/src/exceptions/invalid_direction.d.ts +3 -3
- package/build/src/exceptions/invalid_direction.js +9 -0
- package/build/src/exceptions/invalid_field_query.d.ts +6 -4
- package/build/src/exceptions/invalid_field_query.js +17 -0
- package/build/src/exceptions/invalid_filter_query.d.ts +6 -6
- package/build/src/exceptions/invalid_filter_query.js +17 -0
- package/build/src/exceptions/invalid_filter_value.d.ts +3 -3
- package/build/src/exceptions/invalid_filter_value.js +8 -0
- package/build/src/exceptions/invalid_include_query.d.ts +6 -6
- package/build/src/exceptions/invalid_include_query.js +21 -0
- package/build/src/exceptions/invalid_query.d.ts +3 -3
- package/build/{chunk-XIRHVLVQ.js → src/exceptions/invalid_query.js} +3 -7
- package/build/src/exceptions/invalid_sort_query.d.ts +6 -6
- package/build/src/exceptions/invalid_sort_query.js +17 -0
- package/build/src/exceptions/unknown_included_fields_query.d.ts +4 -4
- package/build/src/exceptions/unknown_included_fields_query.js +12 -0
- package/build/src/exceptions/vine_not_installed.d.ts +5 -0
- package/build/src/exceptions/vine_not_installed.js +12 -0
- package/build/src/filters/filters_begin_with_strict.d.ts +6 -6
- package/build/src/filters/filters_begin_with_strict.js +8 -0
- package/build/src/filters/filters_callback.d.ts +6 -6
- package/build/src/filters/filters_callback.js +11 -0
- package/build/src/filters/filters_ends_with_strict.d.ts +6 -6
- package/build/src/filters/filters_ends_with_strict.js +8 -0
- package/build/src/filters/filters_exact.d.ts +11 -11
- package/build/src/filters/filters_exact.js +50 -0
- package/build/src/filters/filters_operator.d.ts +13 -13
- package/build/src/filters/filters_operator.js +52 -0
- package/build/src/filters/filters_partial.d.ts +9 -11
- package/build/src/filters/filters_partial.js +37 -0
- package/build/src/filters/filters_scope.d.ts +12 -0
- package/build/src/filters/filters_scope.js +98 -0
- package/build/src/filters/filters_trashed.d.ts +5 -5
- package/build/src/filters/filters_trashed.js +13 -0
- package/build/src/includes/included_callback.d.ts +7 -7
- package/build/src/includes/included_callback.js +11 -0
- package/build/src/includes/included_count.d.ts +4 -4
- package/build/src/includes/included_count.js +11 -0
- package/build/src/includes/included_relationship.d.ts +8 -8
- package/build/src/includes/included_relationship.js +37 -0
- package/build/src/sorts/sorts_callback.d.ts +6 -6
- package/build/src/sorts/sorts_callback.js +11 -0
- package/build/src/sorts/sorts_field.d.ts +4 -4
- package/build/src/sorts/sorts_field.js +7 -0
- package/build/src/utils/helpers.js +14 -0
- package/build/src/validation/schema_builder.d.ts +3 -0
- package/build/src/validation/schema_builder.js +38 -0
- package/build/src/validation/vine_resolver.js +13 -0
- package/build/stubs/main.d.ts +1 -1
- package/build/stubs/main.js +3 -0
- package/build/types.d.ts +116 -0
- package/build/types.js +1 -0
- package/package.json +101 -88
- package/build/api_query_request-QXOB4BN6.js +0 -18
- package/build/api_query_request-QXOB4BN6.js.map +0 -1
- package/build/chunk-3ITWUZL5.js +0 -108
- package/build/chunk-3ITWUZL5.js.map +0 -1
- package/build/chunk-54HNQTDY.js +0 -348
- package/build/chunk-54HNQTDY.js.map +0 -1
- package/build/chunk-IV3B5WO7.js +0 -160
- package/build/chunk-IV3B5WO7.js.map +0 -1
- package/build/chunk-MKQ3HMMA.js +0 -198
- package/build/chunk-MKQ3HMMA.js.map +0 -1
- package/build/chunk-XIRHVLVQ.js.map +0 -1
- package/build/filters_query-LCFH2F2X.js +0 -55
- package/build/filters_query-LCFH2F2X.js.map +0 -1
- package/build/includes_query-PVONWQMC.js +0 -70
- package/build/includes_query-PVONWQMC.js.map +0 -1
- package/build/index.js.map +0 -1
- package/build/metafile-esm.json +0 -1
- package/build/providers/api_query_provider.js.map +0 -1
- package/build/sorts_query-NJTC5UXX.js +0 -63
- package/build/sorts_query-NJTC5UXX.js.map +0 -1
- package/build/src/bindings/api_query_request.d.ts +0 -2
- package/build/src/bindings/filters_query.d.ts +0 -3
- package/build/src/bindings/includes_query.d.ts +0 -3
- package/build/src/bindings/sorts_query.d.ts +0 -3
- package/build/src/types.d.ts +0 -90
- package/build/src/types.js +0 -1
- package/build/src/types.js.map +0 -1
- package/build/src/utils/helpers.d.ts +0 -3
- /package/build/{config.stub → stubs/config.stub} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FiltersExact } from "./filters_exact.js";
|
|
2
|
+
import { FilterOperator } from "../enums/filter_operator.js";
|
|
3
|
+
// src/filters/filters_operator.ts
|
|
4
|
+
const SORTED_OPERATORS = Object.values(FilterOperator).filter((op) => op !== FilterOperator.Dynamic).toSorted((a, b) => b.length - a.length);
|
|
5
|
+
var FiltersOperator = class extends FiltersExact {
|
|
6
|
+
shouldAddRelationConstraint;
|
|
7
|
+
filterOperator;
|
|
8
|
+
boolean;
|
|
9
|
+
constructor(shouldAddRelationConstraint, filterOperator, boolean) {
|
|
10
|
+
super(shouldAddRelationConstraint);
|
|
11
|
+
this.shouldAddRelationConstraint = shouldAddRelationConstraint;
|
|
12
|
+
this.filterOperator = filterOperator;
|
|
13
|
+
this.boolean = boolean;
|
|
14
|
+
}
|
|
15
|
+
handle(query, value, property) {
|
|
16
|
+
let { filterOperator } = this;
|
|
17
|
+
if (this.shouldAddRelationConstraint && this.isRelationProperty(query, property)) {
|
|
18
|
+
this.withRelationConstraint(query, value, property);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
const items = value;
|
|
23
|
+
query.where((subQuery) => {
|
|
24
|
+
for (const item of items) this.handle(subQuery, item, property);
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (value === null) {
|
|
29
|
+
query.whereNull(this.qualifyColumn(query, property));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "object") return;
|
|
33
|
+
if (filterOperator === FilterOperator.Dynamic) {
|
|
34
|
+
const strValue = value.toString();
|
|
35
|
+
filterOperator = this.getDynamicFilterOperator(strValue);
|
|
36
|
+
value = this.removeDynamicFilterOperatorFromValue(strValue, filterOperator);
|
|
37
|
+
}
|
|
38
|
+
if (this.boolean === "and") {
|
|
39
|
+
query.where(this.qualifyColumn(query, property), filterOperator, value);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
query.orWhere(this.qualifyColumn(query, property), filterOperator, value);
|
|
43
|
+
}
|
|
44
|
+
getDynamicFilterOperator(value) {
|
|
45
|
+
for (const filterOperatorCase of SORTED_OPERATORS) if (value.startsWith(filterOperatorCase)) return filterOperatorCase;
|
|
46
|
+
return FilterOperator.Equal;
|
|
47
|
+
}
|
|
48
|
+
removeDynamicFilterOperatorFromValue(value, filterOperator) {
|
|
49
|
+
return value.startsWith(filterOperator) ? value.slice(filterOperator.length) : value;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export { FiltersOperator };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { FiltersExact } from './filters_exact.js';
|
|
1
|
+
import { Filter } from "../../types.js";
|
|
2
|
+
import { FiltersExact } from "./filters_exact.js";
|
|
3
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
4
|
+
import { StrictValuesWithoutRaw } from "@adonisjs/lucid/types/querybuilder";
|
|
6
5
|
export declare class FiltersPartial<Model extends LucidModel> extends FiltersExact<Model> implements Filter<Model> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
6
|
+
handle(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, value: StrictValuesWithoutRaw | null, property: string): void;
|
|
7
|
+
protected parsePropertyToColumn(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, property: string): string;
|
|
8
|
+
protected getWhereRawParameters(value: StrictValuesWithoutRaw | null, property: string): [string, string[]];
|
|
9
|
+
protected static escapeLike(value: string): string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { FiltersExact } from "./filters_exact.js";
|
|
2
|
+
// src/filters/filters_partial.ts
|
|
3
|
+
var FiltersPartial = class FiltersPartial extends FiltersExact {
|
|
4
|
+
handle(query, value, property) {
|
|
5
|
+
if (this.shouldAddRelationConstraint && this.isRelationProperty(query, property)) {
|
|
6
|
+
this.withRelationConstraint(query, value, property);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
const subValues = value.flat(Infinity).filter((item) => item !== null && typeof item !== "object" && String(item).length > 0);
|
|
11
|
+
if (subValues.length === 0) return;
|
|
12
|
+
query.where((subQuery) => {
|
|
13
|
+
const column = this.parsePropertyToColumn(subQuery, property);
|
|
14
|
+
for (const partialValue of subValues) {
|
|
15
|
+
const [innerSql, innerBindings] = this.getWhereRawParameters(partialValue, column);
|
|
16
|
+
subQuery.orWhereRaw(innerSql, innerBindings);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (value === null || typeof value === "object") return;
|
|
22
|
+
const [sql, bindings] = this.getWhereRawParameters(value, this.parsePropertyToColumn(query, property));
|
|
23
|
+
query.whereRaw(sql, bindings);
|
|
24
|
+
}
|
|
25
|
+
parsePropertyToColumn(query, property) {
|
|
26
|
+
if (property.includes(".")) return property;
|
|
27
|
+
return query.model.$getColumn(property)?.columnName ?? property;
|
|
28
|
+
}
|
|
29
|
+
getWhereRawParameters(value, property) {
|
|
30
|
+
const resolvedValue = String(value).toLowerCase();
|
|
31
|
+
return ["LOWER(??) LIKE ?", [property, `%${FiltersPartial.escapeLike(resolvedValue)}%`]];
|
|
32
|
+
}
|
|
33
|
+
static escapeLike(value) {
|
|
34
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll("_", String.raw`\_`).replaceAll("%", String.raw`\%`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export { FiltersPartial };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Filter } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
|
+
export declare class FiltersScope<Model extends LucidModel> implements Filter<Model> {
|
|
4
|
+
handle(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, value: unknown, property: string): void;
|
|
5
|
+
protected applyScope(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, value: unknown, property: string): void;
|
|
6
|
+
protected getModelScopeFunction(model: LucidModel, candidate: string): ((...args: unknown[]) => void) | undefined;
|
|
7
|
+
protected resolveScopeMethod(model: Model, property: string): string;
|
|
8
|
+
protected extractParametersString(fn: (...args: unknown[]) => void): string | null;
|
|
9
|
+
protected hasParameters(fn: (...args: unknown[]) => void): boolean;
|
|
10
|
+
protected countParameters(fn: (...args: unknown[]) => void): number;
|
|
11
|
+
protected resolveArguments(scopeFn: ((...args: unknown[]) => void) | undefined, value: unknown, resolvedParamCount?: number): unknown[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import string from "@adonisjs/core/helpers/string";
|
|
2
|
+
import { BaseModel } from "@adonisjs/lucid/orm";
|
|
3
|
+
// src/filters/filters_scope.ts
|
|
4
|
+
const FN_PARAMS_REGEX = /^(?:async\s*)?(?:function[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([^\s(=>]+)\s*=>)/;
|
|
5
|
+
const RESERVED_MODEL_METHODS = /* @__PURE__ */ new Set([
|
|
6
|
+
"boot",
|
|
7
|
+
"before",
|
|
8
|
+
"after",
|
|
9
|
+
"saving",
|
|
10
|
+
"creating",
|
|
11
|
+
"updating",
|
|
12
|
+
"deleting",
|
|
13
|
+
"finding",
|
|
14
|
+
"fetching",
|
|
15
|
+
"paginating",
|
|
16
|
+
"constructor",
|
|
17
|
+
"prototype"
|
|
18
|
+
]);
|
|
19
|
+
var FiltersScope = class {
|
|
20
|
+
handle(query, value, property) {
|
|
21
|
+
const dotIndex = property.indexOf(".");
|
|
22
|
+
if (dotIndex !== -1) {
|
|
23
|
+
const firstRelation = property.slice(0, dotIndex);
|
|
24
|
+
if (query.model.$relationsDefinitions.has(firstRelation)) {
|
|
25
|
+
const remainingProperty = property.slice(dotIndex + 1);
|
|
26
|
+
query.whereHas(firstRelation, (subQuery) => {
|
|
27
|
+
this.handle(subQuery, value, remainingProperty);
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.applyScope(query, value, property);
|
|
33
|
+
}
|
|
34
|
+
applyScope(query, value, property) {
|
|
35
|
+
const scopeMethod = this.resolveScopeMethod(query.model, property);
|
|
36
|
+
const scopeFn = this.getModelScopeFunction(query.model, scopeMethod);
|
|
37
|
+
if (!scopeFn) throw new Error(`"${scopeMethod}" is not defined as a query scope on "${query.model.name}" model`);
|
|
38
|
+
const paramCount = this.countParameters(scopeFn);
|
|
39
|
+
if (paramCount <= 1) {
|
|
40
|
+
if (value === false) return;
|
|
41
|
+
query.withScopes((scopes) => {
|
|
42
|
+
scopes[scopeMethod]();
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const args = this.resolveArguments(scopeFn, value, paramCount);
|
|
47
|
+
query.withScopes((scopes) => {
|
|
48
|
+
scopes[scopeMethod](...args);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
getModelScopeFunction(model, candidate) {
|
|
52
|
+
if (!candidate || candidate.startsWith("$") || candidate.startsWith("_") || RESERVED_MODEL_METHODS.has(candidate)) return;
|
|
53
|
+
let current = model;
|
|
54
|
+
while (typeof current === "function" && current !== BaseModel && current !== Function.prototype && current !== Object.prototype) {
|
|
55
|
+
if (Object.hasOwn(current, candidate)) {
|
|
56
|
+
const fn = current[candidate];
|
|
57
|
+
return typeof fn === "function" ? fn : void 0;
|
|
58
|
+
}
|
|
59
|
+
current = Object.getPrototypeOf(current);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
resolveScopeMethod(model, property) {
|
|
63
|
+
const camelCased = string.camelCase(property);
|
|
64
|
+
const propertyWithScope = `${property}Scope`;
|
|
65
|
+
const camelCasedWithScope = `${camelCased}Scope`;
|
|
66
|
+
const candidates = property === camelCased ? [property, propertyWithScope] : [
|
|
67
|
+
property,
|
|
68
|
+
camelCased,
|
|
69
|
+
propertyWithScope,
|
|
70
|
+
camelCasedWithScope
|
|
71
|
+
];
|
|
72
|
+
for (const candidate of candidates) if (this.getModelScopeFunction(model, candidate)) return candidate;
|
|
73
|
+
return camelCased;
|
|
74
|
+
}
|
|
75
|
+
extractParametersString(fn) {
|
|
76
|
+
const match = FN_PARAMS_REGEX.exec(fn.toString());
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return (match.at(1) ?? match.at(2) ?? match.at(3) ?? "").trim();
|
|
79
|
+
}
|
|
80
|
+
hasParameters(fn) {
|
|
81
|
+
return this.countParameters(fn) > 1;
|
|
82
|
+
}
|
|
83
|
+
countParameters(fn) {
|
|
84
|
+
if (fn.length > 1) return fn.length;
|
|
85
|
+
const params = this.extractParametersString(fn);
|
|
86
|
+
if (!params) return fn.length;
|
|
87
|
+
return params.split(",").length;
|
|
88
|
+
}
|
|
89
|
+
resolveArguments(scopeFn, value, resolvedParamCount) {
|
|
90
|
+
if (Array.isArray(value)) return value;
|
|
91
|
+
if (typeof value === "object" && value !== null) {
|
|
92
|
+
if ((resolvedParamCount ?? (scopeFn ? this.countParameters(scopeFn) : 0)) > 2) return Object.values(value);
|
|
93
|
+
return [value];
|
|
94
|
+
}
|
|
95
|
+
return [value];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
export { FiltersScope };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Filter } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
|
+
import { StrictValuesWithoutRaw } from "@adonisjs/lucid/types/querybuilder";
|
|
4
4
|
export declare class FiltersTrashed<Model extends LucidModel> implements Filter<Model> {
|
|
5
|
-
|
|
6
|
-
}
|
|
5
|
+
handle(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, value: StrictValuesWithoutRaw | null, _property: string): void;
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/filters/filters_trashed.ts
|
|
2
|
+
var FiltersTrashed = class {
|
|
3
|
+
handle(query, value, _property) {
|
|
4
|
+
if (!("withTrashed" in query && "onlyTrashed" in query)) return;
|
|
5
|
+
const queryWithSoftDeletes = query;
|
|
6
|
+
if (value === "with") {
|
|
7
|
+
queryWithSoftDeletes.withTrashed();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (value === "only") queryWithSoftDeletes.onlyTrashed();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export { FiltersTrashed };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Include } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
|
+
import { ModelRelations } from "@adonisjs/lucid/types/relations";
|
|
4
4
|
export declare class IncludedCallback<ParentModel extends LucidModel, RelatedModel extends LucidModel> implements Include<ParentModel> {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
5
|
+
readonly callback: (query: ModelRelations<RelatedModel, ParentModel>['builder'] | ModelRelations<RelatedModel, ParentModel>['subQuery']) => void;
|
|
6
|
+
constructor(callback: (query: ModelRelations<RelatedModel, ParentModel>['builder'] | ModelRelations<RelatedModel, ParentModel>['subQuery']) => void);
|
|
7
|
+
handle(query: ModelQueryBuilderContract<ParentModel, InstanceType<ParentModel>>, include: string): void;
|
|
8
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Include } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
3
|
export declare class IncludedCount<ParentModel extends LucidModel> implements Include<ParentModel> {
|
|
4
|
-
|
|
5
|
-
}
|
|
4
|
+
handle(query: ModelQueryBuilderContract<ParentModel, InstanceType<ParentModel>>, count: string): void;
|
|
5
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import app from "@adonisjs/core/services/app";
|
|
2
|
+
// src/includes/included_count.ts
|
|
3
|
+
var IncludedCount = class {
|
|
4
|
+
handle(query, count) {
|
|
5
|
+
const suffix = app.config.get("apiquery.countSuffix", "Count");
|
|
6
|
+
const relation = count.endsWith(suffix) ? count.slice(0, -suffix.length) : count;
|
|
7
|
+
if (!query.model.$hasRelation(relation)) return;
|
|
8
|
+
query.withCount(relation);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export { IncludedCount };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { type Include } from '../types.js';
|
|
1
|
+
import { Include } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
4
3
|
export declare class IncludedRelationship<ParentModel extends LucidModel> implements Include<ParentModel> {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
getRequestedFieldsForRelatedTable?: (relationship: string) => string[];
|
|
5
|
+
handle(query: ModelQueryBuilderContract<ParentModel, InstanceType<ParentModel>>, relationship: string): void;
|
|
6
|
+
static getIndividualRelationshipPathsFromInclude(include: string): string[];
|
|
7
|
+
private applyRequestedFields;
|
|
8
|
+
private buildNestedQuery;
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/includes/included_relationship.ts
|
|
2
|
+
var IncludedRelationship = class {
|
|
3
|
+
getRequestedFieldsForRelatedTable;
|
|
4
|
+
handle(query, relationship) {
|
|
5
|
+
const [relation, ...rest] = relationship.split(".");
|
|
6
|
+
if (!relation || !query.model.$hasRelation(relation)) return;
|
|
7
|
+
query.preload(relation, (subQuery) => {
|
|
8
|
+
this.applyRequestedFields(subQuery, relation);
|
|
9
|
+
this.buildNestedQuery(rest, subQuery, relation);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
static getIndividualRelationshipPathsFromInclude(include) {
|
|
13
|
+
const parts = include.split(".");
|
|
14
|
+
const includes = [];
|
|
15
|
+
let current = "";
|
|
16
|
+
for (const part of parts) {
|
|
17
|
+
current = current ? `${current}.${part}` : part;
|
|
18
|
+
includes.push(current);
|
|
19
|
+
}
|
|
20
|
+
return includes;
|
|
21
|
+
}
|
|
22
|
+
applyRequestedFields(query, relationshipName) {
|
|
23
|
+
if (this.getRequestedFieldsForRelatedTable === void 0) return;
|
|
24
|
+
const fields = this.getRequestedFieldsForRelatedTable(relationshipName);
|
|
25
|
+
if (fields.length > 0) query.select(fields);
|
|
26
|
+
}
|
|
27
|
+
buildNestedQuery(relatedTables, subQuery, parent = "") {
|
|
28
|
+
const [relation, ...rest] = relatedTables;
|
|
29
|
+
if (!relation || !subQuery.model.$hasRelation(relation)) return;
|
|
30
|
+
subQuery.preload(relation, (nestedQuery) => {
|
|
31
|
+
const fullRelationName = parent === "" ? relation : `${parent}.${relation}`;
|
|
32
|
+
this.applyRequestedFields(nestedQuery, fullRelationName);
|
|
33
|
+
this.buildNestedQuery(rest, nestedQuery, fullRelationName);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export { IncludedRelationship };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Sort } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
3
|
export declare class SortsCallback<Model extends LucidModel> implements Sort<Model> {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
4
|
+
private readonly _callback;
|
|
5
|
+
constructor(_callback: (query: ModelQueryBuilderContract<Model>, isDescending: boolean, property: string) => void);
|
|
6
|
+
handle(query: ModelQueryBuilderContract<Model>, isDescending: boolean, property: string): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/sorts/sorts_callback.ts
|
|
2
|
+
var SortsCallback = class {
|
|
3
|
+
_callback;
|
|
4
|
+
constructor(_callback) {
|
|
5
|
+
this._callback = _callback;
|
|
6
|
+
}
|
|
7
|
+
handle(query, isDescending, property) {
|
|
8
|
+
this._callback(query, isDescending, property);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export { SortsCallback };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Sort } from "../../types.js";
|
|
2
|
+
import { LucidModel, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
3
3
|
export declare class SortsField<Model extends LucidModel> implements Sort<Model> {
|
|
4
|
-
|
|
5
|
-
}
|
|
4
|
+
handle(query: ModelQueryBuilderContract<Model>, isDescending: boolean, property: string): void;
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/utils/helpers.ts
|
|
2
|
+
const strBeforeLast = (subject, search) => {
|
|
3
|
+
if (search === "") return subject;
|
|
4
|
+
const pos = subject.lastIndexOf(search);
|
|
5
|
+
if (pos === -1) return subject;
|
|
6
|
+
return subject.slice(0, Math.max(0, pos));
|
|
7
|
+
};
|
|
8
|
+
const strAfterLast = (subject, search) => {
|
|
9
|
+
if (search === "") return subject;
|
|
10
|
+
const pos = subject.lastIndexOf(search);
|
|
11
|
+
if (pos === -1) return subject;
|
|
12
|
+
return subject.slice(pos + search.length);
|
|
13
|
+
};
|
|
14
|
+
export { strAfterLast, strBeforeLast };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolveVine } from "./vine_resolver.js";
|
|
2
|
+
import app from "@adonisjs/core/services/app";
|
|
3
|
+
import { RuntimeException } from "@adonisjs/core/exceptions";
|
|
4
|
+
// src/validation/schema_builder.ts
|
|
5
|
+
const isVineValidator = (rules) => {
|
|
6
|
+
return typeof rules?.validate === "function";
|
|
7
|
+
};
|
|
8
|
+
const isVineObject = (rules) => {
|
|
9
|
+
return typeof rules.getProperties === "function";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Filters are omitted from the URL most of the time, so every filter rule is made
|
|
13
|
+
* optional while keeping its type and constraints when the value is present.
|
|
14
|
+
*/
|
|
15
|
+
const toOptional = (rule) => {
|
|
16
|
+
const schema = rule;
|
|
17
|
+
return typeof schema.optional === "function" ? schema.optional() : rule;
|
|
18
|
+
};
|
|
19
|
+
const buildQueryValidator = async (rules, filterRules = {}) => {
|
|
20
|
+
if (isVineValidator(rules)) return rules;
|
|
21
|
+
const vine = await resolveVine();
|
|
22
|
+
const filterKey = app.config.get("apiquery.parameters.filter", "filter");
|
|
23
|
+
const filterProperties = Object.fromEntries(Object.entries(filterRules).map(([name, rule]) => [name, toOptional(rule)]));
|
|
24
|
+
const hasFilterRules = Object.keys(filterProperties).length > 0;
|
|
25
|
+
if (rules && isVineObject(rules)) {
|
|
26
|
+
const properties = rules.getProperties();
|
|
27
|
+
if (!hasFilterRules || Object.hasOwn(properties, filterKey)) return vine.create(rules);
|
|
28
|
+
return vine.create({
|
|
29
|
+
...properties,
|
|
30
|
+
[filterKey]: vine.object(filterProperties).optional()
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const properties = { ...rules };
|
|
34
|
+
if (hasFilterRules && !Object.hasOwn(properties, filterKey)) properties[filterKey] = vine.object(filterProperties).optional();
|
|
35
|
+
if (Object.keys(properties).length === 0) throw new RuntimeException("No validation rules defined for the API query request");
|
|
36
|
+
return vine.create(properties);
|
|
37
|
+
};
|
|
38
|
+
export { buildQueryValidator };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { VineNotInstalledException } from "../exceptions/vine_not_installed.js";
|
|
2
|
+
// src/validation/vine_resolver.ts
|
|
3
|
+
let cachedVine;
|
|
4
|
+
const resolveVine = async () => {
|
|
5
|
+
if (cachedVine) return cachedVine;
|
|
6
|
+
try {
|
|
7
|
+
cachedVine = (await import("@vinejs/vine")).default;
|
|
8
|
+
return cachedVine;
|
|
9
|
+
} catch {
|
|
10
|
+
throw new VineNotInstalledException();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export { resolveVine };
|
package/build/stubs/main.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const stubsRoot: string;
|
|
1
|
+
export declare const stubsRoot: string;
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AllowedInclude } from "./src/allowed_include.js";
|
|
2
|
+
import { AllowedSort } from "./src/allowed_sort.js";
|
|
3
|
+
import { ApiQueryBuilderRequest } from "./src/api_query_builder_request.js";
|
|
4
|
+
import { BaseFilterable } from "./src/base_filterable.js";
|
|
5
|
+
import { AllowedFilter } from "./src/allowed_filter.js";
|
|
6
|
+
import { HttpRequest } from "@adonisjs/core/http";
|
|
7
|
+
import { ExtractScopes, LucidModel, ModelAttributes, ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
|
|
8
|
+
import { StrictValuesWithoutRaw } from "@adonisjs/lucid/types/querybuilder";
|
|
9
|
+
import { SchemaTypes } from "@vinejs/vine/types";
|
|
10
|
+
import { ExtractModelRelations } from "@adonisjs/lucid/types/relations";
|
|
11
|
+
import { Vine, VineObject } from "@vinejs/vine";
|
|
12
|
+
export type QueryValidator = ReturnType<Vine['create']>;
|
|
13
|
+
/**
|
|
14
|
+
* Validation rules accepted for an API query request: a compiled `vine.create(...)`,
|
|
15
|
+
* a `vine.object(...)` schema, or a literal object of Vine schemas.
|
|
16
|
+
*/
|
|
17
|
+
export type ValidationRules = QueryValidator | VineObject<Record<string, SchemaTypes>, unknown, unknown, unknown> | Record<string, SchemaTypes>;
|
|
18
|
+
export interface ApiQueryConfigParameters {
|
|
19
|
+
include?: string;
|
|
20
|
+
filter?: string;
|
|
21
|
+
sort?: string;
|
|
22
|
+
fields?: string;
|
|
23
|
+
append?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ApiQueryConfig {
|
|
26
|
+
/**
|
|
27
|
+
* By default the package will use the `include`, `filter`, `sort`
|
|
28
|
+
* and `fields` query parameters as described in the readme.
|
|
29
|
+
*
|
|
30
|
+
* You can customize these query string parameters here.
|
|
31
|
+
*/
|
|
32
|
+
parameters?: ApiQueryConfigParameters;
|
|
33
|
+
/**
|
|
34
|
+
* Related model counts are included using the relationship name suffixed with this string.
|
|
35
|
+
* For example: GET /users?include=postsCount
|
|
36
|
+
*/
|
|
37
|
+
countSuffix?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Related model exists are included using the relationship name suffixed with this string.
|
|
40
|
+
* For example: GET /users?include=postsExists
|
|
41
|
+
*/
|
|
42
|
+
existsSuffix?: string;
|
|
43
|
+
/**
|
|
44
|
+
* By default the package will throw an `InvalidFilterQuery` exception when a filter in the
|
|
45
|
+
* URL is not allowed in the `allowedFilters()` method.
|
|
46
|
+
*/
|
|
47
|
+
disableInvalidFilterQueryException?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* By default the package will throw an `InvalidSortQuery` exception when a sort in the
|
|
50
|
+
* URL is not allowed in the `allowedSorts()` method.
|
|
51
|
+
*/
|
|
52
|
+
disableInvalidSortQueryException?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* By default the package will throw an `InvalidIncludeQuery` exception when an include in the
|
|
55
|
+
* URL is not allowed in the `allowedIncludes()` method.
|
|
56
|
+
*/
|
|
57
|
+
disableInvalidIncludesQueryException?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* By default, the package expects relationship names to be camel case plural when using fields[relationship].
|
|
60
|
+
* For example, fetching the id and name for a userOwner relation would look like this:
|
|
61
|
+
* GET /users?fields[userOwner]=id,name
|
|
62
|
+
*
|
|
63
|
+
* Set this to `true` if you don't want that and keep the requested relationship names as-is and allows you to
|
|
64
|
+
* request the fields using a snake case relationship name:
|
|
65
|
+
* GET /users?fields[user_owner]=id,name
|
|
66
|
+
*/
|
|
67
|
+
convertRelationNamesToSnakeCasePlural?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* By default, the package expects relationship names to be camel case plural when using fields[relationship].
|
|
70
|
+
* For example, fetching the id and name for a userOwner relation would look like this:
|
|
71
|
+
* GET /users?fields[userOwner]=id,name
|
|
72
|
+
*
|
|
73
|
+
* Set this to one of `snake_case`, `camelCase` or `none` if you want to enable table name resolution in addition to the relation name resolution
|
|
74
|
+
* GET /users?include=top_orders&fields[orders]=id,name
|
|
75
|
+
*/
|
|
76
|
+
convertRelationTableNameStrategy?: 'snake_case' | 'camelCase' | 'none' | false;
|
|
77
|
+
/**
|
|
78
|
+
* By default, the package expects the field names to match the database names
|
|
79
|
+
* For example, fetching the field named firstName would look like this:
|
|
80
|
+
* GET /users?fields=firstName
|
|
81
|
+
*
|
|
82
|
+
* Set this to `true` if you want to convert the firstName into first_name for the underlying query
|
|
83
|
+
*/
|
|
84
|
+
convertFieldNamesToSnakeCase?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* By default, the package will automatically validate the request before query execution
|
|
87
|
+
* when validation rules are configured.
|
|
88
|
+
*
|
|
89
|
+
* Set this to `false` if you prefer manual validation via `validate()`.
|
|
90
|
+
*/
|
|
91
|
+
autoValidate?: boolean;
|
|
92
|
+
}
|
|
93
|
+
export type ResolvedApiQueryConfig = Required<ApiQueryConfig> & {
|
|
94
|
+
parameters: Required<ApiQueryConfigParameters>;
|
|
95
|
+
};
|
|
96
|
+
export type EnumType<T> = T[keyof T];
|
|
97
|
+
export interface Sort<Model extends LucidModel> {
|
|
98
|
+
handle(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, isDescending: boolean, property: string): void;
|
|
99
|
+
}
|
|
100
|
+
export interface Filter<Model extends LucidModel> {
|
|
101
|
+
handle(query: ModelQueryBuilderContract<Model, InstanceType<Model>>, value: StrictValuesWithoutRaw | null, property: string): void;
|
|
102
|
+
}
|
|
103
|
+
export interface Include<ParentModel extends LucidModel> {
|
|
104
|
+
handle(query: ModelQueryBuilderContract<ParentModel, InstanceType<ParentModel>>, include: string): void;
|
|
105
|
+
}
|
|
106
|
+
export type ExtractKeys<T> = T extends object ? { [K in keyof T & string]: K; }[keyof T & string] : never;
|
|
107
|
+
export type ExtractKeysWithSort<T> = T extends object ? { [K in keyof T & string]: `-${K}`; }[keyof T & string] : never;
|
|
108
|
+
export type SortUnionKeyParams<Model extends LucidModel> = ExtractKeys<ModelAttributes<InstanceType<Model>>> | ExtractKeysWithSort<ModelAttributes<InstanceType<Model>>>;
|
|
109
|
+
export type ScopeUnionKeyParams<Model extends LucidModel> = [keyof ExtractScopes<Model>] extends [never] ? string : HintedString<keyof ExtractScopes<Model>>;
|
|
110
|
+
export type HintedString<KnownValues> = KnownValues extends string ? (string & {}) | KnownValues : never;
|
|
111
|
+
export type FilterableConstructor<Model extends LucidModel = LucidModel> = new (request?: HttpRequest | ApiQueryBuilderRequest) => BaseFilterable<Model>;
|
|
112
|
+
export type FilterableThunk<Model extends LucidModel = LucidModel> = () => FilterableConstructor<Model>;
|
|
113
|
+
export type FilterableTarget<Model extends LucidModel = LucidModel> = FilterableConstructor<Model> | FilterableThunk<Model>;
|
|
114
|
+
export type FilterItem<Model extends LucidModel = LucidModel> = AllowedFilter<Model> | HintedString<ExtractKeys<ModelAttributes<InstanceType<Model>>>> | (string & {});
|
|
115
|
+
export type SortItem<Model extends LucidModel = LucidModel> = AllowedSort<Model> | HintedString<SortUnionKeyParams<Model>> | (string & {});
|
|
116
|
+
export type IncludeItem<Model extends LucidModel = LucidModel> = AllowedInclude<Model> | AllowedInclude<Model>[] | HintedString<ExtractModelRelations<InstanceType<Model>>> | (string & {});
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|