@kematjaya/crud-ui-generator 0.1.1 → 0.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/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { appendBlock, appendBlockWithImport, newLog, writeIfMissing, writeNewFil
6
6
  import * as shared from './templates/sharedComponents.js';
7
7
  import { editPage, listPage, newPage } from './templates/pages.js';
8
8
  import { table } from './templates/table.js';
9
+ import { entityHook } from './templates/hook.js';
9
10
  import { form } from './templates/form.js';
10
11
  import { useExportHook } from './templates/useExport.js';
11
12
  import { queryLib } from './templates/queryLib.js';
@@ -81,6 +82,7 @@ function main() {
81
82
  writeNewFile(join(compDir, `${names.entityPascal}Table.tsx`), table(spec, names), log);
82
83
  writeNewFile(join(compDir, `${names.entityPascal}Form.tsx`), form(spec, names), log);
83
84
  writeNewFile(join(compDir, `use${names.entitiesPascal}Export.ts`), useExportHook(spec, names), log);
85
+ writeNewFile(join(compDir, `use${names.entitiesPascal}.ts`), entityHook(spec, names), log);
84
86
  const libDir = join(src, 'lib');
85
87
  writeNewFile(join(libDir, `${names.entitiesKebab}-query.ts`), queryLib(spec, names), log);
86
88
  writeNewFile(join(libDir, `${names.entitiesKebab}-csv.ts`), csvLib(spec, names), log);
@@ -0,0 +1,269 @@
1
+ import { lowerWords, searchField } from '../naming.js';
2
+ export function entityHook(spec, names) {
3
+ const { entityPascal, entitiesPascal, entitiesKebab } = names;
4
+ const field = searchField(spec);
5
+ const pluralNoun = lowerWords(entitiesPascal);
6
+ const hasSearchLine = field
7
+ ? ` const hasSearch = Boolean(query.search);\n`
8
+ : ` const hasSearch = false;\n`;
9
+ return `import { useRouter, useSearchParams } from 'next/navigation';
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
+ import { is${entityPascal}Collection } from '@/lib/api-shapes';
12
+ import {
13
+ build${entitiesPascal}ApiPath,
14
+ build${entitiesPascal}Href,
15
+ clampPageToTotal,
16
+ parse${entitiesPascal}Query,
17
+ type ${entitiesPascal}Query
18
+ } from '@/lib/${entitiesKebab}-query';
19
+ import type { ${entityPascal} } from '@/types/api';
20
+ import { use${entitiesPascal}Export } from './use${entitiesPascal}Export';
21
+
22
+ export function use${entitiesPascal}() {
23
+ const router = useRouter();
24
+ const searchParams = useSearchParams();
25
+ const query = useMemo(() => parse${entitiesPascal}Query(searchParams), [searchParams]);
26
+ const [items, setItems] = useState<${entityPascal}[]>([]);
27
+ const [totalItems, setTotalItems] = useState(0);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState('');
30
+ const [deleteTarget, setDeleteTarget] = useState<${entityPascal} | null>(null);
31
+ const [deleting, setDeleting] = useState(false);
32
+ const [selectedIds, setSelectedIds] = useState<Set<${entityPascal}['id']>>(new Set());
33
+ const [bulkDeleteConfirming, setBulkDeleteConfirming] = useState(false);
34
+ const [bulkDeleting, setBulkDeleting] = useState(false);
35
+ const [toastMessage, setToastMessage] = useState(
36
+ () => searchParams.get('toast') ?? ''
37
+ );
38
+ const { exportingSelected, exportingAll, exportSelected, exportAll } =
39
+ use${entitiesPascal}Export();
40
+ const requestId = useRef(0);
41
+
42
+ const loadItems = useCallback(
43
+ async (signal?: AbortSignal) => {
44
+ const id = requestId.current + 1;
45
+ requestId.current = id;
46
+ setError('');
47
+ setLoading(true);
48
+ setItems([]);
49
+ setTotalItems(0);
50
+ let response: Response;
51
+ try {
52
+ response = await fetch(build${entitiesPascal}ApiPath(query), {
53
+ cache: 'no-store',
54
+ signal
55
+ });
56
+ } catch (fetchError) {
57
+ if (
58
+ fetchError instanceof DOMException &&
59
+ fetchError.name === 'AbortError'
60
+ )
61
+ return;
62
+ if (requestId.current !== id) return;
63
+ setError('Network error while loading ${pluralNoun}.');
64
+ setLoading(false);
65
+ return;
66
+ }
67
+ if (requestId.current !== id || signal?.aborted) return;
68
+ if (response.status === 401) {
69
+ router.push('/login');
70
+ return;
71
+ }
72
+ if (!response.ok) {
73
+ setError('Could not load ${pluralNoun}.');
74
+ setLoading(false);
75
+ return;
76
+ }
77
+ const data: unknown = await response.json();
78
+ if (!is${entityPascal}Collection(data)) {
79
+ setError('Unexpected response.');
80
+ setLoading(false);
81
+ return;
82
+ }
83
+ const clampedPage = clampPageToTotal(
84
+ query.page,
85
+ query.itemsPerPage,
86
+ data.totalItems
87
+ );
88
+ if (clampedPage !== query.page) {
89
+ router.replace(build${entitiesPascal}Href(query, { page: clampedPage }));
90
+ return;
91
+ }
92
+ setItems(data.member);
93
+ setTotalItems(data.totalItems);
94
+ setLoading(false);
95
+ },
96
+ [query, router]
97
+ );
98
+
99
+ useEffect(() => {
100
+ const controller = new AbortController();
101
+ const timer = window.setTimeout(
102
+ () => void loadItems(controller.signal),
103
+ 0
104
+ );
105
+ return () => {
106
+ window.clearTimeout(timer);
107
+ controller.abort();
108
+ };
109
+ }, [loadItems]);
110
+
111
+ const [selectionQuery, setSelectionQuery] = useState(query);
112
+ if (selectionQuery !== query) {
113
+ setSelectionQuery(query);
114
+ setSelectedIds(new Set());
115
+ }
116
+
117
+ function navigateTo(nextQuery: ${entitiesPascal}Query) {
118
+ router.push(build${entitiesPascal}Href(nextQuery));
119
+ }
120
+
121
+ ${hasSearchLine} const trueEmpty = !loading && !error && !hasSearch && totalItems === 0;
122
+ const filteredEmpty = !loading && !error && hasSearch && totalItems === 0;
123
+ const hasItems = !loading && items.length > 0;
124
+ const selectedItems = useMemo(
125
+ () => items.filter((item) => selectedIds.has(item.id)),
126
+ [items, selectedIds]
127
+ );
128
+ const allOnPageSelected =
129
+ items.length > 0 && items.every((item) => selectedIds.has(item.id));
130
+ const someOnPageSelected = items.some((item) => selectedIds.has(item.id));
131
+
132
+ function toggleSelect(id: ${entityPascal}['id']) {
133
+ setSelectedIds((prev) => {
134
+ const next = new Set(prev);
135
+ if (next.has(id)) next.delete(id);
136
+ else next.add(id);
137
+ return next;
138
+ });
139
+ }
140
+
141
+ function toggleSelectAll() {
142
+ setSelectedIds((prev) => {
143
+ const allSelected =
144
+ items.length > 0 && items.every((item) => prev.has(item.id));
145
+ return allSelected
146
+ ? new Set()
147
+ : new Set(items.map((item) => item.id));
148
+ });
149
+ }
150
+
151
+ function clearSelection() {
152
+ setSelectedIds(new Set());
153
+ }
154
+
155
+ async function confirmDelete() {
156
+ if (!deleteTarget) return;
157
+ setDeleting(true);
158
+ let response: Response;
159
+ try {
160
+ response = await fetch(\`/api/${entitiesKebab}/\${deleteTarget.id}\`, {
161
+ method: 'DELETE'
162
+ });
163
+ } catch {
164
+ setError('Network error while deleting.');
165
+ setDeleting(false);
166
+ return;
167
+ }
168
+ setDeleting(false);
169
+ if (!response.ok && response.status !== 204) {
170
+ setError('Could not delete.');
171
+ return;
172
+ }
173
+ setDeleteTarget(null);
174
+ setToastMessage('Deleted.');
175
+ await loadItems();
176
+ }
177
+
178
+ async function confirmBulkDelete() {
179
+ const ids = Array.from(selectedIds);
180
+ if (ids.length === 0) return;
181
+ setBulkDeleting(true);
182
+ const outcomes = await Promise.allSettled(
183
+ ids.map(async (id) => {
184
+ const response = await fetch(\`/api/${entitiesKebab}/\${id}\`, {
185
+ method: 'DELETE'
186
+ });
187
+ if (!response.ok && response.status !== 204) {
188
+ throw new Error('delete failed');
189
+ }
190
+ })
191
+ );
192
+ setBulkDeleting(false);
193
+ setBulkDeleteConfirming(false);
194
+ const failedIds = ids.filter(
195
+ (_, index) => outcomes[index]?.status === 'rejected'
196
+ );
197
+ const deletedCount = ids.length - failedIds.length;
198
+ if (failedIds.length === ids.length) {
199
+ setError('Could not delete selected ${pluralNoun}.');
200
+ } else if (failedIds.length > 0) {
201
+ setError(
202
+ \`Deleted \${deletedCount} of \${ids.length}. Some deletions failed.\`
203
+ );
204
+ setToastMessage(\`\${deletedCount} deleted.\`);
205
+ } else {
206
+ setToastMessage(\`\${deletedCount} deleted.\`);
207
+ }
208
+ setSelectedIds(new Set(failedIds));
209
+ await loadItems();
210
+ }
211
+
212
+ function handleExportSelected() {
213
+ setError('');
214
+ if (0 === selectedItems.length) return;
215
+ const result = exportSelected(selectedItems);
216
+ if (result.ok) {
217
+ setSelectedIds(new Set());
218
+ return;
219
+ }
220
+ setError(result.message);
221
+ }
222
+
223
+ async function handleExportAll() {
224
+ setError('');
225
+ const result = await exportAll(query);
226
+ if (result.ok) return;
227
+ if (401 === result.status) {
228
+ router.push('/login');
229
+ return;
230
+ }
231
+ setError(result.message);
232
+ }
233
+
234
+ return {
235
+ query,
236
+ items,
237
+ totalItems,
238
+ loading,
239
+ error,
240
+ hasSearch,
241
+ trueEmpty,
242
+ filteredEmpty,
243
+ hasItems,
244
+ navigateTo,
245
+ selectedIds,
246
+ selectedItems,
247
+ allOnPageSelected,
248
+ someOnPageSelected,
249
+ toggleSelect,
250
+ toggleSelectAll,
251
+ clearSelection,
252
+ deleteTarget,
253
+ setDeleteTarget,
254
+ deleting,
255
+ confirmDelete,
256
+ bulkDeleteConfirming,
257
+ setBulkDeleteConfirming,
258
+ bulkDeleting,
259
+ confirmBulkDelete,
260
+ toastMessage,
261
+ setToastMessage,
262
+ exportingSelected,
263
+ exportingAll,
264
+ handleExportSelected,
265
+ handleExportAll
266
+ };
267
+ }
268
+ `;
269
+ }
@@ -33,154 +33,68 @@ export function table(spec, names) {
33
33
  pending={loading}
34
34
  disabled={trueEmpty}
35
35
  onSearch={(search) =>
36
- router.push(build${entitiesPascal}Href(query, { page: 1, search }))
36
+ navigateTo({ ...query, page: 1, search })
37
37
  }
38
38
  onClear={() =>
39
- router.push(build${entitiesPascal}Href(query, { page: 1, search: '' }))
39
+ navigateTo({ ...query, page: 1, search: '' })
40
40
  }
41
41
  />
42
42
 
43
43
  `
44
44
  : '';
45
- const hasTitleFilterLine = field
46
- ? ` const hasSearch = Boolean(query.search);\n`
47
- : ` const hasSearch = false;\n`;
48
45
  return `'use client';
49
46
 
50
47
  import { Button, EmptyState, LinkButton, ListPageCard, Toast } from '@kematjaya/bootstrap-ui-kit';
51
- import { useRouter, useSearchParams } from 'next/navigation';
52
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
48
+ import { useEffect, useRef } from 'react';
53
49
  import { usePermissions } from '@kematjaya/access-control-ui';
54
50
  import { BulkActionsBar } from '@/components/crud/BulkActionsBar';
55
51
  import { DeleteConfirmModal } from '@/components/crud/DeleteConfirmModal';
56
52
  import { ExportAllButton } from '@/components/crud/ExportAllButton';
57
53
  import { PaginationBar } from '@/components/crud/PaginationBar';
58
54
  import { SearchPanel } from '@/components/crud/SearchPanel';
59
- import { is${entityPascal}Collection } from '@/lib/api-shapes';
60
55
  import {
61
56
  ${constPrefix}_PAGE_SIZES,
62
- build${entitiesPascal}ApiPath,
63
57
  build${entitiesPascal}Href,
64
- clampPageToTotal,
65
- nextItemsPerPagePage,
66
- parse${entitiesPascal}Query,
67
- type ${entitiesPascal}Query
58
+ nextItemsPerPagePage
68
59
  } from '@/lib/${entitiesKebab}-query';
69
- import type { ${entityPascal} } from '@/types/api';
70
- import { use${entitiesPascal}Export } from './use${entitiesPascal}Export';
60
+ import { use${entitiesPascal} } from './use${entitiesPascal}';
71
61
 
72
62
  export function ${entityPascal}Table() {
73
- const router = useRouter();
74
- const searchParams = useSearchParams();
75
- const query = useMemo(() => parse${entitiesPascal}Query(searchParams), [searchParams]);
76
- const [items, setItems] = useState<${entityPascal}[]>([]);
77
- const [totalItems, setTotalItems] = useState(0);
78
- const [loading, setLoading] = useState(true);
79
- const [error, setError] = useState('');
80
- const [deleteTarget, setDeleteTarget] = useState<${entityPascal} | null>(null);
81
- const [deleting, setDeleting] = useState(false);
82
- const [selectedIds, setSelectedIds] = useState<Set<${entityPascal}['id']>>(new Set());
83
- const [bulkDeleteConfirming, setBulkDeleteConfirming] = useState(false);
84
- const [bulkDeleting, setBulkDeleting] = useState(false);
85
- const [toastMessage, setToastMessage] = useState(
86
- () => searchParams.get('toast') ?? ''
87
- );
88
- const { exportingSelected, exportingAll, exportSelected, exportAll } =
89
- use${entitiesPascal}Export();
63
+ const {
64
+ query,
65
+ items,
66
+ totalItems,
67
+ loading,
68
+ error,
69
+ hasSearch,
70
+ trueEmpty,
71
+ filteredEmpty,
72
+ hasItems,
73
+ navigateTo,
74
+ selectedIds,
75
+ allOnPageSelected,
76
+ someOnPageSelected,
77
+ toggleSelect,
78
+ toggleSelectAll,
79
+ clearSelection,
80
+ deleteTarget,
81
+ setDeleteTarget,
82
+ deleting,
83
+ confirmDelete,
84
+ bulkDeleteConfirming,
85
+ setBulkDeleteConfirming,
86
+ bulkDeleting,
87
+ confirmBulkDelete,
88
+ toastMessage,
89
+ setToastMessage,
90
+ exportingSelected,
91
+ exportingAll,
92
+ handleExportSelected,
93
+ handleExportAll
94
+ } = use${entitiesPascal}();
90
95
  const { has: hasPermission, loading: permissionsLoading } = usePermissions();
91
- const requestId = useRef(0);
92
96
  const selectAllRef = useRef<HTMLInputElement>(null);
93
97
 
94
- const loadItems = useCallback(
95
- async (signal?: AbortSignal) => {
96
- const id = requestId.current + 1;
97
- requestId.current = id;
98
- setError('');
99
- setLoading(true);
100
- setItems([]);
101
- setTotalItems(0);
102
- let response: Response;
103
- try {
104
- response = await fetch(build${entitiesPascal}ApiPath(query), {
105
- cache: 'no-store',
106
- signal
107
- });
108
- } catch (fetchError) {
109
- if (
110
- fetchError instanceof DOMException &&
111
- fetchError.name === 'AbortError'
112
- )
113
- return;
114
- if (requestId.current !== id) return;
115
- setError('Network error while loading ${pluralNoun}.');
116
- setLoading(false);
117
- return;
118
- }
119
- if (requestId.current !== id || signal?.aborted) return;
120
- if (response.status === 401) {
121
- router.push('/login');
122
- return;
123
- }
124
- if (!response.ok) {
125
- setError('Could not load ${pluralNoun}.');
126
- setLoading(false);
127
- return;
128
- }
129
- const data: unknown = await response.json();
130
- if (!is${entityPascal}Collection(data)) {
131
- setError('Unexpected response.');
132
- setLoading(false);
133
- return;
134
- }
135
- const clampedPage = clampPageToTotal(
136
- query.page,
137
- query.itemsPerPage,
138
- data.totalItems
139
- );
140
- if (clampedPage !== query.page) {
141
- router.replace(build${entitiesPascal}Href(query, { page: clampedPage }));
142
- return;
143
- }
144
- setItems(data.member);
145
- setTotalItems(data.totalItems);
146
- setLoading(false);
147
- },
148
- [query, router]
149
- );
150
-
151
- useEffect(() => {
152
- const controller = new AbortController();
153
- const timer = window.setTimeout(
154
- () => void loadItems(controller.signal),
155
- 0
156
- );
157
- return () => {
158
- window.clearTimeout(timer);
159
- controller.abort();
160
- };
161
- }, [loadItems]);
162
-
163
- const [selectionQuery, setSelectionQuery] = useState(query);
164
- if (selectionQuery !== query) {
165
- setSelectionQuery(query);
166
- setSelectedIds(new Set());
167
- }
168
-
169
- function navigateTo(nextQuery: ${entitiesPascal}Query) {
170
- router.push(build${entitiesPascal}Href(nextQuery));
171
- }
172
-
173
- ${hasTitleFilterLine} const trueEmpty = !loading && !error && !hasSearch && totalItems === 0;
174
- const filteredEmpty = !loading && !error && hasSearch && totalItems === 0;
175
- const hasItems = !loading && items.length > 0;
176
- const selectedItems = useMemo(
177
- () => items.filter((item) => selectedIds.has(item.id)),
178
- [items, selectedIds]
179
- );
180
- const allOnPageSelected =
181
- items.length > 0 && items.every((item) => selectedIds.has(item.id));
182
- const someOnPageSelected = items.some((item) => selectedIds.has(item.id));
183
-
184
98
  useEffect(() => {
185
99
  if (selectAllRef.current) {
186
100
  selectAllRef.current.indeterminate =
@@ -188,104 +102,6 @@ ${hasTitleFilterLine} const trueEmpty = !loading && !error && !hasSearch && t
188
102
  }
189
103
  }, [someOnPageSelected, allOnPageSelected]);
190
104
 
191
- function toggleSelect(id: ${entityPascal}['id']) {
192
- setSelectedIds((prev) => {
193
- const next = new Set(prev);
194
- if (next.has(id)) next.delete(id);
195
- else next.add(id);
196
- return next;
197
- });
198
- }
199
-
200
- function toggleSelectAll() {
201
- setSelectedIds((prev) => {
202
- const allSelected =
203
- items.length > 0 && items.every((item) => prev.has(item.id));
204
- return allSelected
205
- ? new Set()
206
- : new Set(items.map((item) => item.id));
207
- });
208
- }
209
-
210
- async function confirmDelete() {
211
- if (!deleteTarget) return;
212
- setDeleting(true);
213
- let response: Response;
214
- try {
215
- response = await fetch(\`/api/${entitiesKebab}/\${deleteTarget.id}\`, {
216
- method: 'DELETE'
217
- });
218
- } catch {
219
- setError('Network error while deleting.');
220
- setDeleting(false);
221
- return;
222
- }
223
- setDeleting(false);
224
- if (!response.ok && response.status !== 204) {
225
- setError('Could not delete.');
226
- return;
227
- }
228
- setDeleteTarget(null);
229
- setToastMessage('Deleted.');
230
- await loadItems();
231
- }
232
-
233
- async function confirmBulkDelete() {
234
- const ids = Array.from(selectedIds);
235
- if (ids.length === 0) return;
236
- setBulkDeleting(true);
237
- const outcomes = await Promise.allSettled(
238
- ids.map(async (id) => {
239
- const response = await fetch(\`/api/${entitiesKebab}/\${id}\`, {
240
- method: 'DELETE'
241
- });
242
- if (!response.ok && response.status !== 204) {
243
- throw new Error('delete failed');
244
- }
245
- })
246
- );
247
- setBulkDeleting(false);
248
- setBulkDeleteConfirming(false);
249
- const failedIds = ids.filter(
250
- (_, index) => outcomes[index]?.status === 'rejected'
251
- );
252
- const deletedCount = ids.length - failedIds.length;
253
- if (failedIds.length === ids.length) {
254
- setError('Could not delete selected ${pluralNoun}.');
255
- } else if (failedIds.length > 0) {
256
- setError(
257
- \`Deleted \${deletedCount} of \${ids.length}. Some deletions failed.\`
258
- );
259
- setToastMessage(\`\${deletedCount} deleted.\`);
260
- } else {
261
- setToastMessage(\`\${deletedCount} deleted.\`);
262
- }
263
- setSelectedIds(new Set(failedIds));
264
- await loadItems();
265
- }
266
-
267
- function handleExportSelected() {
268
- setError('');
269
- if (0 === selectedItems.length) return;
270
- const result = exportSelected(selectedItems);
271
- if (result.ok) {
272
- setSelectedIds(new Set());
273
- return;
274
- }
275
- setError(result.message);
276
- }
277
-
278
- async function handleExportAll() {
279
- setError('');
280
- const result = await exportAll(query);
281
- if (result.ok) return;
282
- if (401 === result.status) {
283
- router.push('/login');
284
- return;
285
- }
286
- setError(result.message);
287
- }
288
-
289
105
  return (
290
106
  <ListPageCard
291
107
  title="${entitiesPascal}"
@@ -331,9 +147,7 @@ ${searchPanelBlock} {trueEmpty && (
331
147
  <Button
332
148
  variant="outline"
333
149
  onClick={() =>
334
- router.push(
335
- build${entitiesPascal}Href(query, { page: 1${field ? ", search: ''" : ''} })
336
- )
150
+ navigateTo({ ...query, page: 1${field ? ", search: ''" : ''} })
337
151
  }
338
152
  >
339
153
  Clear search
@@ -350,7 +164,7 @@ ${searchPanelBlock} {trueEmpty && (
350
164
  disabled={bulkDeleting || exportingSelected}
351
165
  onDelete={permissionsLoading || hasPermission('${prefix}.bulk_delete') || hasPermission('${prefix}.delete') ? () => setBulkDeleteConfirming(true) : undefined}
352
166
  onExportSelected={permissionsLoading || hasPermission('${prefix}.export_selected') ? handleExportSelected : undefined}
353
- onClearSelection={() => setSelectedIds(new Set())}
167
+ onClearSelection={clearSelection}
354
168
  />
355
169
  )}
356
170
 
@@ -435,9 +249,7 @@ ${bodyCells}
435
249
  totalItems={totalItems}
436
250
  loadedItems={items.length}
437
251
  buildHref={(patch) => build${entitiesPascal}Href(query, patch)}
438
- onPageChange={(page) =>
439
- router.push(build${entitiesPascal}Href(query, { page }))
440
- }
252
+ onPageChange={(page) => navigateTo({ ...query, page })}
441
253
  onPageSizeChange={(itemsPerPage) =>
442
254
  navigateTo({
443
255
  ...query,
@@ -1,13 +1,17 @@
1
1
  import { displayFields } from '../naming.js';
2
2
  /**
3
3
  * Fresh-file bootstrap, only used when src/types/api.ts doesn't exist yet. In the boilerplate
4
- * app this file already exists (written for the Notes feature) with these same JsonLd/Json
5
- * helpers and the `paths`/`components` import from the OpenAPI-generated `./api.generated` —
6
- * this header is only a fallback for a from-scratch project that hasn't set that up yet.
4
+ * app this file already exists (written for the Notes feature) with this same Json helper
5
+ * and the `paths`/`components` import from the OpenAPI-generated `./api.generated` — this
6
+ * header is only a fallback for a from-scratch project that hasn't set that up yet.
7
+ *
8
+ * Extracts the `application/json` (not `application/ld+json`/Hydra) response shape, matching
9
+ * how `lib/bff.ts`'s `authedBackend()` calls the API Platform backend — plain JSON, so
10
+ * collections come back as `{ totalItems, member }` rather than a Hydra-wrapped `hydra:member`.
7
11
  */
8
12
  export const typesApiFileHeader = `import type { components, paths } from './api.generated';
9
13
 
10
- type JsonLd<T> = T extends { 'application/ld+json': infer V } ? V : never;
14
+ type Json<T> = T extends { 'application/json': infer V } ? V : never;
11
15
  `;
12
16
  export function typesApiMarker(entityPascal) {
13
17
  return `export type ${entityPascal} =`;
@@ -28,9 +32,9 @@ export function typesApiBlock(spec, names) {
28
32
  return `
29
33
  type ${entitiesPascal}CollectionResponses = paths['/api/${entitiesKebab}']['get']['responses'];
30
34
  type ${entityPascal}PostResponses = paths['/api/${entitiesKebab}']['post']['responses'];
31
- type Generated${entityPascal} = NonNullable<JsonLd<${entityPascal}PostResponses[201]['content']>>;
35
+ type Generated${entityPascal} = NonNullable<Json<${entityPascal}PostResponses[201]['content']>>;
32
36
  type Generated${entitiesPascal}Collection = NonNullable<
33
- JsonLd<${entitiesPascal}CollectionResponses[200]['content']>
37
+ Json<${entitiesPascal}CollectionResponses[200]['content']>
34
38
  >;
35
39
 
36
40
  export type ${entityPascal}Input = components['schemas']['${entityPascal}.${entityPascal}Input'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kematjaya/crud-ui-generator",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Next.js CRUD frontend generator that reads crud-specs/{Entity}.json sidecars written by kematjaya/crud-maker-bundle's make:kmj-api-crud and generates pages, components, and BFF routes matching the boilerplate's hand-written Notes feature.",
5
5
  "type": "module",
6
6
  "license": "MIT",