@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.
Files changed (40) hide show
  1. package/dist/backend-rest-api/generators/model-controller.generator.js +109 -11
  2. package/dist/backend-rest-api/generators/model-controller.generator.js.map +1 -1
  3. package/dist/backend-rest-api/rest-api.generator.js +4 -0
  4. package/dist/backend-rest-api/rest-api.generator.js.map +1 -1
  5. package/dist/backend-rest-api/template/utils/src/fieldSelection.spec.ts +252 -0
  6. package/dist/backend-rest-api/template/utils/src/fieldSelection.ts +66 -0
  7. package/dist/backend-router-trpc/generators/model-routes.generator.js +27 -2
  8. package/dist/backend-router-trpc/generators/model-routes.generator.js.map +1 -1
  9. package/dist/backend-router-trpc/router-trpc.generator.d.ts +1 -0
  10. package/dist/backend-router-trpc/router-trpc.generator.js +1 -0
  11. package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
  12. package/dist/backend-update/model-update-service.generator.js +145 -8
  13. package/dist/backend-update/model-update-service.generator.js.map +1 -1
  14. package/dist/backend-view/model-view-service.generator.js +276 -19
  15. package/dist/backend-view/model-view-service.generator.js.map +1 -1
  16. package/dist/backend-view/template/{filter.utils.test.ts → query.utils.test.ts} +101 -1
  17. package/dist/backend-view/template/query.utils.ts +444 -0
  18. package/dist/decoders/excel-field-decoder.helper.js +5 -2
  19. package/dist/decoders/excel-field-decoder.helper.js.map +1 -1
  20. package/dist/frontend-admin/generators/model-admin-page.generator.js +50 -32
  21. package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
  22. package/dist/frontend-core/template/src/components/admin/column-header-state-icon.tsx +72 -0
  23. package/dist/frontend-core/template/src/components/admin/table-filter.tsx +195 -43
  24. package/dist/frontend-tables/generators/model-table.generator.js +108 -29
  25. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  26. package/dist/frontend-trpc-client/generators/model-hook.generator.js +130 -16
  27. package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
  28. package/dist/types/generators/model-type.generator.js +97 -31
  29. package/dist/types/generators/model-type.generator.js.map +1 -1
  30. package/dist/types/template/query.types.ts +151 -0
  31. package/dist/types/types.generator.d.ts +15 -0
  32. package/dist/types/types.generator.js +5 -3
  33. package/dist/types/types.generator.js.map +1 -1
  34. package/package.json +2 -2
  35. package/dist/backend-rest-api/generators/zod-exception-filter.generator.d.ts +0 -1
  36. package/dist/backend-rest-api/generators/zod-exception-filter.generator.js +0 -28
  37. package/dist/backend-rest-api/generators/zod-exception-filter.generator.js.map +0 -1
  38. package/dist/backend-view/template/filter.utils.ts +0 -218
  39. package/dist/frontend-core/template/src/components/admin/table-filter-header-icon.tsx +0 -30
  40. 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
- >