@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 CHANGED
@@ -49,19 +49,48 @@ export default class UserBravo extends LucidBravo<typeof User> {
49
49
  }
50
50
  ```
51
51
 
52
- Use the class in a controller or service with the query builder and request params:
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 bravo = new UserBravo(User.query(), {
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 users = await bravo.apply()
64
- const result = await bravo.paginate()
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
- - If you want request validation, create a Vine schema in your app that matches the same query shape.
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
@@ -1,2 +1,3 @@
1
1
  export { configure } from './configure.ts';
2
2
  export { stubsRoot } from './stubs/main.ts';
3
+ export { LucidBravo } from './src/lucid_bravo.ts';
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
- export { configure, stubsRoot };
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: ModelQueryBuilderContract<T>, params: BravoParams);
12
- static build<T extends LucidModel, B extends LucidBravo<T>>(this: {
13
- new (query: ModelQueryBuilderContract<T>, params: BravoParams): B;
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 type {{ entity.name }} from '{{ modelImportPath }}'
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.1",
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",