@kadirgun/lucid-bravo 0.0.1 → 0.0.2
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 +39 -9
- package/build/index.d.ts +1 -0
- package/build/index.js +125 -1
- package/build/src/lucid_bravo.d.ts +5 -4
- package/build/stubs/make/bravos/main.stub +3 -1
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -49,19 +49,48 @@ export default class UserBravo extends LucidBravo<typeof User> {
|
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
You can create a Bravo instance in a few different ways, depending on whether you already have a Lucid query builder or want Bravo to create one from the model declared in the subclass:
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
const
|
|
56
|
-
sort: { field: 'name', order: 'asc' },
|
|
57
|
-
include: ['posts'],
|
|
55
|
+
const bravoA = new UserBravo({
|
|
58
56
|
limit: 20,
|
|
59
|
-
page: 1,
|
|
60
|
-
name: 'Alice',
|
|
61
57
|
})
|
|
62
58
|
|
|
63
|
-
const
|
|
64
|
-
|
|
59
|
+
const bravoB = new UserBravo(
|
|
60
|
+
{
|
|
61
|
+
limit: 20,
|
|
62
|
+
},
|
|
63
|
+
User.query()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const bravoC = UserBravo.build(
|
|
67
|
+
{
|
|
68
|
+
limit: 20,
|
|
69
|
+
},
|
|
70
|
+
User.query()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// default query and params
|
|
74
|
+
const bravoD = UserBravo.build()
|
|
75
|
+
const bravoE = new UserBravo()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use the class in a controller with `bravoValidator` to validate the common query params before passing them to Bravo:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
82
|
+
import User from '#models/user'
|
|
83
|
+
import UserBravo from '#bravos/user_bravo'
|
|
84
|
+
import { bravoValidator } from '@kadirgun/lucid-bravo/validators'
|
|
85
|
+
|
|
86
|
+
export default class UsersController {
|
|
87
|
+
public async index({ request }: HttpContext) {
|
|
88
|
+
const params = await request.validateUsing(bravoValidator)
|
|
89
|
+
const bravo = new UserBravo(params, User.query())
|
|
90
|
+
|
|
91
|
+
return bravo.paginate()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
65
94
|
```
|
|
66
95
|
|
|
67
96
|
## Query params
|
|
@@ -80,7 +109,8 @@ For example, `first_name` maps to a `firstName()` method.
|
|
|
80
109
|
|
|
81
110
|
- `LucidBravo` expects to run inside an active HTTP context.
|
|
82
111
|
- Only relations returned by `getAllowedIncludes()` are preloaded.
|
|
83
|
-
-
|
|
112
|
+
- For the common query shape, you can reuse `bravoValidator` from `@kadirgun/lucid-bravo/validators`.
|
|
113
|
+
- If you also need model-specific filters, add a separate Vine schema in your app and merge the validated values before creating the Bravo instance.
|
|
84
114
|
|
|
85
115
|
## Authorization
|
|
86
116
|
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { configure } from "./configure.js";
|
|
2
|
+
import stringHelpers from "@adonisjs/core/helpers/string";
|
|
3
|
+
import { HttpContext } from "@adonisjs/core/http";
|
|
2
4
|
//#region stubs/main.ts
|
|
3
5
|
/**
|
|
4
6
|
* Path to the root directory where the stubs are stored. We use
|
|
@@ -6,4 +8,126 @@ import { configure } from "./configure.js";
|
|
|
6
8
|
*/
|
|
7
9
|
const stubsRoot = import.meta.dirname;
|
|
8
10
|
//#endregion
|
|
9
|
-
|
|
11
|
+
//#region src/lucid_bravo.ts
|
|
12
|
+
var LucidBravo = class {
|
|
13
|
+
$model;
|
|
14
|
+
$query;
|
|
15
|
+
$params;
|
|
16
|
+
$http;
|
|
17
|
+
$countQuery;
|
|
18
|
+
defaultLimit = 20;
|
|
19
|
+
defaultSort = null;
|
|
20
|
+
constructor(params, query) {
|
|
21
|
+
this.$params = params || {};
|
|
22
|
+
this.$http = HttpContext.getOrFail();
|
|
23
|
+
if (query) {
|
|
24
|
+
this.$model = query.model;
|
|
25
|
+
this.$query = query;
|
|
26
|
+
this.$countQuery = this.$query.clone();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
static build(params, query) {
|
|
30
|
+
return new this(params, query);
|
|
31
|
+
}
|
|
32
|
+
resolveQuery() {
|
|
33
|
+
if (this.$query) return this.$query;
|
|
34
|
+
if (this.$model) {
|
|
35
|
+
this.$query = this.$model.query();
|
|
36
|
+
this.$countQuery = this.$query.clone();
|
|
37
|
+
return this.$query;
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Either a query must be provided or $model must be set in the subclass");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return a whitelist of sortable columns
|
|
43
|
+
*/
|
|
44
|
+
getSortable() {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return a whitelist of allowed relations for preload include
|
|
49
|
+
*/
|
|
50
|
+
getAllowedIncludes() {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Main entry point to apply all filters, includes, sorting and pagination
|
|
55
|
+
*/
|
|
56
|
+
async apply() {
|
|
57
|
+
this.resolveQuery();
|
|
58
|
+
await this.applyFilters();
|
|
59
|
+
await this.applyIncludes();
|
|
60
|
+
await this.applySorting();
|
|
61
|
+
await this.applyPagination();
|
|
62
|
+
return this.$query;
|
|
63
|
+
}
|
|
64
|
+
async count() {
|
|
65
|
+
this.resolveQuery();
|
|
66
|
+
const result = await this.$countQuery.count("* as total").firstOrFail();
|
|
67
|
+
return Number(result.$extras.total);
|
|
68
|
+
}
|
|
69
|
+
async paginate() {
|
|
70
|
+
return {
|
|
71
|
+
items: await this.apply(),
|
|
72
|
+
total: await this.count()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Automatically call methods that match camelCase version of snake_case params
|
|
77
|
+
*/
|
|
78
|
+
async applyFilters() {
|
|
79
|
+
this.resolveQuery();
|
|
80
|
+
for (const [key, value] of Object.entries(this.$params)) {
|
|
81
|
+
if ([
|
|
82
|
+
"page",
|
|
83
|
+
"limit",
|
|
84
|
+
"sort"
|
|
85
|
+
].includes(key)) continue;
|
|
86
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
87
|
+
const methodName = stringHelpers.camelCase(key);
|
|
88
|
+
if (!(methodName in this)) continue;
|
|
89
|
+
const method = this[methodName];
|
|
90
|
+
if (typeof method !== "function") throw new Error(`Expected ${methodName} to be a method on ${this.constructor.name}`);
|
|
91
|
+
await method.call(this, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Apply preload include relations based on allowlist
|
|
96
|
+
*/
|
|
97
|
+
async applyIncludes() {
|
|
98
|
+
const query = this.resolveQuery();
|
|
99
|
+
const includes = this.$params.include;
|
|
100
|
+
if (!Array.isArray(includes) || includes.length === 0) return;
|
|
101
|
+
const allowed = this.getAllowedIncludes();
|
|
102
|
+
for (const relation of includes) {
|
|
103
|
+
if (!allowed.includes(relation)) continue;
|
|
104
|
+
query.preload(relation);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Apply sorting based on sort[field] and sort[order] params
|
|
109
|
+
*/
|
|
110
|
+
async applySorting() {
|
|
111
|
+
const query = this.resolveQuery();
|
|
112
|
+
const sort = this.$params.sort;
|
|
113
|
+
const sortable = this.getSortable();
|
|
114
|
+
let field = sort?.field;
|
|
115
|
+
let order = sort?.order || "asc";
|
|
116
|
+
if (!field && this.defaultSort) {
|
|
117
|
+
field = this.defaultSort.field;
|
|
118
|
+
order = this.defaultSort.order;
|
|
119
|
+
}
|
|
120
|
+
if (field && sortable.includes(field)) query.orderBy(field, order);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Apply simple limit and offset pagination
|
|
124
|
+
*/
|
|
125
|
+
async applyPagination() {
|
|
126
|
+
const query = this.resolveQuery();
|
|
127
|
+
const limit = Number(this.$params.limit) || this.defaultLimit;
|
|
128
|
+
const offset = ((Number(this.$params.page) || 1) - 1) * limit;
|
|
129
|
+
if (limit > 0) query.limit(limit).offset(offset);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
export { LucidBravo, configure, stubsRoot };
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { LucidModel, ModelAttributes, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
|
2
2
|
import type { BravoParams, BravoSortOption } from './types.ts';
|
|
3
3
|
import { HttpContext } from '@adonisjs/core/http';
|
|
4
|
+
import type { Constructor } from '@adonisjs/core/types/common';
|
|
4
5
|
export declare abstract class LucidBravo<T extends LucidModel> {
|
|
6
|
+
protected $model?: T;
|
|
5
7
|
protected $query: ModelQueryBuilderContract<T>;
|
|
6
8
|
protected $params: BravoParams;
|
|
7
9
|
protected $http: HttpContext;
|
|
8
10
|
protected $countQuery: ModelQueryBuilderContract<T>;
|
|
9
11
|
protected defaultLimit: number;
|
|
10
12
|
protected defaultSort: BravoSortOption | null;
|
|
11
|
-
constructor(query
|
|
12
|
-
static build<T extends LucidModel, B extends LucidBravo<T>>(this:
|
|
13
|
-
|
|
14
|
-
}, query: ModelQueryBuilderContract<T>, params: BravoParams): B;
|
|
13
|
+
constructor(params?: BravoParams, query?: ModelQueryBuilderContract<T>);
|
|
14
|
+
static build<T extends LucidModel, B extends LucidBravo<T>>(this: Constructor<B>, params?: BravoParams, query?: ModelQueryBuilderContract<T>): B;
|
|
15
|
+
protected resolveQuery(): ModelQueryBuilderContract<T, InstanceType<T>>;
|
|
15
16
|
/**
|
|
16
17
|
* Return a whitelist of sortable columns
|
|
17
18
|
*/
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
})
|
|
9
9
|
}}}
|
|
10
10
|
import { LucidBravo } from '@kadirgun/lucid-bravo'
|
|
11
|
-
import
|
|
11
|
+
import {{ entity.name }} from '{{ modelImportPath }}'
|
|
12
12
|
import type { ModelAttributes } from '@adonisjs/lucid/types/model'
|
|
13
13
|
|
|
14
14
|
export default class {{ modelName }}Bravo extends LucidBravo<typeof {{ entity.name }}> {
|
|
15
|
+
protected $model = {{ entity.name }}
|
|
16
|
+
|
|
15
17
|
public override getSortable(): string[] {
|
|
16
18
|
return []
|
|
17
19
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kadirgun/lucid-bravo",
|
|
3
3
|
"description": "A powerful, fluent query builder for AdonisJS Lucid to handle filtering, sorting and pagination in a single flow",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.0.0"
|
|
7
7
|
},
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
"main": "build/index.js",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./build/index.js",
|
|
17
|
-
"./types": "./build/src/types.js"
|
|
17
|
+
"./types": "./build/src/types.js",
|
|
18
|
+
"./validators": "./build/validators.js",
|
|
19
|
+
"./commands": {
|
|
20
|
+
"import": "./build/commands/main.js",
|
|
21
|
+
"types": "./build/commands/main.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./commands/*": {
|
|
24
|
+
"import": "./build/commands/*.js",
|
|
25
|
+
"types": "./build/commands/*.d.ts"
|
|
26
|
+
}
|
|
18
27
|
},
|
|
19
28
|
"scripts": {
|
|
20
29
|
"copy:templates": "copyfiles \"stubs/**/*.stub\" build",
|