@devstroupe/devkit-nest 1.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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @devstroupe/devkit-nest
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@devstroupe/devkit-nest?color=red)](https://www.npmjs.com/package/@devstroupe/devkit-nest)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
5
+
6
+ Classes base para **NestJS + TypeORM** do DT-DevKit. Provê repositórios genéricos paginados e controladores CRUD padronizados.
7
+
8
+ ## Instalação
9
+
10
+ ```bash
11
+ npm install @devstroupe/devkit-nest
12
+ ```
13
+
14
+ ### Peer Dependencies
15
+ ```bash
16
+ npm install @nestjs/common @nestjs/core @nestjs/swagger typeorm reflect-metadata
17
+ ```
18
+
19
+ ## Classes disponíveis
20
+
21
+ ### `BaseRepository<Entity>`
22
+
23
+ Repositório TypeORM com parser automático de paginação e filtros. Todos os repositórios gerados pelo `devstroupe generate crud` herdam desta classe.
24
+
25
+ ```typescript
26
+ import { BaseRepository } from '@devstroupe/devkit-nest';
27
+ import { InjectRepository } from '@nestjs/typeorm';
28
+ import { Repository } from 'typeorm';
29
+ import { Cabin } from '../domain/entities/cabin.entity';
30
+
31
+ @Injectable()
32
+ export class CabinRepository extends BaseRepository<Cabin> {
33
+ constructor(
34
+ @InjectRepository(Cabin)
35
+ private readonly repo: Repository<Cabin>
36
+ ) {
37
+ super(repo);
38
+ }
39
+ }
40
+ ```
41
+
42
+ Métodos herdados:
43
+ - `findMany(query)` — paginação automática com filtros
44
+ - `findById(id)` — busca por ID com validação
45
+
46
+ ### `BaseCrudController<Entity, CreateDto, UpdateDto>`
47
+
48
+ Controller NestJS com endpoints CRUD padronizados e documentação Swagger automática.
49
+
50
+ ```typescript
51
+ import { BaseCrudController } from '@devstroupe/devkit-nest';
52
+
53
+ @Controller('cabins')
54
+ export class CabinController extends BaseCrudController(
55
+ Cabin, CreateCabinDto, UpdateCabinDto
56
+ ) {}
57
+ ```
58
+
59
+ Endpoints gerados automaticamente:
60
+ - `GET /` — listagem paginada com filtros via query params
61
+ - `GET /:id` — busca por ID
62
+ - `POST /` — criação
63
+ - `PATCH /:id` — atualização parcial
64
+ - `DELETE /:id` — remoção
65
+
66
+ ## Regras de governança
67
+
68
+ A regra `use-standard-devkit-queries` do linter exige que:
69
+ - Todos os **Controllers CRUD** herdem de `BaseCrudController`
70
+ - Todos os **Repositories** herdem de `BaseRepository`
71
+
72
+ Execute `devstroupe lint:rules` para verificar conformidade.
@@ -0,0 +1,16 @@
1
+ import { Repository, SelectQueryBuilder, ObjectLiteral } from 'typeorm';
2
+ import { IPaginator, IFilterPagination } from '@devstroupe/devkit-core';
3
+ import { IQueryBuilderOptions } from './typeorm-query.builder';
4
+ export declare abstract class BaseRepository<Entity extends ObjectLiteral, FilterDto extends IFilterPagination = IFilterPagination> {
5
+ protected readonly repository: Repository<Entity>;
6
+ protected allowedFilters?: string[];
7
+ protected allowedSorts?: string[];
8
+ protected searchableFields?: string[];
9
+ constructor(repository: Repository<Entity>);
10
+ create(data: any): Promise<Entity>;
11
+ update(id: any, data: any): Promise<Entity>;
12
+ delete(id: any): Promise<void>;
13
+ findOne(id: any): Promise<Entity | null>;
14
+ findMany(filter: FilterDto): Promise<IPaginator<Entity>>;
15
+ protected paginate(qb: SelectQueryBuilder<Entity>, filter: any, options?: IQueryBuilderOptions): Promise<IPaginator<Entity>>;
16
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseRepository = void 0;
4
+ const typeorm_query_builder_1 = require("./typeorm-query.builder");
5
+ class BaseRepository {
6
+ repository;
7
+ allowedFilters;
8
+ allowedSorts;
9
+ searchableFields;
10
+ constructor(repository) {
11
+ this.repository = repository;
12
+ }
13
+ async create(data) {
14
+ const entity = this.repository.create(data);
15
+ return await this.repository.save(entity);
16
+ }
17
+ async update(id, data) {
18
+ await this.repository.save({ id, ...data });
19
+ const updated = await this.findOne(id);
20
+ if (!updated) {
21
+ throw new Error(`Entity with ID ${id} not found after update`);
22
+ }
23
+ return updated;
24
+ }
25
+ async delete(id) {
26
+ await this.repository.softDelete(id);
27
+ }
28
+ async findOne(id) {
29
+ return await this.repository.findOne({ where: { id } });
30
+ }
31
+ async findMany(filter) {
32
+ const alias = this.repository.metadata.name.toLowerCase();
33
+ const queryBuilder = this.repository.createQueryBuilder(alias);
34
+ return await this.paginate(queryBuilder, filter, {
35
+ allowedFilters: this.allowedFilters,
36
+ allowedSorts: this.allowedSorts,
37
+ searchableFields: this.searchableFields,
38
+ });
39
+ }
40
+ async paginate(qb, filter, options) {
41
+ const queryBuilder = typeorm_query_builder_1.TypeOrmQueryBuilder.apply(qb, filter, options);
42
+ const [items, totalItems] = await queryBuilder.getManyAndCount();
43
+ const page = Number(filter.page) || 1;
44
+ let limit = 10;
45
+ if (filter.limit === 'todos' || filter.limit === 'all') {
46
+ limit = totalItems;
47
+ }
48
+ else if (filter.limit) {
49
+ limit = Number(filter.limit);
50
+ }
51
+ return {
52
+ items,
53
+ meta: {
54
+ totalItems,
55
+ currentPage: page,
56
+ itemsPerPage: limit === 0 ? totalItems : limit,
57
+ itemCount: items.length,
58
+ totalPage: limit === 0 ? 1 : Math.ceil(totalItems / limit),
59
+ },
60
+ };
61
+ }
62
+ }
63
+ exports.BaseRepository = BaseRepository;
@@ -0,0 +1,32 @@
1
+ import { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
2
+ export declare const CONDITION_SUFFIXES: {
3
+ ARRAY: string;
4
+ DATE_START: string;
5
+ DATE_END: string;
6
+ IN: string;
7
+ NOT_IN: string;
8
+ BOOL: string;
9
+ GT: string;
10
+ GTE: string;
11
+ LT: string;
12
+ LTE: string;
13
+ NULL: string;
14
+ LIKE: string;
15
+ ILIKE: string;
16
+ NOT_LIKE: string;
17
+ NOT_ILIKE: string;
18
+ };
19
+ export declare class ConditionHandlers {
20
+ static handleArray<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string): SelectQueryBuilder<QR>;
21
+ static handleDate<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string, isEndDate: boolean): SelectQueryBuilder<QR>;
22
+ static handleInCondition<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string, isNotIn?: boolean): SelectQueryBuilder<QR>;
23
+ static handleBoolean<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string): SelectQueryBuilder<QR>;
24
+ static handleComparison<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string, operator: '>' | '>=' | '<' | '<='): SelectQueryBuilder<QR>;
25
+ static handleNull<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string): SelectQueryBuilder<QR>;
26
+ static handleDefault<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string): SelectQueryBuilder<QR>;
27
+ static handleLike<QR extends ObjectLiteral>(qb: SelectQueryBuilder<QR>, key: string, value: any, tableName: string, options?: {
28
+ caseSensitive?: boolean;
29
+ not?: boolean;
30
+ }): SelectQueryBuilder<QR>;
31
+ }
32
+ export declare const factoryWhere: <QR extends ObjectLiteral, T>(queryBuilder: SelectQueryBuilder<QR>, where: T) => SelectQueryBuilder<QR>;
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.factoryWhere = exports.ConditionHandlers = exports.CONDITION_SUFFIXES = void 0;
4
+ exports.CONDITION_SUFFIXES = {
5
+ ARRAY: '_$array',
6
+ DATE_START: '_$date_start',
7
+ DATE_END: '_$date_end',
8
+ IN: '_$in',
9
+ NOT_IN: '_$not_in',
10
+ BOOL: '_$bool',
11
+ GT: '_$gt',
12
+ GTE: '_$gte',
13
+ LT: '_$lt',
14
+ LTE: '_$lte',
15
+ NULL: '_$null',
16
+ LIKE: '_$like',
17
+ ILIKE: '_$ilike',
18
+ NOT_LIKE: '_$not_like',
19
+ NOT_ILIKE: '_$not_ilike',
20
+ };
21
+ function createParamName(key) {
22
+ return key.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '') + Math.random().toString(36).slice(2, 6);
23
+ }
24
+ function StringToArray(value) {
25
+ if (Array.isArray(value))
26
+ return value;
27
+ if (typeof value === 'string') {
28
+ return value.split(',').map((v) => v.trim());
29
+ }
30
+ return [value];
31
+ }
32
+ function BooleanString(value) {
33
+ if (typeof value === 'boolean')
34
+ return value;
35
+ if (value === 'true' || value === '1' || value === 1)
36
+ return true;
37
+ return false;
38
+ }
39
+ function formatDateTime(value, isEndDate) {
40
+ const dateStr = typeof value === 'object' && value instanceof Date ? value.toISOString() : String(value);
41
+ if (dateStr.includes('T')) {
42
+ return dateStr;
43
+ }
44
+ // Se for apenas data YYYY-MM-DD
45
+ return isEndDate ? `${dateStr} 23:59:59` : `${dateStr} 00:00:00`;
46
+ }
47
+ class ConditionHandlers {
48
+ static handleArray(qb, key, value, tableName) {
49
+ const originalKey = key.replace(exports.CONDITION_SUFFIXES.ARRAY, '');
50
+ const paramName = `${createParamName(key)}_array`;
51
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
52
+ const condition = `${prefix}${originalKey} IN (:${paramName})`;
53
+ return qb.andWhere(condition, {
54
+ [paramName]: StringToArray(value),
55
+ });
56
+ }
57
+ static handleDate(qb, key, value, tableName, isEndDate) {
58
+ const originalKey = key.replace(exports.CONDITION_SUFFIXES.DATE_END, '').replace(exports.CONDITION_SUFFIXES.DATE_START, '');
59
+ const paramName = `${createParamName(key)}_date`;
60
+ const operator = isEndDate ? '<=' : '>=';
61
+ const formattedDate = formatDateTime(value, isEndDate);
62
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
63
+ const condition = `${prefix}${originalKey} ${operator} :${paramName}`;
64
+ return qb.andWhere(condition, { [paramName]: formattedDate });
65
+ }
66
+ static handleInCondition(qb, key, value, tableName, isNotIn = false) {
67
+ const originalKey = key.replace(isNotIn ? exports.CONDITION_SUFFIXES.NOT_IN : exports.CONDITION_SUFFIXES.IN, '');
68
+ const paramName = createParamName(key);
69
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
70
+ const arr = StringToArray(value).map((v) => (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : v));
71
+ const condition = `${prefix}${originalKey} ${isNotIn ? 'NOT IN' : 'IN'} (:...${paramName})`;
72
+ return qb.andWhere(condition, { [paramName]: arr });
73
+ }
74
+ static handleBoolean(qb, key, value, tableName) {
75
+ const originalKey = key.replace(exports.CONDITION_SUFFIXES.BOOL, '');
76
+ const paramName = createParamName(key);
77
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
78
+ const condition = `${prefix}${originalKey} = :${paramName}`;
79
+ return qb.andWhere(condition, { [paramName]: BooleanString(value) });
80
+ }
81
+ static handleComparison(qb, key, value, tableName, operator) {
82
+ const suffixMap = {
83
+ '>': exports.CONDITION_SUFFIXES.GT,
84
+ '>=': exports.CONDITION_SUFFIXES.GTE,
85
+ '<': exports.CONDITION_SUFFIXES.LT,
86
+ '<=': exports.CONDITION_SUFFIXES.LTE,
87
+ };
88
+ const originalKey = key.replace(suffixMap[operator], '');
89
+ const paramName = createParamName(key);
90
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
91
+ const condition = `${prefix}${originalKey} ${operator} :${paramName}`;
92
+ return qb.andWhere(condition, { [paramName]: value });
93
+ }
94
+ static handleNull(qb, key, value, tableName) {
95
+ const originalKey = key.replace(exports.CONDITION_SUFFIXES.NULL, '');
96
+ const isNull = BooleanString(value);
97
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
98
+ const condition = `${prefix}${originalKey} IS ${isNull ? 'NULL' : 'NOT NULL'}`;
99
+ return qb.andWhere(condition);
100
+ }
101
+ static handleDefault(qb, key, value, tableName) {
102
+ const paramName = createParamName(key);
103
+ if (key.includes('.')) {
104
+ return qb.andWhere(`${key} = :${paramName}`, { [paramName]: value });
105
+ }
106
+ return qb.andWhere(`${tableName}.${key} = :${paramName}`, { [paramName]: value });
107
+ }
108
+ static handleLike(qb, key, value, tableName, options = {}) {
109
+ const { caseSensitive = true, not = false } = options;
110
+ const originalKey = key
111
+ .replace(exports.CONDITION_SUFFIXES.LIKE, '')
112
+ .replace(exports.CONDITION_SUFFIXES.ILIKE, '')
113
+ .replace(exports.CONDITION_SUFFIXES.NOT_LIKE, '')
114
+ .replace(exports.CONDITION_SUFFIXES.NOT_ILIKE, '');
115
+ const paramName = createParamName(key);
116
+ const dbType = qb.connection.options.type;
117
+ const isMySQL = dbType === 'mysql' || dbType === 'mariadb';
118
+ const prefix = key.includes('.') ? '' : `${tableName}.`;
119
+ let condition;
120
+ let paramValue;
121
+ if (!caseSensitive && isMySQL) {
122
+ const operator = not ? 'NOT LIKE' : 'LIKE';
123
+ condition = `LOWER(${prefix}${originalKey}) ${operator} :${paramName}`;
124
+ paramValue = `%${String(value).toLowerCase()}%`;
125
+ }
126
+ else {
127
+ const operator = caseSensitive ? (not ? 'NOT LIKE' : 'LIKE') : not ? 'NOT ILIKE' : 'ILIKE';
128
+ condition = `${prefix}${originalKey} ${operator} :${paramName}`;
129
+ paramValue = `%${value}%`;
130
+ }
131
+ return qb.andWhere(condition, { [paramName]: paramValue });
132
+ }
133
+ }
134
+ exports.ConditionHandlers = ConditionHandlers;
135
+ const CONDITION_HANDLERS = [
136
+ { suffix: exports.CONDITION_SUFFIXES.ARRAY, handle: (qb, k, v, t) => ConditionHandlers.handleArray(qb, k, v, t) },
137
+ { suffix: exports.CONDITION_SUFFIXES.DATE_END, handle: (qb, k, v, t) => ConditionHandlers.handleDate(qb, k, v, t, true) },
138
+ { suffix: exports.CONDITION_SUFFIXES.DATE_START, handle: (qb, k, v, t) => ConditionHandlers.handleDate(qb, k, v, t, false) },
139
+ { suffix: exports.CONDITION_SUFFIXES.IN, handle: (qb, k, v, t) => ConditionHandlers.handleInCondition(qb, k, v, t, false) },
140
+ { suffix: exports.CONDITION_SUFFIXES.NOT_IN, handle: (qb, k, v, t) => ConditionHandlers.handleInCondition(qb, k, v, t, true) },
141
+ { suffix: exports.CONDITION_SUFFIXES.BOOL, handle: (qb, k, v, t) => ConditionHandlers.handleBoolean(qb, k, v, t) },
142
+ { suffix: exports.CONDITION_SUFFIXES.GT, handle: (qb, k, v, t) => ConditionHandlers.handleComparison(qb, k, v, t, '>') },
143
+ { suffix: exports.CONDITION_SUFFIXES.GTE, handle: (qb, k, v, t) => ConditionHandlers.handleComparison(qb, k, v, t, '>=') },
144
+ { suffix: exports.CONDITION_SUFFIXES.LT, handle: (qb, k, v, t) => ConditionHandlers.handleComparison(qb, k, v, t, '<') },
145
+ { suffix: exports.CONDITION_SUFFIXES.LTE, handle: (qb, k, v, t) => ConditionHandlers.handleComparison(qb, k, v, t, '<=') },
146
+ { suffix: exports.CONDITION_SUFFIXES.NULL, handle: (qb, k, v, t) => ConditionHandlers.handleNull(qb, k, v, t) },
147
+ { suffix: exports.CONDITION_SUFFIXES.LIKE, handle: (qb, k, v, t) => ConditionHandlers.handleLike(qb, k, v, t, { caseSensitive: true, not: false }) },
148
+ { suffix: exports.CONDITION_SUFFIXES.ILIKE, handle: (qb, k, v, t) => ConditionHandlers.handleLike(qb, k, v, t, { caseSensitive: false, not: false }) },
149
+ { suffix: exports.CONDITION_SUFFIXES.NOT_LIKE, handle: (qb, k, v, t) => ConditionHandlers.handleLike(qb, k, v, t, { caseSensitive: true, not: true }) },
150
+ { suffix: exports.CONDITION_SUFFIXES.NOT_ILIKE, handle: (qb, k, v, t) => ConditionHandlers.handleLike(qb, k, v, t, { caseSensitive: false, not: true }) },
151
+ ];
152
+ const factoryWhere = (queryBuilder, where) => {
153
+ if (!where || typeof where !== 'object')
154
+ return queryBuilder;
155
+ const tableName = queryBuilder.alias;
156
+ for (const [key, value] of Object.entries(where)) {
157
+ if (value === undefined || value === null || value === '')
158
+ continue;
159
+ try {
160
+ const handler = CONDITION_HANDLERS.find((h) => key.endsWith(h.suffix));
161
+ if (handler) {
162
+ queryBuilder = handler.handle(queryBuilder, key, value, tableName);
163
+ }
164
+ else {
165
+ queryBuilder = ConditionHandlers.handleDefault(queryBuilder, key, value, tableName);
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.warn(`Erro ao processar condição para a chave "${key}":`, error);
170
+ }
171
+ }
172
+ return queryBuilder;
173
+ };
174
+ exports.factoryWhere = factoryWhere;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const node_test_1 = require("node:test");
37
+ const assert = __importStar(require("node:assert"));
38
+ const factory_where_1 = require("./factory-where");
39
+ // Mock de SelectQueryBuilder
40
+ function createMockQueryBuilder(alias, dbType = 'postgres') {
41
+ const calls = [];
42
+ const qb = {
43
+ alias,
44
+ connection: {
45
+ options: {
46
+ type: dbType
47
+ }
48
+ },
49
+ andWhere(condition, params) {
50
+ calls.push({ condition, params });
51
+ return this;
52
+ },
53
+ getCalls() {
54
+ return calls;
55
+ }
56
+ };
57
+ return qb;
58
+ }
59
+ (0, node_test_1.test)('factoryWhere should process simple equality filters', () => {
60
+ const qb = createMockQueryBuilder('cabin');
61
+ const filter = {
62
+ company_id: 5,
63
+ name: 'Cabine A'
64
+ };
65
+ const result = (0, factory_where_1.factoryWhere)(qb, filter);
66
+ const calls = result.getCalls();
67
+ assert.strictEqual(calls.length, 2);
68
+ // Verifica primeiro filtro: company_id
69
+ assert.ok(calls[0].condition.startsWith('cabin.company_id = :company_id'));
70
+ assert.strictEqual(calls[0].params[Object.keys(calls[0].params)[0]], 5);
71
+ // Verifica segundo filtro: name
72
+ assert.ok(calls[1].condition.startsWith('cabin.name = :name'));
73
+ assert.strictEqual(calls[1].params[Object.keys(calls[1].params)[0]], 'Cabine A');
74
+ });
75
+ (0, node_test_1.test)('factoryWhere should process suffixes correctly', () => {
76
+ const qb = createMockQueryBuilder('cabin');
77
+ const filter = {
78
+ 'is_active_$bool': 'true',
79
+ 'code_$like': 'TX',
80
+ 'company_id_$in': '1,2,3',
81
+ 'id_$gt': 10
82
+ };
83
+ const result = (0, factory_where_1.factoryWhere)(qb, filter);
84
+ const calls = result.getCalls();
85
+ assert.strictEqual(calls.length, 4);
86
+ // 1. is_active_$bool
87
+ assert.ok(calls[0].condition.startsWith('cabin.is_active = :is_active_bool'));
88
+ assert.strictEqual(calls[0].params[Object.keys(calls[0].params)[0]], true);
89
+ // 2. code_$like
90
+ assert.ok(calls[1].condition.startsWith('cabin.code LIKE :code_like'));
91
+ assert.strictEqual(calls[1].params[Object.keys(calls[1].params)[0]], '%TX%');
92
+ // 3. company_id_$in
93
+ assert.ok(calls[2].condition.startsWith('cabin.company_id IN (:...company_id_in'));
94
+ assert.deepStrictEqual(calls[2].params[Object.keys(calls[2].params)[0]], [1, 2, 3]);
95
+ // 4. id_$gt
96
+ assert.ok(calls[3].condition.startsWith('cabin.id > :id_gt'));
97
+ assert.strictEqual(calls[3].params[Object.keys(calls[3].params)[0]], 10);
98
+ });
99
+ (0, node_test_1.test)('factoryWhere should ignore empty, null, and undefined values', () => {
100
+ const qb = createMockQueryBuilder('cabin');
101
+ const filter = {
102
+ name: '',
103
+ code: null,
104
+ company_id: undefined,
105
+ is_active: true
106
+ };
107
+ const result = (0, factory_where_1.factoryWhere)(qb, filter);
108
+ const calls = result.getCalls();
109
+ assert.strictEqual(calls.length, 1);
110
+ assert.ok(calls[0].condition.startsWith('cabin.is_active = :is_active'));
111
+ });
@@ -0,0 +1,11 @@
1
+ import { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
2
+ import { IFilterPagination } from '@devstroupe/devkit-core';
3
+ export interface IQueryBuilderOptions {
4
+ allowedFilters?: string[];
5
+ allowedSorts?: string[];
6
+ searchableFields?: string[];
7
+ }
8
+ export declare class TypeOrmQueryBuilder {
9
+ static apply<Entity extends ObjectLiteral>(qb: SelectQueryBuilder<Entity>, filter: IFilterPagination, options?: IQueryBuilderOptions): SelectQueryBuilder<Entity>;
10
+ private static parseStructuredValue;
11
+ }
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TypeOrmQueryBuilder = void 0;
4
+ const typeorm_1 = require("typeorm");
5
+ class TypeOrmQueryBuilder {
6
+ static apply(qb, filter, options = {}) {
7
+ const alias = qb.alias;
8
+ const { allowedFilters, allowedSorts, searchableFields } = options;
9
+ const structuredFilter = this.parseStructuredValue(filter.filter);
10
+ const structuredSort = this.parseStructuredValue(filter.sort);
11
+ // 1. Paginação e Limite
12
+ const page = Number(filter.page) || 1;
13
+ let limit = 10;
14
+ if (filter.limit === 'todos' || filter.limit === 'all') {
15
+ limit = 0;
16
+ }
17
+ else if (filter.limit) {
18
+ limit = Number(filter.limit);
19
+ }
20
+ if (limit > 0) {
21
+ qb.take(limit).skip((page - 1) * limit);
22
+ }
23
+ // 2. Busca Global (search)
24
+ if (filter.search && searchableFields?.length) {
25
+ qb.andWhere(new typeorm_1.Brackets((subQb) => {
26
+ searchableFields.forEach((field, index) => {
27
+ const paramName = `search_param_${index}_${Math.random().toString(36).slice(2, 6)}`;
28
+ const fullField = field.includes('.') ? field : `${alias}.${field}`;
29
+ subQb.orWhere(`LOWER(${fullField}) LIKE :${paramName}`, {
30
+ [paramName]: `%${String(filter.search).toLowerCase()}%`,
31
+ });
32
+ });
33
+ }));
34
+ }
35
+ // 3. Filtros Estruturados (filter[field][operator]=value ou filter[field]=value)
36
+ if (structuredFilter) {
37
+ for (const [rawField, value] of Object.entries(structuredFilter)) {
38
+ // Tratar sufixos legados para retrocompatibilidade
39
+ let field = rawField;
40
+ let inferredOperator = null;
41
+ if (field.endsWith('_$bool')) {
42
+ field = field.replace('_$bool', '');
43
+ inferredOperator = 'eq';
44
+ }
45
+ else if (field.endsWith('_$like') || field.endsWith('_$ilike')) {
46
+ field = field.replace(/_\$i?like/, '');
47
+ inferredOperator = 'contains';
48
+ }
49
+ else if (field.endsWith('_$date_start') || field.endsWith('_$gte')) {
50
+ field = field.replace(/_\$(date_start|gte)/, '');
51
+ inferredOperator = 'gte';
52
+ }
53
+ else if (field.endsWith('_$date_end') || field.endsWith('_$lte')) {
54
+ field = field.replace(/_\$(date_end|lte)/, '');
55
+ inferredOperator = 'lte';
56
+ }
57
+ else if (field.endsWith('_$in')) {
58
+ field = field.replace('_$in', '');
59
+ inferredOperator = 'in';
60
+ }
61
+ else if (field.endsWith('_$not_in')) {
62
+ field = field.replace('_$not_in', '');
63
+ inferredOperator = 'notIn';
64
+ }
65
+ else if (field.endsWith('_$null')) {
66
+ field = field.replace('_$null', '');
67
+ inferredOperator = 'isNull';
68
+ }
69
+ // Validação de campos permitidos
70
+ if (allowedFilters && !allowedFilters.includes(field)) {
71
+ console.warn(`Filter on field "${field}" is not allowed.`);
72
+ continue;
73
+ }
74
+ const prefix = field.includes('.') ? '' : `${alias}.`;
75
+ const fullField = `${prefix}${field}`;
76
+ const applyCondition = (op, val) => {
77
+ if (val === undefined || val === null || val === '')
78
+ return;
79
+ const paramName = `${field.replace(/\./g, '_')}_${op}_${Math.random().toString(36).slice(2, 6)}`;
80
+ switch (op) {
81
+ case 'eq':
82
+ qb.andWhere(`${fullField} = :${paramName}`, { [paramName]: val });
83
+ break;
84
+ case 'neq':
85
+ qb.andWhere(`${fullField} != :${paramName}`, { [paramName]: val });
86
+ break;
87
+ case 'contains':
88
+ qb.andWhere(`LOWER(${fullField}) LIKE :${paramName}`, { [paramName]: `%${String(val).toLowerCase()}%` });
89
+ break;
90
+ case 'startsWith':
91
+ qb.andWhere(`LOWER(${fullField}) LIKE :${paramName}`, { [paramName]: `${String(val).toLowerCase()}%` });
92
+ break;
93
+ case 'endsWith':
94
+ qb.andWhere(`LOWER(${fullField}) LIKE :${paramName}`, { [paramName]: `%${String(val).toLowerCase()}` });
95
+ break;
96
+ case 'gt':
97
+ qb.andWhere(`${fullField} > :${paramName}`, { [paramName]: val });
98
+ break;
99
+ case 'gte':
100
+ qb.andWhere(`${fullField} >= :${paramName}`, { [paramName]: val });
101
+ break;
102
+ case 'lt':
103
+ qb.andWhere(`${fullField} < :${paramName}`, { [paramName]: val });
104
+ break;
105
+ case 'lte':
106
+ qb.andWhere(`${fullField} <= :${paramName}`, { [paramName]: val });
107
+ break;
108
+ case 'in':
109
+ const inArr = Array.isArray(val) ? val : String(val).split(',').map((s) => s.trim());
110
+ if (inArr.length > 0) {
111
+ qb.andWhere(`${fullField} IN (:...${paramName})`, { [paramName]: inArr });
112
+ }
113
+ break;
114
+ case 'notIn':
115
+ const notInArr = Array.isArray(val) ? val : String(val).split(',').map((s) => s.trim());
116
+ if (notInArr.length > 0) {
117
+ qb.andWhere(`${fullField} NOT IN (:...${paramName})`, { [paramName]: notInArr });
118
+ }
119
+ break;
120
+ case 'between':
121
+ const betweenArr = Array.isArray(val) ? val : String(val).split(',').map((s) => s.trim());
122
+ if (betweenArr.length === 2) {
123
+ const p1 = `${paramName}_1`;
124
+ const p2 = `${paramName}_2`;
125
+ qb.andWhere(`${fullField} BETWEEN :${p1} AND :${p2}`, { [p1]: betweenArr[0], [p2]: betweenArr[1] });
126
+ }
127
+ break;
128
+ case 'isNull':
129
+ const isNullVal = typeof val === 'boolean' ? val : val === 'true' || val === '1';
130
+ qb.andWhere(`${fullField} IS ${isNullVal ? 'NULL' : 'NOT NULL'}`);
131
+ break;
132
+ case 'isNotNull':
133
+ const isNotNullVal = typeof val === 'boolean' ? val : val === 'true' || val === '1';
134
+ qb.andWhere(`${fullField} IS ${isNotNullVal ? 'NOT NULL' : 'NULL'}`);
135
+ break;
136
+ }
137
+ };
138
+ if (inferredOperator) {
139
+ applyCondition(inferredOperator, value);
140
+ }
141
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
142
+ for (const [op, val] of Object.entries(value)) {
143
+ applyCondition(op, val);
144
+ }
145
+ }
146
+ else {
147
+ // Valor direto, mapeia para 'eq'
148
+ applyCondition('eq', value);
149
+ }
150
+ }
151
+ }
152
+ // 4. Ordenação Estruturada (sort[field]=asc)
153
+ if (structuredSort) {
154
+ for (const [field, direction] of Object.entries(structuredSort)) {
155
+ if (allowedSorts && !allowedSorts.includes(field)) {
156
+ console.warn(`Sorting on field "${field}" is not allowed.`);
157
+ continue;
158
+ }
159
+ const orderDirection = String(direction).toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
160
+ const fullField = field.includes('.') ? field : `${alias}.${field}`;
161
+ qb.addOrderBy(fullField, orderDirection);
162
+ }
163
+ }
164
+ else if (filter.sort && typeof filter.sort === 'string' && filter.direction) {
165
+ // Retrocompatibilidade com query params planos (sort=name&direction=desc)
166
+ const field = filter.sort;
167
+ if (!allowedSorts || allowedSorts.includes(field)) {
168
+ const orderDirection = String(filter.direction).toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
169
+ const fullField = field.includes('.') ? field : `${alias}.${field}`;
170
+ qb.addOrderBy(fullField, orderDirection);
171
+ }
172
+ }
173
+ return qb;
174
+ }
175
+ static parseStructuredValue(value) {
176
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
177
+ return value;
178
+ }
179
+ if (typeof value !== 'string' || !value.trim().startsWith('{'))
180
+ return undefined;
181
+ try {
182
+ const parsed = JSON.parse(value);
183
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
184
+ ? parsed
185
+ : undefined;
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ }
191
+ }
192
+ exports.TypeOrmQueryBuilder = TypeOrmQueryBuilder;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const typeorm_query_builder_1 = require("./typeorm-query.builder");
9
+ function createQueryBuilder() {
10
+ const conditions = [];
11
+ return {
12
+ alias: 'cabin',
13
+ take() { return this; },
14
+ skip() { return this; },
15
+ andWhere(condition, params) {
16
+ conditions.push({ condition, params });
17
+ return this;
18
+ },
19
+ addOrderBy() { return this; },
20
+ conditions,
21
+ };
22
+ }
23
+ (0, node_test_1.default)('TypeOrmQueryBuilder parses JSON relationship filters from HTTP query params', () => {
24
+ const queryBuilder = createQueryBuilder();
25
+ typeorm_query_builder_1.TypeOrmQueryBuilder.apply(queryBuilder, { filter: JSON.stringify({ company_id: 42 }) }, { allowedFilters: ['company_id'] });
26
+ strict_1.default.equal(queryBuilder.conditions.length, 1);
27
+ strict_1.default.match(queryBuilder.conditions[0].condition, /^cabin\.company_id = :company_id_eq_/);
28
+ strict_1.default.equal(Object.values(queryBuilder.conditions[0].params)[0], 42);
29
+ });
@@ -0,0 +1,9 @@
1
+ export declare abstract class BaseCrudController<Entity, FilterDto = any> {
2
+ protected readonly service: any;
3
+ constructor(service: any);
4
+ create(body: any): Promise<Entity>;
5
+ update(id: string, body: any): Promise<Entity>;
6
+ findMany(filter: FilterDto): Promise<any>;
7
+ findById(id: string): Promise<Entity>;
8
+ delete(id: string): Promise<void>;
9
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BaseCrudController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const swagger_1 = require("@nestjs/swagger");
18
+ class BaseCrudController {
19
+ service;
20
+ constructor(service) {
21
+ this.service = service;
22
+ }
23
+ async create(body) {
24
+ return await this.service.create(body);
25
+ }
26
+ async update(id, body) {
27
+ return await this.service.update(+id, body);
28
+ }
29
+ async findMany(filter) {
30
+ return await this.service.findMany(filter);
31
+ }
32
+ async findById(id) {
33
+ return await this.service.findOne(+id);
34
+ }
35
+ async delete(id) {
36
+ return await this.service.delete(+id);
37
+ }
38
+ }
39
+ exports.BaseCrudController = BaseCrudController;
40
+ __decorate([
41
+ (0, common_1.Post)(),
42
+ (0, swagger_1.ApiOperation)({ summary: 'Create entity' }),
43
+ (0, swagger_1.ApiResponse)({ status: 201, description: 'Created successfully' }),
44
+ __param(0, (0, common_1.Body)()),
45
+ __metadata("design:type", Function),
46
+ __metadata("design:paramtypes", [Object]),
47
+ __metadata("design:returntype", Promise)
48
+ ], BaseCrudController.prototype, "create", null);
49
+ __decorate([
50
+ (0, common_1.Put)(':id'),
51
+ (0, swagger_1.ApiOperation)({ summary: 'Update entity' }),
52
+ (0, swagger_1.ApiResponse)({ status: 200, description: 'Updated successfully' }),
53
+ __param(0, (0, common_1.Param)('id')),
54
+ __param(1, (0, common_1.Body)()),
55
+ __metadata("design:type", Function),
56
+ __metadata("design:paramtypes", [String, Object]),
57
+ __metadata("design:returntype", Promise)
58
+ ], BaseCrudController.prototype, "update", null);
59
+ __decorate([
60
+ (0, common_1.Get)(),
61
+ (0, swagger_1.ApiOperation)({ summary: 'List entities with filter and pagination' }),
62
+ __param(0, (0, common_1.Query)()),
63
+ __metadata("design:type", Function),
64
+ __metadata("design:paramtypes", [Object]),
65
+ __metadata("design:returntype", Promise)
66
+ ], BaseCrudController.prototype, "findMany", null);
67
+ __decorate([
68
+ (0, common_1.Get)(':id'),
69
+ (0, swagger_1.ApiOperation)({ summary: 'Find entity by ID' }),
70
+ __param(0, (0, common_1.Param)('id')),
71
+ __metadata("design:type", Function),
72
+ __metadata("design:paramtypes", [String]),
73
+ __metadata("design:returntype", Promise)
74
+ ], BaseCrudController.prototype, "findById", null);
75
+ __decorate([
76
+ (0, common_1.Delete)(':id'),
77
+ (0, swagger_1.ApiOperation)({ summary: 'Delete entity' }),
78
+ __param(0, (0, common_1.Param)('id')),
79
+ __metadata("design:type", Function),
80
+ __metadata("design:paramtypes", [String]),
81
+ __metadata("design:returntype", Promise)
82
+ ], BaseCrudController.prototype, "delete", null);
@@ -0,0 +1,4 @@
1
+ export * from './database/factory-where';
2
+ export * from './database/base.repository';
3
+ export * from './database/typeorm-query.builder';
4
+ export * from './http/base-crud.controller';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./database/factory-where"), exports);
18
+ __exportStar(require("./database/base.repository"), exports);
19
+ __exportStar(require("./database/typeorm-query.builder"), exports);
20
+ __exportStar(require("./http/base-crud.controller"), exports);
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@devstroupe/devkit-nest",
3
+ "version": "1.0.0",
4
+ "description": "DevsTroupe Development Kit — NestJS base classes: BaseRepository, BaseCrudController and query parsers",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "devstroupe",
8
+ "devkit",
9
+ "nestjs",
10
+ "typeorm",
11
+ "crud",
12
+ "repository",
13
+ "base-controller",
14
+ "typescript"
15
+ ],
16
+ "homepage": "https://github.com/devstroupe/devkit#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/devstroupe/devkit.git",
20
+ "directory": "packages/devkit-nest"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "dependencies": {
32
+ "@devstroupe/devkit-core": "^1.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
36
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
37
+ "@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
38
+ "typeorm": "^0.3.0",
39
+ "reflect-metadata": "^0.1.13 || ^0.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "@nestjs/common": "^10.3.8",
43
+ "@nestjs/core": "^10.3.8",
44
+ "@nestjs/swagger": "^7.3.1",
45
+ "@types/node": "^20.11.0",
46
+ "typeorm": "^0.3.17",
47
+ "reflect-metadata": "^0.1.13",
48
+ "typescript": "^5.4.5"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "test": "tsc && node --test dist/**/*.test.js"
53
+ }
54
+ }