@postxl/generators 1.4.1 → 1.5.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/dist/backend-rest-api/generators/model-controller.generator.js +109 -11
- package/dist/backend-rest-api/generators/model-controller.generator.js.map +1 -1
- package/dist/backend-rest-api/rest-api.generator.js +4 -0
- package/dist/backend-rest-api/rest-api.generator.js.map +1 -1
- package/dist/backend-rest-api/template/utils/src/fieldSelection.spec.ts +252 -0
- package/dist/backend-rest-api/template/utils/src/fieldSelection.ts +66 -0
- package/dist/backend-router-trpc/generators/model-routes.generator.js +27 -2
- package/dist/backend-router-trpc/generators/model-routes.generator.js.map +1 -1
- package/dist/backend-router-trpc/router-trpc.generator.d.ts +1 -0
- package/dist/backend-router-trpc/router-trpc.generator.js +1 -0
- package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
- package/dist/backend-update/model-update-service.generator.js +145 -8
- package/dist/backend-update/model-update-service.generator.js.map +1 -1
- package/dist/backend-view/model-view-service.generator.js +276 -19
- package/dist/backend-view/model-view-service.generator.js.map +1 -1
- package/dist/backend-view/template/{filter.utils.test.ts → query.utils.test.ts} +101 -1
- package/dist/backend-view/template/query.utils.ts +444 -0
- package/dist/decoders/excel-field-decoder.helper.js +5 -2
- package/dist/decoders/excel-field-decoder.helper.js.map +1 -1
- package/dist/frontend-admin/generators/model-admin-page.generator.js +50 -32
- package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
- package/dist/frontend-core/template/src/components/admin/column-header-state-icon.tsx +72 -0
- package/dist/frontend-core/template/src/components/admin/table-filter.tsx +195 -43
- package/dist/frontend-tables/generators/model-table.generator.js +108 -29
- package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
- package/dist/frontend-trpc-client/generators/model-hook.generator.js +130 -16
- package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
- package/dist/types/generators/model-type.generator.js +97 -31
- package/dist/types/generators/model-type.generator.js.map +1 -1
- package/dist/types/template/query.types.ts +151 -0
- package/dist/types/types.generator.d.ts +15 -0
- package/dist/types/types.generator.js +5 -3
- package/dist/types/types.generator.js.map +1 -1
- package/package.json +2 -2
- package/dist/backend-rest-api/generators/zod-exception-filter.generator.d.ts +0 -1
- package/dist/backend-rest-api/generators/zod-exception-filter.generator.js +0 -28
- package/dist/backend-rest-api/generators/zod-exception-filter.generator.js.map +0 -1
- package/dist/backend-view/template/filter.utils.ts +0 -218
- package/dist/frontend-core/template/src/components/admin/table-filter-header-icon.tsx +0 -30
- package/dist/types/template/filter.types.ts +0 -70
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { DateFilter, FilterConfigItem, FilterState, NumberFilter, RelationFilterConfig } from '@types'
|
|
2
|
-
|
|
3
|
-
export function matchFilter(itemValue: any, filterValues: any[] | undefined): boolean {
|
|
4
|
-
if (!filterValues || filterValues.length === 0) {
|
|
5
|
-
return true
|
|
6
|
-
}
|
|
7
|
-
if (itemValue === null || itemValue === undefined || itemValue === '') {
|
|
8
|
-
return filterValues.includes('')
|
|
9
|
-
}
|
|
10
|
-
const itemString = itemValue instanceof Date ? itemValue.toISOString() : String(itemValue)
|
|
11
|
-
return filterValues.some((f) => String(f) === itemString)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function matchNumberFilter(itemValue: number | null, filter: NumberFilter | undefined): boolean {
|
|
15
|
-
if (!filter) {
|
|
16
|
-
return true
|
|
17
|
-
}
|
|
18
|
-
const { values, min, max } = filter
|
|
19
|
-
|
|
20
|
-
// Check range
|
|
21
|
-
if (itemValue == null) {
|
|
22
|
-
// If entry is empty, it fails range check if min or max is set
|
|
23
|
-
if ((min !== undefined && min !== null) || (max !== undefined && max !== null)) {
|
|
24
|
-
return false
|
|
25
|
-
}
|
|
26
|
-
} else {
|
|
27
|
-
if (min !== undefined && min !== null && itemValue < min) {
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
if (max !== undefined && max !== null && itemValue > max) {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return matchFilter(itemValue, values)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function matchDateFilter(itemValue: Date | null, filter: DateFilter | undefined): boolean {
|
|
39
|
-
if (!filter) {
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
const { values, start, end } = filter
|
|
43
|
-
const itemString = itemValue?.toISOString() ?? null
|
|
44
|
-
|
|
45
|
-
// Check range
|
|
46
|
-
if (itemString) {
|
|
47
|
-
if (start && itemString < start) {
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
if (end && itemString > end) {
|
|
51
|
-
return false
|
|
52
|
-
}
|
|
53
|
-
} else if (start || end) {
|
|
54
|
-
// If entry is empty, it fails range check if start or end is set
|
|
55
|
-
return false
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return matchFilter(itemValue, values)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Applies range filters for number and date fields.
|
|
63
|
-
*/
|
|
64
|
-
export function applyRangeFilters<T, TFilters extends FilterState>(
|
|
65
|
-
items: T[],
|
|
66
|
-
accessor: (item: T) => any,
|
|
67
|
-
config: FilterConfigItem<TFilters>,
|
|
68
|
-
filters: TFilters,
|
|
69
|
-
): T[] {
|
|
70
|
-
let filteredItems = items
|
|
71
|
-
if (config.valueType === 'number') {
|
|
72
|
-
const filter = filters[config.filterKey] as NumberFilter | undefined
|
|
73
|
-
if (filter) {
|
|
74
|
-
const { min, max } = filter
|
|
75
|
-
if (min !== undefined || max !== undefined) {
|
|
76
|
-
// Apply range filter manually. We only check the range part here, ignoring 'values'
|
|
77
|
-
filteredItems = filteredItems.filter((item) => matchNumberFilter(accessor(item), { min, max }))
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} else if (config.valueType === 'date') {
|
|
81
|
-
const filter = filters[config.filterKey] as DateFilter | undefined
|
|
82
|
-
if (filter) {
|
|
83
|
-
const { start, end } = filter
|
|
84
|
-
if (start !== undefined || end !== undefined) {
|
|
85
|
-
// Apply range filter manually. We only check the range part here, ignoring 'values'
|
|
86
|
-
filteredItems = filteredItems.filter((item) => matchDateFilter(accessor(item), { start, end }))
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return filteredItems
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Collects matches from the filtered items.
|
|
95
|
-
*/
|
|
96
|
-
export function getMatches<T>(filteredItems: T[], accessor: (item: T) => any): Set<string> {
|
|
97
|
-
const matches = new Set<string>()
|
|
98
|
-
for (const item of filteredItems) {
|
|
99
|
-
const val = accessor(item)
|
|
100
|
-
if (val === null || val === undefined || val === '') {
|
|
101
|
-
matches.add('')
|
|
102
|
-
} else if (val instanceof Date) {
|
|
103
|
-
matches.add(val.toISOString())
|
|
104
|
-
} else {
|
|
105
|
-
matches.add(String(val))
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return matches
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generates all possible options based on field type.
|
|
113
|
-
*/
|
|
114
|
-
export function getOptions<T>(
|
|
115
|
-
allItems: T[],
|
|
116
|
-
accessor: (item: T) => any,
|
|
117
|
-
config: FilterConfigItem<any>,
|
|
118
|
-
referencedItems?: Map<string, any>,
|
|
119
|
-
): { value: string; label: string }[] {
|
|
120
|
-
if (config.kind === 'relation') {
|
|
121
|
-
return referencedItems ? getRelationOptions(allItems, accessor, config, referencedItems) : []
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (config.kind === 'enum') {
|
|
125
|
-
return config.enumValues.map((s: string) => ({ value: s, label: s }))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (config.kind === 'discriminatedUnion') {
|
|
129
|
-
return config.duMemberTypes.map((m: { type: string; label: string }) => ({ value: m.type, label: m.label }))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (config.valueType === 'boolean') {
|
|
133
|
-
return [
|
|
134
|
-
{ value: 'true', label: 'Yes' },
|
|
135
|
-
{ value: 'false', label: 'No' },
|
|
136
|
-
]
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return getScalarOptions(allItems, accessor)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Generates relation options from referenced items.
|
|
144
|
-
*/
|
|
145
|
-
function getRelationOptions<T>(
|
|
146
|
-
allItems: T[],
|
|
147
|
-
accessor: (item: T) => any,
|
|
148
|
-
config: RelationFilterConfig<any>,
|
|
149
|
-
referencedItems: Map<string, any>,
|
|
150
|
-
): { value: string; label: string }[] {
|
|
151
|
-
const options: { value: string; label: string }[] = []
|
|
152
|
-
const usedIds = new Set<any>()
|
|
153
|
-
|
|
154
|
-
for (const item of allItems) {
|
|
155
|
-
usedIds.add(accessor(item))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const id of usedIds) {
|
|
159
|
-
if (id === null || id === undefined) {
|
|
160
|
-
options.push({ value: '', label: 'Empty' })
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
const ref = referencedItems.get(id)
|
|
164
|
-
if (ref) {
|
|
165
|
-
const label = String(ref[config.referencedModelLabelField] ?? ref[config.referencedModelIdField])
|
|
166
|
-
options.push({ value: String(ref[config.referencedModelIdField]), label })
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return options
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Generates scalar options from item values.
|
|
175
|
-
*/
|
|
176
|
-
function getScalarOptions<T>(allItems: T[], accessor: (item: T) => any): { value: string; label: string }[] {
|
|
177
|
-
const values = new Set<string>()
|
|
178
|
-
for (const item of allItems) {
|
|
179
|
-
const raw = accessor(item)
|
|
180
|
-
const normalized = raw instanceof Date ? raw.toISOString() : String(raw ?? '')
|
|
181
|
-
values.add(normalized)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const options: { value: string; label: string }[] = []
|
|
185
|
-
for (const v of values) {
|
|
186
|
-
options.push({ value: v, label: v === '' ? 'Empty' : v })
|
|
187
|
-
}
|
|
188
|
-
return options
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Formats and sorts the filter options.
|
|
193
|
-
*/
|
|
194
|
-
export function formatAndSortOptions(
|
|
195
|
-
options: { value: string; label: string }[],
|
|
196
|
-
matches: Set<string>,
|
|
197
|
-
): { value: string; label: string; hasMatches: boolean }[] {
|
|
198
|
-
return options
|
|
199
|
-
.map((opt) => ({
|
|
200
|
-
...opt,
|
|
201
|
-
hasMatches: matches.has(opt.value),
|
|
202
|
-
}))
|
|
203
|
-
.sort((a, b) => {
|
|
204
|
-
if (a.hasMatches && !b.hasMatches) {
|
|
205
|
-
return -1
|
|
206
|
-
}
|
|
207
|
-
if (!a.hasMatches && b.hasMatches) {
|
|
208
|
-
return 1
|
|
209
|
-
}
|
|
210
|
-
if (a.value === '' && b.value !== '') {
|
|
211
|
-
return -1
|
|
212
|
-
}
|
|
213
|
-
if (a.value !== '' && b.value === '') {
|
|
214
|
-
return 1
|
|
215
|
-
}
|
|
216
|
-
return a.label.localeCompare(b.label)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { FilterIcon } from 'lucide-react'
|
|
2
|
-
import { DateFilter, FilterValue, NumberFilter } from '@types'
|
|
3
|
-
|
|
4
|
-
export const TableFilterHeaderIcon = ({ filterValue }: { filterValue: FilterValue }) => {
|
|
5
|
-
let isActive = false
|
|
6
|
-
|
|
7
|
-
if (filterValue !== null && filterValue !== undefined) {
|
|
8
|
-
if (Array.isArray(filterValue)) {
|
|
9
|
-
isActive = filterValue.length > 0
|
|
10
|
-
} else if (typeof filterValue === 'object') {
|
|
11
|
-
const obj = filterValue as NumberFilter | DateFilter
|
|
12
|
-
isActive =
|
|
13
|
-
(Array.isArray(obj.values) && obj.values.length > 0) ||
|
|
14
|
-
('min' in obj && obj.min !== null) ||
|
|
15
|
-
('max' in obj && obj.max !== null) ||
|
|
16
|
-
('start' in obj && !!obj.start) ||
|
|
17
|
-
('end' in obj && !!obj.end)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!isActive) {
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="absolute top-1/2 translate-y-[calc(-50%-2.5px)] right-[13.5px] flex items-center justify-center p-0 size-5.5 rounded-full pointer-events-none">
|
|
27
|
-
<FilterIcon className="size-4 text-muted-foreground/60" />
|
|
28
|
-
</div>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export type NumberFilter = {
|
|
4
|
-
values?: (number | string)[]
|
|
5
|
-
min?: number | null
|
|
6
|
-
max?: number | null
|
|
7
|
-
}
|
|
8
|
-
export type DateFilter = {
|
|
9
|
-
values?: string[]
|
|
10
|
-
start?: string | null
|
|
11
|
-
end?: string | null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const zNumberFilter = z.object({
|
|
15
|
-
values: z.array(z.union([z.number(), z.string()])).optional(),
|
|
16
|
-
min: z.number().nullable().optional(),
|
|
17
|
-
max: z.number().nullable().optional(),
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export const zDateFilter = z.object({
|
|
21
|
-
values: z.array(z.string()).optional(),
|
|
22
|
-
start: z.string().nullable().optional(),
|
|
23
|
-
end: z.string().nullable().optional(),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export type FilterFieldType = 'string' | 'number' | 'date' | 'boolean'
|
|
27
|
-
|
|
28
|
-
export type FilterValue = string[] | boolean[] | NumberFilter | DateFilter | undefined
|
|
29
|
-
|
|
30
|
-
export type FilterFieldName = string & { __brand?: 'FilterFieldName' }
|
|
31
|
-
|
|
32
|
-
export type FilterState = Record<FilterFieldName, FilterValue>
|
|
33
|
-
|
|
34
|
-
type BaseFilterConfig<TFilters extends FilterState> = {
|
|
35
|
-
filterKey: keyof TFilters
|
|
36
|
-
valueType: FilterFieldType
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type RelationFilterConfig<TFilters extends FilterState> = BaseFilterConfig<TFilters> & {
|
|
40
|
-
kind: 'relation'
|
|
41
|
-
referencedModelViewServiceVariableName: string
|
|
42
|
-
referencedModelLabelField: string
|
|
43
|
-
referencedModelIdField: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type EnumFilterConfig<TFilters extends FilterState> = BaseFilterConfig<TFilters> & {
|
|
47
|
-
kind: 'enum'
|
|
48
|
-
enumName: string
|
|
49
|
-
enumValues: string[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type DiscriminatedUnionFilterConfig<TFilters extends FilterState> = BaseFilterConfig<TFilters> & {
|
|
53
|
-
kind: 'discriminatedUnion'
|
|
54
|
-
duMemberTypes: { type: string; label: string }[]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type ScalarFilterConfig<TFilters extends FilterState> = BaseFilterConfig<TFilters> & {
|
|
58
|
-
kind: 'scalar' | 'id'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export type FilterConfigItem<TFilters extends FilterState> =
|
|
62
|
-
| RelationFilterConfig<TFilters>
|
|
63
|
-
| EnumFilterConfig<TFilters>
|
|
64
|
-
| DiscriminatedUnionFilterConfig<TFilters>
|
|
65
|
-
| ScalarFilterConfig<TFilters>
|
|
66
|
-
|
|
67
|
-
export type FilterConfig<TField extends FilterFieldName, TFilters extends FilterState> = Record<
|
|
68
|
-
TField,
|
|
69
|
-
FilterConfigItem<TFilters>
|
|
70
|
-
>
|