nestjs-ddd-cli 2.2.1 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +24 -5
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Query & Filter DSL (Specification Pattern)
|
|
4
|
+
* Provides standardized filtering, sorting, and search capabilities
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.generateFilterDSL = generateFilterDSL;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const file_utils_1 = require("../utils/file.utils");
|
|
47
|
+
async function generateFilterDSL(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n🔍 Generating Filter DSL System\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared');
|
|
50
|
+
const filtersPath = path.join(sharedPath, 'filters');
|
|
51
|
+
await (0, file_utils_1.ensureDir)(filtersPath);
|
|
52
|
+
// Generate base filter types
|
|
53
|
+
await generateBaseFilterTypes(filtersPath);
|
|
54
|
+
// Generate filter builder
|
|
55
|
+
await generateFilterBuilder(filtersPath);
|
|
56
|
+
// Generate specification pattern
|
|
57
|
+
await generateSpecificationPattern(filtersPath);
|
|
58
|
+
// Generate TypeORM filter adapter
|
|
59
|
+
await generateTypeORMAdapter(filtersPath);
|
|
60
|
+
// Generate Prisma filter adapter
|
|
61
|
+
await generatePrismaAdapter(filtersPath);
|
|
62
|
+
// Generate filter DTO templates
|
|
63
|
+
await generateFilterDTOTemplates(filtersPath);
|
|
64
|
+
// Generate index
|
|
65
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'index.ts'), `export * from './filter.types';
|
|
66
|
+
export * from './filter.builder';
|
|
67
|
+
export * from './specification';
|
|
68
|
+
export * from './typeorm.adapter';
|
|
69
|
+
export * from './prisma.adapter';
|
|
70
|
+
export * from './filter.dto';
|
|
71
|
+
`);
|
|
72
|
+
console.log(chalk_1.default.green('\n✅ Filter DSL system generated!'));
|
|
73
|
+
}
|
|
74
|
+
async function generateBaseFilterTypes(filtersPath) {
|
|
75
|
+
const content = `/**
|
|
76
|
+
* Base filter types for query DSL
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
export type FilterOperator =
|
|
80
|
+
| 'eq' // equals
|
|
81
|
+
| 'ne' // not equals
|
|
82
|
+
| 'gt' // greater than
|
|
83
|
+
| 'gte' // greater than or equal
|
|
84
|
+
| 'lt' // less than
|
|
85
|
+
| 'lte' // less than or equal
|
|
86
|
+
| 'in' // in array
|
|
87
|
+
| 'nin' // not in array
|
|
88
|
+
| 'like' // LIKE pattern
|
|
89
|
+
| 'ilike' // case-insensitive LIKE
|
|
90
|
+
| 'between' // between two values
|
|
91
|
+
| 'isNull' // is null
|
|
92
|
+
| 'isNotNull' // is not null
|
|
93
|
+
| 'contains' // contains substring
|
|
94
|
+
| 'startsWith'// starts with
|
|
95
|
+
| 'endsWith'; // ends with
|
|
96
|
+
|
|
97
|
+
export type SortDirection = 'ASC' | 'DESC';
|
|
98
|
+
|
|
99
|
+
export interface FilterCondition<T = any> {
|
|
100
|
+
field: string;
|
|
101
|
+
operator: FilterOperator;
|
|
102
|
+
value: T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SortCondition {
|
|
106
|
+
field: string;
|
|
107
|
+
direction: SortDirection;
|
|
108
|
+
nulls?: 'first' | 'last';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PaginationOptions {
|
|
112
|
+
page: number;
|
|
113
|
+
limit: number;
|
|
114
|
+
offset?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FilterQuery {
|
|
118
|
+
conditions: FilterCondition[];
|
|
119
|
+
sort?: SortCondition[];
|
|
120
|
+
pagination?: PaginationOptions;
|
|
121
|
+
search?: SearchOptions;
|
|
122
|
+
include?: string[];
|
|
123
|
+
select?: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SearchOptions {
|
|
127
|
+
query: string;
|
|
128
|
+
fields: string[];
|
|
129
|
+
mode?: 'any' | 'all' | 'phrase';
|
|
130
|
+
fuzzy?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface FilterResult<T> {
|
|
134
|
+
data: T[];
|
|
135
|
+
total: number;
|
|
136
|
+
page: number;
|
|
137
|
+
limit: number;
|
|
138
|
+
totalPages: number;
|
|
139
|
+
hasNext: boolean;
|
|
140
|
+
hasPrev: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface FilterGroup {
|
|
144
|
+
type: 'and' | 'or';
|
|
145
|
+
conditions: (FilterCondition | FilterGroup)[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Type-safe filter definition for an entity
|
|
150
|
+
*/
|
|
151
|
+
export type EntityFilter<T> = {
|
|
152
|
+
[K in keyof T]?: FilterValue<T[K]>;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type FilterValue<T> =
|
|
156
|
+
| T
|
|
157
|
+
| { eq: T }
|
|
158
|
+
| { ne: T }
|
|
159
|
+
| { gt: T }
|
|
160
|
+
| { gte: T }
|
|
161
|
+
| { lt: T }
|
|
162
|
+
| { lte: T }
|
|
163
|
+
| { in: T[] }
|
|
164
|
+
| { nin: T[] }
|
|
165
|
+
| { like: string }
|
|
166
|
+
| { ilike: string }
|
|
167
|
+
| { between: [T, T] }
|
|
168
|
+
| { isNull: boolean }
|
|
169
|
+
| { contains: string }
|
|
170
|
+
| { startsWith: string }
|
|
171
|
+
| { endsWith: string };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Range filter for numeric/date fields
|
|
175
|
+
*/
|
|
176
|
+
export interface RangeFilter<T = number | Date> {
|
|
177
|
+
from?: T;
|
|
178
|
+
to?: T;
|
|
179
|
+
inclusive?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Date range presets
|
|
184
|
+
*/
|
|
185
|
+
export type DateRangePreset =
|
|
186
|
+
| 'today'
|
|
187
|
+
| 'yesterday'
|
|
188
|
+
| 'thisWeek'
|
|
189
|
+
| 'lastWeek'
|
|
190
|
+
| 'thisMonth'
|
|
191
|
+
| 'lastMonth'
|
|
192
|
+
| 'thisYear'
|
|
193
|
+
| 'lastYear'
|
|
194
|
+
| 'last7Days'
|
|
195
|
+
| 'last30Days'
|
|
196
|
+
| 'last90Days';
|
|
197
|
+
|
|
198
|
+
export function getDateRange(preset: DateRangePreset): { from: Date; to: Date } {
|
|
199
|
+
const now = new Date();
|
|
200
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
201
|
+
|
|
202
|
+
switch (preset) {
|
|
203
|
+
case 'today':
|
|
204
|
+
return { from: today, to: now };
|
|
205
|
+
case 'yesterday':
|
|
206
|
+
const yesterday = new Date(today);
|
|
207
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
208
|
+
return { from: yesterday, to: today };
|
|
209
|
+
case 'thisWeek':
|
|
210
|
+
const weekStart = new Date(today);
|
|
211
|
+
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
|
|
212
|
+
return { from: weekStart, to: now };
|
|
213
|
+
case 'lastWeek':
|
|
214
|
+
const lastWeekEnd = new Date(today);
|
|
215
|
+
lastWeekEnd.setDate(lastWeekEnd.getDate() - lastWeekEnd.getDay());
|
|
216
|
+
const lastWeekStart = new Date(lastWeekEnd);
|
|
217
|
+
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
|
|
218
|
+
return { from: lastWeekStart, to: lastWeekEnd };
|
|
219
|
+
case 'thisMonth':
|
|
220
|
+
return { from: new Date(now.getFullYear(), now.getMonth(), 1), to: now };
|
|
221
|
+
case 'lastMonth':
|
|
222
|
+
return {
|
|
223
|
+
from: new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
|
224
|
+
to: new Date(now.getFullYear(), now.getMonth(), 0),
|
|
225
|
+
};
|
|
226
|
+
case 'thisYear':
|
|
227
|
+
return { from: new Date(now.getFullYear(), 0, 1), to: now };
|
|
228
|
+
case 'lastYear':
|
|
229
|
+
return {
|
|
230
|
+
from: new Date(now.getFullYear() - 1, 0, 1),
|
|
231
|
+
to: new Date(now.getFullYear() - 1, 11, 31),
|
|
232
|
+
};
|
|
233
|
+
case 'last7Days':
|
|
234
|
+
const week = new Date(today);
|
|
235
|
+
week.setDate(week.getDate() - 7);
|
|
236
|
+
return { from: week, to: now };
|
|
237
|
+
case 'last30Days':
|
|
238
|
+
const month = new Date(today);
|
|
239
|
+
month.setDate(month.getDate() - 30);
|
|
240
|
+
return { from: month, to: now };
|
|
241
|
+
case 'last90Days':
|
|
242
|
+
const quarter = new Date(today);
|
|
243
|
+
quarter.setDate(quarter.getDate() - 90);
|
|
244
|
+
return { from: quarter, to: now };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.types.ts'), content);
|
|
249
|
+
console.log(chalk_1.default.green(' ✓ Filter types'));
|
|
250
|
+
}
|
|
251
|
+
async function generateFilterBuilder(filtersPath) {
|
|
252
|
+
const content = `import {
|
|
253
|
+
FilterCondition,
|
|
254
|
+
FilterGroup,
|
|
255
|
+
FilterOperator,
|
|
256
|
+
FilterQuery,
|
|
257
|
+
PaginationOptions,
|
|
258
|
+
SearchOptions,
|
|
259
|
+
SortCondition,
|
|
260
|
+
SortDirection,
|
|
261
|
+
} from './filter.types';
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fluent filter builder for constructing queries
|
|
265
|
+
*/
|
|
266
|
+
export class FilterBuilder {
|
|
267
|
+
private query: FilterQuery = {
|
|
268
|
+
conditions: [],
|
|
269
|
+
sort: [],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
private currentGroup: FilterGroup | null = null;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Add a filter condition
|
|
276
|
+
*/
|
|
277
|
+
where(field: string, operator: FilterOperator, value: any): this {
|
|
278
|
+
const condition: FilterCondition = { field, operator, value };
|
|
279
|
+
|
|
280
|
+
if (this.currentGroup) {
|
|
281
|
+
this.currentGroup.conditions.push(condition);
|
|
282
|
+
} else {
|
|
283
|
+
this.query.conditions.push(condition);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Shorthand for equals
|
|
291
|
+
*/
|
|
292
|
+
eq(field: string, value: any): this {
|
|
293
|
+
return this.where(field, 'eq', value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Shorthand for not equals
|
|
298
|
+
*/
|
|
299
|
+
ne(field: string, value: any): this {
|
|
300
|
+
return this.where(field, 'ne', value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Shorthand for greater than
|
|
305
|
+
*/
|
|
306
|
+
gt(field: string, value: any): this {
|
|
307
|
+
return this.where(field, 'gt', value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Shorthand for greater than or equal
|
|
312
|
+
*/
|
|
313
|
+
gte(field: string, value: any): this {
|
|
314
|
+
return this.where(field, 'gte', value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Shorthand for less than
|
|
319
|
+
*/
|
|
320
|
+
lt(field: string, value: any): this {
|
|
321
|
+
return this.where(field, 'lt', value);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Shorthand for less than or equal
|
|
326
|
+
*/
|
|
327
|
+
lte(field: string, value: any): this {
|
|
328
|
+
return this.where(field, 'lte', value);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Filter by value in array
|
|
333
|
+
*/
|
|
334
|
+
in(field: string, values: any[]): this {
|
|
335
|
+
return this.where(field, 'in', values);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Filter by value not in array
|
|
340
|
+
*/
|
|
341
|
+
notIn(field: string, values: any[]): this {
|
|
342
|
+
return this.where(field, 'nin', values);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Filter by LIKE pattern
|
|
347
|
+
*/
|
|
348
|
+
like(field: string, pattern: string): this {
|
|
349
|
+
return this.where(field, 'like', pattern);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Filter by case-insensitive LIKE pattern
|
|
354
|
+
*/
|
|
355
|
+
ilike(field: string, pattern: string): this {
|
|
356
|
+
return this.where(field, 'ilike', pattern);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Filter by value between two values
|
|
361
|
+
*/
|
|
362
|
+
between(field: string, from: any, to: any): this {
|
|
363
|
+
return this.where(field, 'between', [from, to]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Filter by null
|
|
368
|
+
*/
|
|
369
|
+
isNull(field: string): this {
|
|
370
|
+
return this.where(field, 'isNull', true);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Filter by not null
|
|
375
|
+
*/
|
|
376
|
+
isNotNull(field: string): this {
|
|
377
|
+
return this.where(field, 'isNotNull', true);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Filter by contains substring
|
|
382
|
+
*/
|
|
383
|
+
contains(field: string, value: string): this {
|
|
384
|
+
return this.where(field, 'contains', value);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Filter by starts with
|
|
389
|
+
*/
|
|
390
|
+
startsWith(field: string, value: string): this {
|
|
391
|
+
return this.where(field, 'startsWith', value);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Filter by ends with
|
|
396
|
+
*/
|
|
397
|
+
endsWith(field: string, value: string): this {
|
|
398
|
+
return this.where(field, 'endsWith', value);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Start an AND group
|
|
403
|
+
*/
|
|
404
|
+
andWhere(builder: (fb: FilterBuilder) => FilterBuilder): this {
|
|
405
|
+
const group: FilterGroup = { type: 'and', conditions: [] };
|
|
406
|
+
const nestedBuilder = new FilterBuilder();
|
|
407
|
+
nestedBuilder.currentGroup = group;
|
|
408
|
+
builder(nestedBuilder);
|
|
409
|
+
this.query.conditions.push(group as any);
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Start an OR group
|
|
415
|
+
*/
|
|
416
|
+
orWhere(builder: (fb: FilterBuilder) => FilterBuilder): this {
|
|
417
|
+
const group: FilterGroup = { type: 'or', conditions: [] };
|
|
418
|
+
const nestedBuilder = new FilterBuilder();
|
|
419
|
+
nestedBuilder.currentGroup = group;
|
|
420
|
+
builder(nestedBuilder);
|
|
421
|
+
this.query.conditions.push(group as any);
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Add sort condition
|
|
427
|
+
*/
|
|
428
|
+
orderBy(field: string, direction: SortDirection = 'ASC', nulls?: 'first' | 'last'): this {
|
|
429
|
+
this.query.sort = this.query.sort || [];
|
|
430
|
+
this.query.sort.push({ field, direction, nulls });
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Sort ascending
|
|
436
|
+
*/
|
|
437
|
+
asc(field: string): this {
|
|
438
|
+
return this.orderBy(field, 'ASC');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Sort descending
|
|
443
|
+
*/
|
|
444
|
+
desc(field: string): this {
|
|
445
|
+
return this.orderBy(field, 'DESC');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Set pagination
|
|
450
|
+
*/
|
|
451
|
+
paginate(page: number, limit: number): this {
|
|
452
|
+
this.query.pagination = {
|
|
453
|
+
page,
|
|
454
|
+
limit,
|
|
455
|
+
offset: (page - 1) * limit,
|
|
456
|
+
};
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Set offset-based pagination
|
|
462
|
+
*/
|
|
463
|
+
skip(offset: number): this {
|
|
464
|
+
this.query.pagination = this.query.pagination || { page: 1, limit: 10 };
|
|
465
|
+
this.query.pagination.offset = offset;
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Set limit
|
|
471
|
+
*/
|
|
472
|
+
take(limit: number): this {
|
|
473
|
+
this.query.pagination = this.query.pagination || { page: 1, limit };
|
|
474
|
+
this.query.pagination.limit = limit;
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Add full-text search
|
|
480
|
+
*/
|
|
481
|
+
search(query: string, fields: string[], options?: Partial<SearchOptions>): this {
|
|
482
|
+
this.query.search = {
|
|
483
|
+
query,
|
|
484
|
+
fields,
|
|
485
|
+
mode: options?.mode || 'any',
|
|
486
|
+
fuzzy: options?.fuzzy || false,
|
|
487
|
+
};
|
|
488
|
+
return this;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Include relations
|
|
493
|
+
*/
|
|
494
|
+
include(...relations: string[]): this {
|
|
495
|
+
this.query.include = [...(this.query.include || []), ...relations];
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Select specific fields
|
|
501
|
+
*/
|
|
502
|
+
select(...fields: string[]): this {
|
|
503
|
+
this.query.select = [...(this.query.select || []), ...fields];
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Build the filter query
|
|
509
|
+
*/
|
|
510
|
+
build(): FilterQuery {
|
|
511
|
+
return { ...this.query };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create a new builder
|
|
516
|
+
*/
|
|
517
|
+
static create(): FilterBuilder {
|
|
518
|
+
return new FilterBuilder();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Helper function to create filter builder
|
|
524
|
+
*/
|
|
525
|
+
export function filter(): FilterBuilder {
|
|
526
|
+
return FilterBuilder.create();
|
|
527
|
+
}
|
|
528
|
+
`;
|
|
529
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.builder.ts'), content);
|
|
530
|
+
console.log(chalk_1.default.green(' ✓ Filter builder'));
|
|
531
|
+
}
|
|
532
|
+
async function generateSpecificationPattern(filtersPath) {
|
|
533
|
+
const content = `/**
|
|
534
|
+
* Specification Pattern Implementation
|
|
535
|
+
* For composable business rules
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
export interface Specification<T> {
|
|
539
|
+
isSatisfiedBy(candidate: T): boolean;
|
|
540
|
+
and(other: Specification<T>): Specification<T>;
|
|
541
|
+
or(other: Specification<T>): Specification<T>;
|
|
542
|
+
not(): Specification<T>;
|
|
543
|
+
toSQL?(): { where: string; params: any[] };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Base specification class
|
|
548
|
+
*/
|
|
549
|
+
export abstract class BaseSpecification<T> implements Specification<T> {
|
|
550
|
+
abstract isSatisfiedBy(candidate: T): boolean;
|
|
551
|
+
|
|
552
|
+
and(other: Specification<T>): Specification<T> {
|
|
553
|
+
return new AndSpecification(this, other);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
or(other: Specification<T>): Specification<T> {
|
|
557
|
+
return new OrSpecification(this, other);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
not(): Specification<T> {
|
|
561
|
+
return new NotSpecification(this);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* AND composite specification
|
|
567
|
+
*/
|
|
568
|
+
export class AndSpecification<T> extends BaseSpecification<T> {
|
|
569
|
+
constructor(
|
|
570
|
+
private readonly left: Specification<T>,
|
|
571
|
+
private readonly right: Specification<T>
|
|
572
|
+
) {
|
|
573
|
+
super();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
577
|
+
return this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
toSQL(): { where: string; params: any[] } {
|
|
581
|
+
const leftSql = this.left.toSQL?.() || { where: '1=1', params: [] };
|
|
582
|
+
const rightSql = this.right.toSQL?.() || { where: '1=1', params: [] };
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
where: \`(\${leftSql.where}) AND (\${rightSql.where})\`,
|
|
586
|
+
params: [...leftSql.params, ...rightSql.params],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* OR composite specification
|
|
593
|
+
*/
|
|
594
|
+
export class OrSpecification<T> extends BaseSpecification<T> {
|
|
595
|
+
constructor(
|
|
596
|
+
private readonly left: Specification<T>,
|
|
597
|
+
private readonly right: Specification<T>
|
|
598
|
+
) {
|
|
599
|
+
super();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
603
|
+
return this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
toSQL(): { where: string; params: any[] } {
|
|
607
|
+
const leftSql = this.left.toSQL?.() || { where: '1=1', params: [] };
|
|
608
|
+
const rightSql = this.right.toSQL?.() || { where: '1=1', params: [] };
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
where: \`(\${leftSql.where}) OR (\${rightSql.where})\`,
|
|
612
|
+
params: [...leftSql.params, ...rightSql.params],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* NOT specification
|
|
619
|
+
*/
|
|
620
|
+
export class NotSpecification<T> extends BaseSpecification<T> {
|
|
621
|
+
constructor(private readonly spec: Specification<T>) {
|
|
622
|
+
super();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
626
|
+
return !this.spec.isSatisfiedBy(candidate);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
toSQL(): { where: string; params: any[] } {
|
|
630
|
+
const sql = this.spec.toSQL?.() || { where: '1=1', params: [] };
|
|
631
|
+
return {
|
|
632
|
+
where: \`NOT (\${sql.where})\`,
|
|
633
|
+
params: sql.params,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Field equals value specification
|
|
640
|
+
*/
|
|
641
|
+
export class FieldEqualsSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
642
|
+
constructor(
|
|
643
|
+
private readonly field: K,
|
|
644
|
+
private readonly value: T[K]
|
|
645
|
+
) {
|
|
646
|
+
super();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
650
|
+
return candidate[this.field] === this.value;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
toSQL(): { where: string; params: any[] } {
|
|
654
|
+
return {
|
|
655
|
+
where: \`\${String(this.field)} = ?\`,
|
|
656
|
+
params: [this.value],
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Field greater than specification
|
|
663
|
+
*/
|
|
664
|
+
export class FieldGreaterThanSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
665
|
+
constructor(
|
|
666
|
+
private readonly field: K,
|
|
667
|
+
private readonly value: T[K]
|
|
668
|
+
) {
|
|
669
|
+
super();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
673
|
+
return candidate[this.field] > this.value;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
toSQL(): { where: string; params: any[] } {
|
|
677
|
+
return {
|
|
678
|
+
where: \`\${String(this.field)} > ?\`,
|
|
679
|
+
params: [this.value],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Field in array specification
|
|
686
|
+
*/
|
|
687
|
+
export class FieldInSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
688
|
+
constructor(
|
|
689
|
+
private readonly field: K,
|
|
690
|
+
private readonly values: T[K][]
|
|
691
|
+
) {
|
|
692
|
+
super();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
696
|
+
return this.values.includes(candidate[this.field]);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
toSQL(): { where: string; params: any[] } {
|
|
700
|
+
const placeholders = this.values.map(() => '?').join(', ');
|
|
701
|
+
return {
|
|
702
|
+
where: \`\${String(this.field)} IN (\${placeholders})\`,
|
|
703
|
+
params: this.values,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Field contains substring specification
|
|
710
|
+
*/
|
|
711
|
+
export class FieldContainsSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
712
|
+
constructor(
|
|
713
|
+
private readonly field: K,
|
|
714
|
+
private readonly substring: string
|
|
715
|
+
) {
|
|
716
|
+
super();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
720
|
+
const value = candidate[this.field];
|
|
721
|
+
return typeof value === 'string' && value.includes(this.substring);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
toSQL(): { where: string; params: any[] } {
|
|
725
|
+
return {
|
|
726
|
+
where: \`\${String(this.field)} LIKE ?\`,
|
|
727
|
+
params: [\`%\${this.substring}%\`],
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Field is null specification
|
|
734
|
+
*/
|
|
735
|
+
export class FieldIsNullSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
736
|
+
constructor(private readonly field: K) {
|
|
737
|
+
super();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
741
|
+
return candidate[this.field] === null || candidate[this.field] === undefined;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
toSQL(): { where: string; params: any[] } {
|
|
745
|
+
return {
|
|
746
|
+
where: \`\${String(this.field)} IS NULL\`,
|
|
747
|
+
params: [],
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Date range specification
|
|
754
|
+
*/
|
|
755
|
+
export class DateRangeSpecification<T, K extends keyof T> extends BaseSpecification<T> {
|
|
756
|
+
constructor(
|
|
757
|
+
private readonly field: K,
|
|
758
|
+
private readonly from: Date,
|
|
759
|
+
private readonly to: Date
|
|
760
|
+
) {
|
|
761
|
+
super();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
isSatisfiedBy(candidate: T): boolean {
|
|
765
|
+
const value = candidate[this.field];
|
|
766
|
+
if (!(value instanceof Date)) return false;
|
|
767
|
+
return value >= this.from && value <= this.to;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
toSQL(): { where: string; params: any[] } {
|
|
771
|
+
return {
|
|
772
|
+
where: \`\${String(this.field)} BETWEEN ? AND ?\`,
|
|
773
|
+
params: [this.from, this.to],
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Helper functions to create specifications
|
|
780
|
+
*/
|
|
781
|
+
export const spec = {
|
|
782
|
+
equals: <T, K extends keyof T>(field: K, value: T[K]) =>
|
|
783
|
+
new FieldEqualsSpecification<T, K>(field, value),
|
|
784
|
+
|
|
785
|
+
greaterThan: <T, K extends keyof T>(field: K, value: T[K]) =>
|
|
786
|
+
new FieldGreaterThanSpecification<T, K>(field, value),
|
|
787
|
+
|
|
788
|
+
in: <T, K extends keyof T>(field: K, values: T[K][]) =>
|
|
789
|
+
new FieldInSpecification<T, K>(field, values),
|
|
790
|
+
|
|
791
|
+
contains: <T, K extends keyof T>(field: K, substring: string) =>
|
|
792
|
+
new FieldContainsSpecification<T, K>(field, substring),
|
|
793
|
+
|
|
794
|
+
isNull: <T, K extends keyof T>(field: K) =>
|
|
795
|
+
new FieldIsNullSpecification<T, K>(field),
|
|
796
|
+
|
|
797
|
+
dateRange: <T, K extends keyof T>(field: K, from: Date, to: Date) =>
|
|
798
|
+
new DateRangeSpecification<T, K>(field, from, to),
|
|
799
|
+
};
|
|
800
|
+
`;
|
|
801
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'specification.ts'), content);
|
|
802
|
+
console.log(chalk_1.default.green(' ✓ Specification pattern'));
|
|
803
|
+
}
|
|
804
|
+
async function generateTypeORMAdapter(filtersPath) {
|
|
805
|
+
const content = `import { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
|
|
806
|
+
import { FilterCondition, FilterQuery, FilterResult, FilterGroup } from './filter.types';
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* TypeORM adapter for filter queries
|
|
810
|
+
*/
|
|
811
|
+
export class TypeORMFilterAdapter {
|
|
812
|
+
/**
|
|
813
|
+
* Apply filter query to TypeORM query builder
|
|
814
|
+
*/
|
|
815
|
+
static apply<T extends ObjectLiteral>(
|
|
816
|
+
qb: SelectQueryBuilder<T>,
|
|
817
|
+
query: FilterQuery,
|
|
818
|
+
alias?: string
|
|
819
|
+
): SelectQueryBuilder<T> {
|
|
820
|
+
const tableAlias = alias || qb.alias;
|
|
821
|
+
|
|
822
|
+
// Apply conditions
|
|
823
|
+
for (const condition of query.conditions) {
|
|
824
|
+
if ('type' in condition) {
|
|
825
|
+
// It's a filter group
|
|
826
|
+
this.applyGroup(qb, condition as unknown as FilterGroup, tableAlias);
|
|
827
|
+
} else {
|
|
828
|
+
this.applyCondition(qb, condition, tableAlias);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Apply search
|
|
833
|
+
if (query.search) {
|
|
834
|
+
const searchConditions = query.search.fields.map(field => {
|
|
835
|
+
const paramName = \`search_\${field.replace('.', '_')}\`;
|
|
836
|
+
qb.setParameter(paramName, \`%\${query.search!.query}%\`);
|
|
837
|
+
return \`\${tableAlias}.\${field} ILIKE :\${paramName}\`;
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const operator = query.search.mode === 'all' ? ' AND ' : ' OR ';
|
|
841
|
+
qb.andWhere(\`(\${searchConditions.join(operator)})\`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Apply sorting
|
|
845
|
+
if (query.sort?.length) {
|
|
846
|
+
for (const sort of query.sort) {
|
|
847
|
+
qb.addOrderBy(
|
|
848
|
+
\`\${tableAlias}.\${sort.field}\`,
|
|
849
|
+
sort.direction,
|
|
850
|
+
sort.nulls === 'first' ? 'NULLS FIRST' : sort.nulls === 'last' ? 'NULLS LAST' : undefined
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Apply pagination
|
|
856
|
+
if (query.pagination) {
|
|
857
|
+
qb.skip(query.pagination.offset ?? (query.pagination.page - 1) * query.pagination.limit);
|
|
858
|
+
qb.take(query.pagination.limit);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Apply relations
|
|
862
|
+
if (query.include?.length) {
|
|
863
|
+
for (const relation of query.include) {
|
|
864
|
+
qb.leftJoinAndSelect(\`\${tableAlias}.\${relation}\`, relation);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Apply field selection
|
|
869
|
+
if (query.select?.length) {
|
|
870
|
+
qb.select(query.select.map(f => \`\${tableAlias}.\${f}\`));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return qb;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Apply a single condition
|
|
878
|
+
*/
|
|
879
|
+
private static applyCondition<T extends ObjectLiteral>(
|
|
880
|
+
qb: SelectQueryBuilder<T>,
|
|
881
|
+
condition: FilterCondition,
|
|
882
|
+
alias: string
|
|
883
|
+
): void {
|
|
884
|
+
const field = \`\${alias}.\${condition.field}\`;
|
|
885
|
+
const paramName = \`\${condition.field.replace('.', '_')}_\${Date.now()}\`;
|
|
886
|
+
|
|
887
|
+
switch (condition.operator) {
|
|
888
|
+
case 'eq':
|
|
889
|
+
qb.andWhere(\`\${field} = :\${paramName}\`, { [paramName]: condition.value });
|
|
890
|
+
break;
|
|
891
|
+
case 'ne':
|
|
892
|
+
qb.andWhere(\`\${field} != :\${paramName}\`, { [paramName]: condition.value });
|
|
893
|
+
break;
|
|
894
|
+
case 'gt':
|
|
895
|
+
qb.andWhere(\`\${field} > :\${paramName}\`, { [paramName]: condition.value });
|
|
896
|
+
break;
|
|
897
|
+
case 'gte':
|
|
898
|
+
qb.andWhere(\`\${field} >= :\${paramName}\`, { [paramName]: condition.value });
|
|
899
|
+
break;
|
|
900
|
+
case 'lt':
|
|
901
|
+
qb.andWhere(\`\${field} < :\${paramName}\`, { [paramName]: condition.value });
|
|
902
|
+
break;
|
|
903
|
+
case 'lte':
|
|
904
|
+
qb.andWhere(\`\${field} <= :\${paramName}\`, { [paramName]: condition.value });
|
|
905
|
+
break;
|
|
906
|
+
case 'in':
|
|
907
|
+
qb.andWhere(\`\${field} IN (:\${paramName})\`, { [paramName]: condition.value });
|
|
908
|
+
break;
|
|
909
|
+
case 'nin':
|
|
910
|
+
qb.andWhere(\`\${field} NOT IN (:\${paramName})\`, { [paramName]: condition.value });
|
|
911
|
+
break;
|
|
912
|
+
case 'like':
|
|
913
|
+
qb.andWhere(\`\${field} LIKE :\${paramName}\`, { [paramName]: condition.value });
|
|
914
|
+
break;
|
|
915
|
+
case 'ilike':
|
|
916
|
+
qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: condition.value });
|
|
917
|
+
break;
|
|
918
|
+
case 'between':
|
|
919
|
+
const [from, to] = condition.value;
|
|
920
|
+
qb.andWhere(\`\${field} BETWEEN :\${paramName}_from AND :\${paramName}_to\`, {
|
|
921
|
+
[\`\${paramName}_from\`]: from,
|
|
922
|
+
[\`\${paramName}_to\`]: to,
|
|
923
|
+
});
|
|
924
|
+
break;
|
|
925
|
+
case 'isNull':
|
|
926
|
+
qb.andWhere(\`\${field} IS NULL\`);
|
|
927
|
+
break;
|
|
928
|
+
case 'isNotNull':
|
|
929
|
+
qb.andWhere(\`\${field} IS NOT NULL\`);
|
|
930
|
+
break;
|
|
931
|
+
case 'contains':
|
|
932
|
+
qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`%\${condition.value}%\` });
|
|
933
|
+
break;
|
|
934
|
+
case 'startsWith':
|
|
935
|
+
qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`\${condition.value}%\` });
|
|
936
|
+
break;
|
|
937
|
+
case 'endsWith':
|
|
938
|
+
qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`%\${condition.value}\` });
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Apply a filter group (AND/OR)
|
|
945
|
+
*/
|
|
946
|
+
private static applyGroup<T extends ObjectLiteral>(
|
|
947
|
+
qb: SelectQueryBuilder<T>,
|
|
948
|
+
group: FilterGroup,
|
|
949
|
+
alias: string
|
|
950
|
+
): void {
|
|
951
|
+
// Create a subquery builder for the group
|
|
952
|
+
const conditions: string[] = [];
|
|
953
|
+
const params: Record<string, any> = {};
|
|
954
|
+
|
|
955
|
+
for (const condition of group.conditions) {
|
|
956
|
+
if ('type' in condition) {
|
|
957
|
+
// Nested group - recursive
|
|
958
|
+
// For simplicity, we'll handle one level of nesting
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const cond = condition as FilterCondition;
|
|
963
|
+
const field = \`\${alias}.\${cond.field}\`;
|
|
964
|
+
const paramName = \`\${cond.field.replace('.', '_')}_\${Date.now()}_\${Math.random().toString(36).slice(2, 7)}\`;
|
|
965
|
+
|
|
966
|
+
switch (cond.operator) {
|
|
967
|
+
case 'eq':
|
|
968
|
+
conditions.push(\`\${field} = :\${paramName}\`);
|
|
969
|
+
params[paramName] = cond.value;
|
|
970
|
+
break;
|
|
971
|
+
case 'ne':
|
|
972
|
+
conditions.push(\`\${field} != :\${paramName}\`);
|
|
973
|
+
params[paramName] = cond.value;
|
|
974
|
+
break;
|
|
975
|
+
// Add other operators as needed
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (conditions.length > 0) {
|
|
980
|
+
const joinOperator = group.type === 'and' ? ' AND ' : ' OR ';
|
|
981
|
+
qb.andWhere(\`(\${conditions.join(joinOperator)})\`, params);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Execute query and return paginated result
|
|
987
|
+
*/
|
|
988
|
+
static async execute<T extends ObjectLiteral>(
|
|
989
|
+
qb: SelectQueryBuilder<T>,
|
|
990
|
+
query: FilterQuery
|
|
991
|
+
): Promise<FilterResult<T>> {
|
|
992
|
+
const [data, total] = await qb.getManyAndCount();
|
|
993
|
+
|
|
994
|
+
const page = query.pagination?.page || 1;
|
|
995
|
+
const limit = query.pagination?.limit || data.length;
|
|
996
|
+
const totalPages = Math.ceil(total / limit);
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
data,
|
|
1000
|
+
total,
|
|
1001
|
+
page,
|
|
1002
|
+
limit,
|
|
1003
|
+
totalPages,
|
|
1004
|
+
hasNext: page < totalPages,
|
|
1005
|
+
hasPrev: page > 1,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
`;
|
|
1010
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'typeorm.adapter.ts'), content);
|
|
1011
|
+
console.log(chalk_1.default.green(' ✓ TypeORM adapter'));
|
|
1012
|
+
}
|
|
1013
|
+
async function generatePrismaAdapter(filtersPath) {
|
|
1014
|
+
const content = `import { FilterCondition, FilterQuery, FilterResult, FilterGroup } from './filter.types';
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Prisma adapter for filter queries
|
|
1018
|
+
*/
|
|
1019
|
+
export class PrismaFilterAdapter {
|
|
1020
|
+
/**
|
|
1021
|
+
* Convert filter query to Prisma where clause
|
|
1022
|
+
*/
|
|
1023
|
+
static toWhereClause(query: FilterQuery): Record<string, any> {
|
|
1024
|
+
const where: Record<string, any> = {};
|
|
1025
|
+
|
|
1026
|
+
for (const condition of query.conditions) {
|
|
1027
|
+
if ('type' in condition) {
|
|
1028
|
+
// It's a filter group
|
|
1029
|
+
const group = condition as unknown as FilterGroup;
|
|
1030
|
+
const groupConditions = this.groupToWhere(group);
|
|
1031
|
+
|
|
1032
|
+
if (group.type === 'and') {
|
|
1033
|
+
where.AND = where.AND || [];
|
|
1034
|
+
where.AND.push(groupConditions);
|
|
1035
|
+
} else {
|
|
1036
|
+
where.OR = where.OR || [];
|
|
1037
|
+
where.OR.push(groupConditions);
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
const prismaCondition = this.conditionToWhere(condition);
|
|
1041
|
+
Object.assign(where, prismaCondition);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Apply search
|
|
1046
|
+
if (query.search) {
|
|
1047
|
+
const searchConditions = query.search.fields.map(field => ({
|
|
1048
|
+
[field]: { contains: query.search!.query, mode: 'insensitive' },
|
|
1049
|
+
}));
|
|
1050
|
+
|
|
1051
|
+
if (query.search.mode === 'all') {
|
|
1052
|
+
where.AND = [...(where.AND || []), ...searchConditions];
|
|
1053
|
+
} else {
|
|
1054
|
+
where.OR = [...(where.OR || []), ...searchConditions];
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return where;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Convert a single condition to Prisma format
|
|
1063
|
+
*/
|
|
1064
|
+
private static conditionToWhere(condition: FilterCondition): Record<string, any> {
|
|
1065
|
+
const { field, operator, value } = condition;
|
|
1066
|
+
|
|
1067
|
+
switch (operator) {
|
|
1068
|
+
case 'eq':
|
|
1069
|
+
return { [field]: value };
|
|
1070
|
+
case 'ne':
|
|
1071
|
+
return { [field]: { not: value } };
|
|
1072
|
+
case 'gt':
|
|
1073
|
+
return { [field]: { gt: value } };
|
|
1074
|
+
case 'gte':
|
|
1075
|
+
return { [field]: { gte: value } };
|
|
1076
|
+
case 'lt':
|
|
1077
|
+
return { [field]: { lt: value } };
|
|
1078
|
+
case 'lte':
|
|
1079
|
+
return { [field]: { lte: value } };
|
|
1080
|
+
case 'in':
|
|
1081
|
+
return { [field]: { in: value } };
|
|
1082
|
+
case 'nin':
|
|
1083
|
+
return { [field]: { notIn: value } };
|
|
1084
|
+
case 'like':
|
|
1085
|
+
return { [field]: { contains: value.replace(/%/g, '') } };
|
|
1086
|
+
case 'ilike':
|
|
1087
|
+
return { [field]: { contains: value.replace(/%/g, ''), mode: 'insensitive' } };
|
|
1088
|
+
case 'between':
|
|
1089
|
+
return { [field]: { gte: value[0], lte: value[1] } };
|
|
1090
|
+
case 'isNull':
|
|
1091
|
+
return { [field]: null };
|
|
1092
|
+
case 'isNotNull':
|
|
1093
|
+
return { [field]: { not: null } };
|
|
1094
|
+
case 'contains':
|
|
1095
|
+
return { [field]: { contains: value, mode: 'insensitive' } };
|
|
1096
|
+
case 'startsWith':
|
|
1097
|
+
return { [field]: { startsWith: value, mode: 'insensitive' } };
|
|
1098
|
+
case 'endsWith':
|
|
1099
|
+
return { [field]: { endsWith: value, mode: 'insensitive' } };
|
|
1100
|
+
default:
|
|
1101
|
+
return {};
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Convert filter group to Prisma format
|
|
1107
|
+
*/
|
|
1108
|
+
private static groupToWhere(group: FilterGroup): Record<string, any> {
|
|
1109
|
+
const conditions = group.conditions.map(c => {
|
|
1110
|
+
if ('type' in c) {
|
|
1111
|
+
return this.groupToWhere(c as FilterGroup);
|
|
1112
|
+
}
|
|
1113
|
+
return this.conditionToWhere(c as FilterCondition);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
return group.type === 'and' ? { AND: conditions } : { OR: conditions };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Convert filter query to Prisma orderBy
|
|
1121
|
+
*/
|
|
1122
|
+
static toOrderBy(query: FilterQuery): Record<string, 'asc' | 'desc'>[] {
|
|
1123
|
+
if (!query.sort?.length) return [];
|
|
1124
|
+
|
|
1125
|
+
return query.sort.map(sort => ({
|
|
1126
|
+
[sort.field]: sort.direction.toLowerCase() as 'asc' | 'desc',
|
|
1127
|
+
}));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Convert filter query to Prisma include
|
|
1132
|
+
*/
|
|
1133
|
+
static toInclude(query: FilterQuery): Record<string, boolean> | undefined {
|
|
1134
|
+
if (!query.include?.length) return undefined;
|
|
1135
|
+
|
|
1136
|
+
const include: Record<string, boolean> = {};
|
|
1137
|
+
for (const relation of query.include) {
|
|
1138
|
+
include[relation] = true;
|
|
1139
|
+
}
|
|
1140
|
+
return include;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Convert filter query to Prisma select
|
|
1145
|
+
*/
|
|
1146
|
+
static toSelect(query: FilterQuery): Record<string, boolean> | undefined {
|
|
1147
|
+
if (!query.select?.length) return undefined;
|
|
1148
|
+
|
|
1149
|
+
const select: Record<string, boolean> = {};
|
|
1150
|
+
for (const field of query.select) {
|
|
1151
|
+
select[field] = true;
|
|
1152
|
+
}
|
|
1153
|
+
return select;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Build complete Prisma query args
|
|
1158
|
+
*/
|
|
1159
|
+
static toQueryArgs(query: FilterQuery): {
|
|
1160
|
+
where?: Record<string, any>;
|
|
1161
|
+
orderBy?: Record<string, 'asc' | 'desc'>[];
|
|
1162
|
+
include?: Record<string, boolean>;
|
|
1163
|
+
select?: Record<string, boolean>;
|
|
1164
|
+
skip?: number;
|
|
1165
|
+
take?: number;
|
|
1166
|
+
} {
|
|
1167
|
+
const args: any = {};
|
|
1168
|
+
|
|
1169
|
+
const where = this.toWhereClause(query);
|
|
1170
|
+
if (Object.keys(where).length > 0) {
|
|
1171
|
+
args.where = where;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const orderBy = this.toOrderBy(query);
|
|
1175
|
+
if (orderBy.length > 0) {
|
|
1176
|
+
args.orderBy = orderBy;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const include = this.toInclude(query);
|
|
1180
|
+
if (include) {
|
|
1181
|
+
args.include = include;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const select = this.toSelect(query);
|
|
1185
|
+
if (select) {
|
|
1186
|
+
args.select = select;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (query.pagination) {
|
|
1190
|
+
args.skip = query.pagination.offset ?? (query.pagination.page - 1) * query.pagination.limit;
|
|
1191
|
+
args.take = query.pagination.limit;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return args;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Execute query and return paginated result
|
|
1199
|
+
*/
|
|
1200
|
+
static async execute<T>(
|
|
1201
|
+
model: any,
|
|
1202
|
+
query: FilterQuery
|
|
1203
|
+
): Promise<FilterResult<T>> {
|
|
1204
|
+
const args = this.toQueryArgs(query);
|
|
1205
|
+
|
|
1206
|
+
const [data, total] = await Promise.all([
|
|
1207
|
+
model.findMany(args),
|
|
1208
|
+
model.count({ where: args.where }),
|
|
1209
|
+
]);
|
|
1210
|
+
|
|
1211
|
+
const page = query.pagination?.page || 1;
|
|
1212
|
+
const limit = query.pagination?.limit || data.length;
|
|
1213
|
+
const totalPages = Math.ceil(total / limit);
|
|
1214
|
+
|
|
1215
|
+
return {
|
|
1216
|
+
data,
|
|
1217
|
+
total,
|
|
1218
|
+
page,
|
|
1219
|
+
limit,
|
|
1220
|
+
totalPages,
|
|
1221
|
+
hasNext: page < totalPages,
|
|
1222
|
+
hasPrev: page > 1,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
`;
|
|
1227
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'prisma.adapter.ts'), content);
|
|
1228
|
+
console.log(chalk_1.default.green(' ✓ Prisma adapter'));
|
|
1229
|
+
}
|
|
1230
|
+
async function generateFilterDTOTemplates(filtersPath) {
|
|
1231
|
+
const content = `import { IsOptional, IsString, IsInt, Min, Max, IsIn, IsArray, ValidateNested } from 'class-validator';
|
|
1232
|
+
import { Type, Transform } from 'class-transformer';
|
|
1233
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Base pagination query DTO
|
|
1237
|
+
*/
|
|
1238
|
+
export class PaginationQueryDto {
|
|
1239
|
+
@ApiPropertyOptional({ minimum: 1, default: 1 })
|
|
1240
|
+
@IsOptional()
|
|
1241
|
+
@Type(() => Number)
|
|
1242
|
+
@IsInt()
|
|
1243
|
+
@Min(1)
|
|
1244
|
+
page?: number = 1;
|
|
1245
|
+
|
|
1246
|
+
@ApiPropertyOptional({ minimum: 1, maximum: 100, default: 10 })
|
|
1247
|
+
@IsOptional()
|
|
1248
|
+
@Type(() => Number)
|
|
1249
|
+
@IsInt()
|
|
1250
|
+
@Min(1)
|
|
1251
|
+
@Max(100)
|
|
1252
|
+
limit?: number = 10;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Base sorting query DTO
|
|
1257
|
+
*/
|
|
1258
|
+
export class SortQueryDto {
|
|
1259
|
+
@ApiPropertyOptional({ description: 'Field to sort by' })
|
|
1260
|
+
@IsOptional()
|
|
1261
|
+
@IsString()
|
|
1262
|
+
sortBy?: string;
|
|
1263
|
+
|
|
1264
|
+
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'ASC' })
|
|
1265
|
+
@IsOptional()
|
|
1266
|
+
@IsIn(['ASC', 'DESC', 'asc', 'desc'])
|
|
1267
|
+
@Transform(({ value }) => value?.toUpperCase())
|
|
1268
|
+
sortOrder?: 'ASC' | 'DESC' = 'ASC';
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Base search query DTO
|
|
1273
|
+
*/
|
|
1274
|
+
export class SearchQueryDto {
|
|
1275
|
+
@ApiPropertyOptional({ description: 'Search query string' })
|
|
1276
|
+
@IsOptional()
|
|
1277
|
+
@IsString()
|
|
1278
|
+
q?: string;
|
|
1279
|
+
|
|
1280
|
+
@ApiPropertyOptional({ description: 'Fields to search in', type: [String] })
|
|
1281
|
+
@IsOptional()
|
|
1282
|
+
@IsArray()
|
|
1283
|
+
@IsString({ each: true })
|
|
1284
|
+
@Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
|
|
1285
|
+
searchFields?: string[];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Combined filter query DTO
|
|
1290
|
+
*/
|
|
1291
|
+
export class BaseFilterQueryDto extends PaginationQueryDto {
|
|
1292
|
+
@ApiPropertyOptional({ description: 'Field to sort by' })
|
|
1293
|
+
@IsOptional()
|
|
1294
|
+
@IsString()
|
|
1295
|
+
sortBy?: string;
|
|
1296
|
+
|
|
1297
|
+
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'ASC' })
|
|
1298
|
+
@IsOptional()
|
|
1299
|
+
@IsIn(['ASC', 'DESC', 'asc', 'desc'])
|
|
1300
|
+
@Transform(({ value }) => value?.toUpperCase())
|
|
1301
|
+
sortOrder?: 'ASC' | 'DESC' = 'ASC';
|
|
1302
|
+
|
|
1303
|
+
@ApiPropertyOptional({ description: 'Search query string' })
|
|
1304
|
+
@IsOptional()
|
|
1305
|
+
@IsString()
|
|
1306
|
+
search?: string;
|
|
1307
|
+
|
|
1308
|
+
@ApiPropertyOptional({ description: 'Relations to include' })
|
|
1309
|
+
@IsOptional()
|
|
1310
|
+
@IsArray()
|
|
1311
|
+
@IsString({ each: true })
|
|
1312
|
+
@Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
|
|
1313
|
+
include?: string[];
|
|
1314
|
+
|
|
1315
|
+
@ApiPropertyOptional({ description: 'Fields to select' })
|
|
1316
|
+
@IsOptional()
|
|
1317
|
+
@IsArray()
|
|
1318
|
+
@IsString({ each: true })
|
|
1319
|
+
@Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
|
|
1320
|
+
select?: string[];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Date range filter DTO
|
|
1325
|
+
*/
|
|
1326
|
+
export class DateRangeFilterDto {
|
|
1327
|
+
@ApiPropertyOptional({ description: 'Start date' })
|
|
1328
|
+
@IsOptional()
|
|
1329
|
+
@Type(() => Date)
|
|
1330
|
+
from?: Date;
|
|
1331
|
+
|
|
1332
|
+
@ApiPropertyOptional({ description: 'End date' })
|
|
1333
|
+
@IsOptional()
|
|
1334
|
+
@Type(() => Date)
|
|
1335
|
+
to?: Date;
|
|
1336
|
+
|
|
1337
|
+
@ApiPropertyOptional({
|
|
1338
|
+
description: 'Preset date range',
|
|
1339
|
+
enum: ['today', 'yesterday', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth', 'last7Days', 'last30Days'],
|
|
1340
|
+
})
|
|
1341
|
+
@IsOptional()
|
|
1342
|
+
@IsString()
|
|
1343
|
+
preset?: string;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Numeric range filter DTO
|
|
1348
|
+
*/
|
|
1349
|
+
export class NumericRangeFilterDto {
|
|
1350
|
+
@ApiPropertyOptional({ description: 'Minimum value' })
|
|
1351
|
+
@IsOptional()
|
|
1352
|
+
@Type(() => Number)
|
|
1353
|
+
min?: number;
|
|
1354
|
+
|
|
1355
|
+
@ApiPropertyOptional({ description: 'Maximum value' })
|
|
1356
|
+
@IsOptional()
|
|
1357
|
+
@Type(() => Number)
|
|
1358
|
+
max?: number;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* String filter DTO
|
|
1363
|
+
*/
|
|
1364
|
+
export class StringFilterDto {
|
|
1365
|
+
@ApiPropertyOptional({ description: 'Exact match' })
|
|
1366
|
+
@IsOptional()
|
|
1367
|
+
@IsString()
|
|
1368
|
+
eq?: string;
|
|
1369
|
+
|
|
1370
|
+
@ApiPropertyOptional({ description: 'Contains substring' })
|
|
1371
|
+
@IsOptional()
|
|
1372
|
+
@IsString()
|
|
1373
|
+
contains?: string;
|
|
1374
|
+
|
|
1375
|
+
@ApiPropertyOptional({ description: 'Starts with' })
|
|
1376
|
+
@IsOptional()
|
|
1377
|
+
@IsString()
|
|
1378
|
+
startsWith?: string;
|
|
1379
|
+
|
|
1380
|
+
@ApiPropertyOptional({ description: 'Ends with' })
|
|
1381
|
+
@IsOptional()
|
|
1382
|
+
@IsString()
|
|
1383
|
+
endsWith?: string;
|
|
1384
|
+
|
|
1385
|
+
@ApiPropertyOptional({ description: 'In list', type: [String] })
|
|
1386
|
+
@IsOptional()
|
|
1387
|
+
@IsArray()
|
|
1388
|
+
@IsString({ each: true })
|
|
1389
|
+
in?: string[];
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Factory to create entity-specific filter DTOs
|
|
1394
|
+
*/
|
|
1395
|
+
export function createFilterDto<T>(
|
|
1396
|
+
entityName: string,
|
|
1397
|
+
filterableFields: (keyof T)[]
|
|
1398
|
+
): any {
|
|
1399
|
+
// This is a placeholder - actual implementation would use
|
|
1400
|
+
// class-transformer and class-validator dynamically
|
|
1401
|
+
return BaseFilterQueryDto;
|
|
1402
|
+
}
|
|
1403
|
+
`;
|
|
1404
|
+
await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.dto.ts'), content);
|
|
1405
|
+
console.log(chalk_1.default.green(' ✓ Filter DTOs'));
|
|
1406
|
+
}
|
|
1407
|
+
//# sourceMappingURL=filter-dsl.js.map
|