@donotdev/cli 0.0.5 → 0.0.6

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 (48) hide show
  1. package/dependencies-matrix.json +57 -33
  2. package/dist/bin/commands/build.js +9 -3
  3. package/dist/bin/commands/bump.js +19 -7
  4. package/dist/bin/commands/cacheout.js +9 -3
  5. package/dist/bin/commands/create-app.js +21 -7
  6. package/dist/bin/commands/create-project.js +22 -7
  7. package/dist/bin/commands/deploy.js +22 -14
  8. package/dist/bin/commands/dev.js +9 -3
  9. package/dist/bin/commands/emu.js +9 -3
  10. package/dist/bin/commands/format.js +9 -3
  11. package/dist/bin/commands/lint.js +9 -3
  12. package/dist/bin/commands/make-admin.d.ts +11 -0
  13. package/dist/bin/commands/make-admin.d.ts.map +1 -0
  14. package/dist/bin/commands/make-admin.js +12 -0
  15. package/dist/bin/commands/make-admin.js.map +1 -0
  16. package/dist/bin/commands/preview.js +9 -3
  17. package/dist/bin/commands/sync-secrets.js +9 -3
  18. package/dist/index.js +33 -17
  19. package/package.json +1 -1
  20. package/templates/app-demo/index.html.example +4 -0
  21. package/templates/app-demo/src/App.tsx.example +28 -10
  22. package/templates/app-demo/src/config/app.ts.example +56 -0
  23. package/templates/app-next/src/app/ClientLayout.tsx.example +4 -3
  24. package/templates/app-next/src/app/layout.tsx.example +17 -25
  25. package/templates/app-next/src/globals.css.example +10 -7
  26. package/templates/app-next/src/locales/dndev_en.json.example +68 -0
  27. package/templates/app-next/src/pages/locales/example_en.json.example +5 -0
  28. package/templates/app-vite/index.html.example +3 -0
  29. package/templates/app-vite/src/globals.css.example +14 -6
  30. package/templates/app-vite/src/locales/dndev_en.json.example +68 -0
  31. package/templates/functions-firebase/README.md.example +25 -0
  32. package/templates/functions-firebase/tsconfig.json.example +3 -13
  33. package/templates/functions-vercel/tsconfig.json.example +1 -13
  34. package/templates/root-consumer/firebase.json.example +1 -1
  35. package/templates/root-consumer/guides/COMPONENTS_ADV.md.example +456 -360
  36. package/templates/root-consumer/guides/COMPONENTS_ATOMIC.md.example +42 -0
  37. package/templates/root-consumer/guides/INDEX.md.example +3 -0
  38. package/templates/root-consumer/guides/SETUP_APP_CONFIG.md.example +5 -2
  39. package/templates/root-consumer/guides/SETUP_BILLING.md.example +44 -4
  40. package/templates/root-consumer/guides/SETUP_CRUD.md.example +1244 -0
  41. package/templates/root-consumer/guides/SETUP_FUNCTIONS.md.example +52 -0
  42. package/templates/root-consumer/guides/SETUP_PAGES.md.example +17 -0
  43. package/templates/root-consumer/guides/SETUP_PWA.md.example +213 -0
  44. package/templates/root-consumer/guides/USE_ROUTING.md.example +503 -0
  45. package/templates/root-consumer/vercel.json.example +315 -20
  46. package/templates/app-demo/src/Routes.tsx.example +0 -20
  47. package/templates/app-vite/src/Routes.tsx.example +0 -16
  48. package/templates/app-vite/src/pages/locales/README.md.example +0 -1
@@ -0,0 +1,1244 @@
1
+ # Setup: CRUD Operations
2
+
3
+ **CRUD = Create, Read, Update, Delete.** Framework handles data ops with built-in security.
4
+
5
+ Choose between direct Firestore access or Cloud Functions based on your security needs. Functions provide field-level filtering, Firestore relies on security rules.
6
+
7
+ ## Backend Selection
8
+
9
+ | Backend | Use When | Example |
10
+ |---------|----------|---------|
11
+ | `firestore` | User's own data, public data, no field filtering needed | userProfile, public listings |
12
+ | `functions` | Mixed visibility fields, admin data, field-level security | admin panels, multi-role access |
13
+
14
+ ---
15
+
16
+ ## When to Use Each Backend
17
+
18
+ ### `backend: 'firestore'` (Direct)
19
+
20
+ **Use for:**
21
+ - **User's own data** - Firestore rules enforce `request.auth.uid == userId`
22
+ - **Public data** - No sensitive fields, everyone sees everything
23
+ - **Simple CRUD** - No field-level visibility requirements
24
+
25
+ ```typescript
26
+ // User reading their own profile - Firestore rules are enough
27
+ const { data: profile } = useCrud('userProfiles', { backend: 'firestore' });
28
+
29
+ // Public product listing - no sensitive fields
30
+ const { data: products } = useCrud('products', { backend: 'firestore' });
31
+ ```
32
+
33
+ **Security:** Firestore Security Rules handle document-level access.
34
+
35
+ ---
36
+
37
+ ### `backend: 'functions'` (Via Cloud Functions)
38
+
39
+ **Use for:**
40
+ - **Mixed visibility** - Some fields guest-visible, some admin-only
41
+ - **Admin panels** - Data with internal/sensitive fields
42
+ - **Multi-role access** - Different users see different fields
43
+
44
+ ```typescript
45
+ // Admin panel - has purchasePrice, vin (admin-only fields)
46
+ const { data: cars } = useCrud('cars', { backend: 'functions' });
47
+
48
+ // Multi-role - users see guest-visible fields, admins see all
49
+ const { data: orders } = useCrud('orders', { backend: 'functions' });
50
+ ```
51
+
52
+ **Security:** Cloud Functions filter fields based on user role BEFORE sending to client.
53
+
54
+ ---
55
+
56
+ ## Decision Tree
57
+
58
+ ```
59
+ Does the data have admin-only fields?
60
+ ├─ No → Does user own this data?
61
+ │ ├─ Yes → backend: 'firestore' (rules enforce ownership)
62
+ │ └─ No → Is it public data?
63
+ │ ├─ Yes → backend: 'firestore'
64
+ │ └─ No → backend: 'functions'
65
+ └─ Yes → backend: 'functions' (field filtering required)
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Security Model
71
+
72
+ **Critical: Separation of concerns**
73
+
74
+ | Concern | Handled By | Where |
75
+ |---------|------------|-------|
76
+ | **Visibility** (who sees) | Backend | Cloud Functions (`filterVisibleFields`) |
77
+ | **Editability** (who modifies) | Frontend | `EntityFormRenderer` |
78
+
79
+ **Why?**
80
+ - Visibility = security concern → must be server-side (prevents data leaks)
81
+ - Editability = UI concern → frontend decides input vs read-only display
82
+
83
+ **Flow:**
84
+ ```
85
+ 1. Frontend calls backend (useCrud with backend: 'functions')
86
+ 2. Backend checks user role, filters fields by visibility
87
+ 3. Frontend receives only fields user can see
88
+ 4. Frontend renders based on editability (input or read-only)
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Field Configuration
94
+
95
+ Controls which fields are included in API responses based on user role. Backend filters fields before sending to client - frontend only renders what it receives.
96
+
97
+ ### Visibility (who SEES the field) - BACKEND enforced
98
+
99
+ | Value | Who Sees | Filtered By | Shown in Forms |
100
+ |-------|----------|-------------|---------------|
101
+ | `'guest'` | Everyone (even unauthenticated) | Backend | Yes |
102
+ | `'user'` | Authenticated users | Backend | Yes |
103
+ | `'admin'` | Admins only | Backend | Yes |
104
+ | `'technical'` | Admins only (system metadata) | Backend | Edit only (read-only) |
105
+ | `'hidden'` | Never (DB only) | Backend | No |
106
+
107
+ **Visibility filtering only works with `backend: 'functions'`.**
108
+ Frontend trusts backend - it only renders what it receives.
109
+
110
+ **Technical fields** (`id`, `createdAt`, etc.) are auto-added by `defineEntity` and shown as read-only in edit forms, never in create forms.
111
+
112
+ Controls whether a field renders as an input or read-only display. If not specified, defaults to editable (anyone who sees can edit).
113
+
114
+ ### Editable (who MODIFIES the field) - FRONTEND enforced
115
+
116
+ | Value | Who Can Edit | Rendered As |
117
+ |-------|--------------|-------------|
118
+ | *(not specified)* | Anyone who sees | Input field |
119
+ | `'admin'` | Admins only | Input (admin) / Read-only (others) |
120
+ | `'create-only'` | On create only | Input (new) / Read-only (edit) |
121
+
122
+ **Default behavior:** If `editable` is not specified, anyone who can see the field can edit it.
123
+
124
+ ---
125
+
126
+ ## Entity Definition Example
127
+
128
+ **Recommended:** Use `defineEntity` to define your entities. It automatically adds technical fields (`id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`) with `visibility: 'technical'`. These fields can be customized (e.g., `editable: 'admin'` to make them editable for admins, custom `label`, additional `validation`) - user-defined properties are merged with defaults.
129
+
130
+ Use i18n keys for labels (e.g., `'entities.car.make'`) for automatic translation support.
131
+
132
+ ```typescript
133
+ // entities/car.ts
134
+ import { defineEntity } from '@donotdev/core';
135
+
136
+ export const carEntity = defineEntity({
137
+ name: 'Car',
138
+ collection: 'cars',
139
+ fields: {
140
+ // Guest-visible - everyone sees, anyone can edit
141
+ make: {
142
+ type: 'text',
143
+ label: 'entities.car.make',
144
+ visibility: 'guest'
145
+ },
146
+ model: {
147
+ type: 'text',
148
+ label: 'entities.car.model',
149
+ visibility: 'guest'
150
+ },
151
+ price: {
152
+ type: 'number',
153
+ label: 'entities.car.price',
154
+ visibility: 'guest'
155
+ },
156
+
157
+ // User-visible - authenticated users see (users see both guest and user fields)
158
+ description: {
159
+ type: 'textarea',
160
+ label: 'entities.car.description',
161
+ visibility: 'user'
162
+ },
163
+ ownerNotes: {
164
+ type: 'textarea',
165
+ label: 'entities.car.ownerNotes',
166
+ visibility: 'user'
167
+ },
168
+
169
+ // Select/Combobox fields - options go in validation.options
170
+ make: {
171
+ type: 'combobox',
172
+ label: 'entities.car.make',
173
+ visibility: 'guest',
174
+ validation: {
175
+ required: true,
176
+ options: [
177
+ { value: 'Toyota', label: 'Toyota' },
178
+ { value: 'Honda', label: 'Honda' },
179
+ { value: 'Ford', label: 'Ford' }
180
+ ]
181
+ }
182
+ },
183
+ fuelType: {
184
+ type: 'select',
185
+ label: 'entities.car.fuelType',
186
+ visibility: 'guest',
187
+ validation: {
188
+ required: true,
189
+ options: [
190
+ { value: 'Petrol', label: 'Petrol' },
191
+ { value: 'Diesel', label: 'Diesel' },
192
+ { value: 'Electric', label: 'Electric' }
193
+ ]
194
+ }
195
+ },
196
+ // Status: framework provides draft/available/deleted, we add custom options
197
+ status: {
198
+ label: 'entities.car.status',
199
+ validation: {
200
+ options: [
201
+ { value: 'reserved', label: 'reserved' },
202
+ { value: 'sold', label: 'sold' }
203
+ ]
204
+ }
205
+ },
206
+ // Result: ['draft', 'available', 'deleted', 'reserved', 'sold']
207
+
208
+ // Admin-only - only admins see and edit
209
+ vin: {
210
+ type: 'text',
211
+ label: 'entities.car.vin',
212
+ visibility: 'admin',
213
+ editable: 'admin'
214
+ },
215
+ purchasePrice: {
216
+ type: 'number',
217
+ label: 'entities.car.purchasePrice',
218
+ visibility: 'admin',
219
+ editable: 'admin'
220
+ },
221
+ supplier: {
222
+ type: 'text',
223
+ label: 'entities.car.supplier',
224
+ visibility: 'admin',
225
+ editable: 'admin'
226
+ },
227
+
228
+ // Hidden fields - never shown in UI, only in DB (passwords, tokens, API keys)
229
+ // apiKey: {
230
+ // type: 'text',
231
+ // visibility: 'hidden',
232
+ // validation: { required: false }
233
+ // },
234
+
235
+ // Technical fields (id, createdAt, updatedAt, createdById, updatedById)
236
+ // are automatically added by defineEntity with visibility: 'technical'
237
+ // Shown as read-only in edit forms (admins only) by default, never in create forms
238
+ // Can be customized: createdAt: { editable: 'admin', label: 'Created Date' }
239
+ }
240
+ });
241
+ ```
242
+
243
+ **Note:** Technical fields are automatically added by `defineEntity`. They're shown as read-only in edit forms (for admins) by default, but can be customized (e.g., `editable: 'admin'` to make editable). They're never shown in create forms. Hidden fields (passwords, tokens) are never shown in any UI.
244
+
245
+ ### Core Types Reference
246
+
247
+ The framework uses a type-safe system for field definitions. Understanding these types helps when working with entities:
248
+
249
+ **EntityField<T>** - Field definition with type parameter:
250
+ ```typescript
251
+ interface EntityField<T extends FieldType = FieldType> {
252
+ type: T; // Field type ('text', 'number', etc.)
253
+ visibility: Visibility; // 'guest' | 'user' | 'admin' | 'technical' | 'hidden'
254
+ editable?: Editable; // true | false | 'admin' | 'create-only'
255
+ validation?: ValidationRules<T>; // Type-safe validation rules
256
+ label?: string; // Display label
257
+ hint?: string; // Helper text
258
+ // ... other optional properties
259
+ }
260
+ ```
261
+
262
+ **FieldType** - Union of all supported field types:
263
+ ```typescript
264
+ type FieldType =
265
+ | 'text' | 'textarea' | 'email' | 'password' | 'url' | 'tel'
266
+ | 'number' | 'range' | 'boolean' | 'checkbox'
267
+ | 'date' | 'datetime-local' | 'time' | 'timestamp'
268
+ | 'select' | 'multiselect' | 'radio' | 'combobox'
269
+ | 'file' | 'image' | 'images' | 'reference'
270
+ | 'geopoint' | 'address' | 'map' | 'array'
271
+ | ... // See full list in Built-in Field Types Reference below
272
+ ```
273
+
274
+ **FieldTypeToValue** - Maps field types to their TypeScript value types:
275
+ ```typescript
276
+ type FieldTypeToValue = {
277
+ text: string;
278
+ email: string;
279
+ number: number;
280
+ boolean: boolean;
281
+ date: string; // ISO string
282
+ select: string;
283
+ multiselect: string[];
284
+ image: Picture | null;
285
+ images: Picture[];
286
+ geopoint: { lat: number; lng: number };
287
+ // ... complete mapping
288
+ };
289
+ ```
290
+
291
+ **ValueTypeForField<T>** - Helper to get value type for a field type:
292
+ ```typescript
293
+ type ValueTypeForField<T extends FieldType> = FieldTypeToValue[T];
294
+
295
+ // Usage examples:
296
+ type EmailValue = ValueTypeForField<'email'>; // string
297
+ type GeoValue = ValueTypeForField<'geopoint'>; // { lat: number; lng: number }
298
+ type ImageValue = ValueTypeForField<'image'>; // Picture | null
299
+ ```
300
+
301
+ **InferEntityData<E>** - Infers data type from entity definition:
302
+ ```typescript
303
+ import type { InferEntityData } from '@donotdev/crud';
304
+
305
+ type CarData = InferEntityData<typeof carEntity>;
306
+ // Infers: {
307
+ // make: string;
308
+ // model: string;
309
+ // price: number;
310
+ // id: string;
311
+ // createdAt: string;
312
+ // ...
313
+ // }
314
+ ```
315
+
316
+ **Type Safety Benefits:**
317
+ - TypeScript knows the value type for each field
318
+ - Validation rules are type-checked (e.g., `minLength` only on text fields)
319
+ - Form components receive correctly typed props
320
+ - Entity data types are automatically inferred
321
+
322
+ ### Entity Labels (i18n)
323
+
324
+ **Framework auto-discovers entity label files** - no configuration needed.
325
+
326
+ **File format:** `entity-<name>_<lang>.json` in `src/locales/` (eager) or `src/pages/locales/` (lazy)
327
+
328
+ **Example:**
329
+ ```json
330
+ // src/locales/entity-car_en.json
331
+ {
332
+ "make": "Make",
333
+ "model": "Model",
334
+ "price": "Price",
335
+ "fuelType": "Fuel Type",
336
+ "status": "Status"
337
+ }
338
+ ```
339
+
340
+ **Usage in entity definition:**
341
+ ```typescript
342
+ export const carEntity = defineEntity({
343
+ name: 'Car', // Framework uses this to find entity-car_*.json files
344
+ collection: 'cars',
345
+ fields: {
346
+ make: {
347
+ type: 'combobox',
348
+ visibility: 'guest',
349
+ // Label is just the field name - framework looks up "car.make" in entity-car_*.json
350
+ label: 'make',
351
+ validation: { required: true, options: [...] }
352
+ }
353
+ }
354
+ });
355
+ ```
356
+
357
+ **How it works:**
358
+ - Entity name `'Car'` → namespace `'car'` (lowercase)
359
+ - Field label `'make'` → looks up `car.make` in `entity-car_<lang>.json`
360
+ - If file not found, falls back to `dndev` namespace
361
+ - Framework auto-discovers `entity_*_*.json` files
362
+
363
+ ### Field Types with Options
364
+
365
+ For `select`, `combobox`, `multiselect`, and `radio` fields, **options must be placed in `validation.options`**:
366
+
367
+ ```typescript
368
+ make: {
369
+ type: 'combobox', // or 'select', 'multiselect', 'radio'
370
+ visibility: 'guest',
371
+ validation: {
372
+ required: true,
373
+ // ✅ CORRECT: options go inside validation
374
+ options: [
375
+ { value: 'Toyota', label: 'Toyota' },
376
+ { value: 'Honda', label: 'Honda' }
377
+ ]
378
+ }
379
+ }
380
+ ```
381
+
382
+ **Format:** Options must be an array of `{ value: string, label: string }` objects. Labels can use i18n keys for translation.
383
+
384
+ ### Field-Specific Options
385
+
386
+ Some field types support additional options via the `options` property:
387
+
388
+ #### Phone Number (`tel`) Options
389
+
390
+ The `tel` field supports country code customization:
391
+
392
+ ```typescript
393
+ phone: {
394
+ type: 'tel',
395
+ label: 'entities.contact.phone',
396
+ visibility: 'guest',
397
+ options: {
398
+ fieldSpecific: {
399
+ // Default country code (ISO country code, e.g., 'FR' for +33)
400
+ defaultCountry: 'FR',
401
+
402
+ // Whether to show country flags in the selector
403
+ showFlags: true,
404
+
405
+ // Preferred countries to show at the top of the dropdown
406
+ // These appear first, followed by all other common countries
407
+ preferredCountries: ['FR', 'BE', 'CH'],
408
+
409
+ // OR: Restrict to only specific countries
410
+ // If provided, only these countries are shown (no others)
411
+ countries: ['FR', 'US', 'GB'],
412
+ }
413
+ }
414
+ }
415
+ ```
416
+
417
+ **Options (inside `options.fieldSpecific`):**
418
+ - `defaultCountry` (string, optional): ISO country code for default selection (default: `'FR'`)
419
+ - `showFlags` (boolean, optional): Show country flags in selector (default: `true`)
420
+ - `preferredCountries` (string[], optional): Country codes to show first in dropdown
421
+ - `countries` (string[], optional): If provided, only these countries are shown (overrides `preferredCountries`)
422
+
423
+ **Examples:**
424
+
425
+ ```typescript
426
+ // French app - show France, Belgium, Switzerland first
427
+ phone: {
428
+ type: 'tel',
429
+ options: {
430
+ fieldSpecific: {
431
+ defaultCountry: 'FR',
432
+ preferredCountries: ['FR', 'BE', 'CH'],
433
+ }
434
+ }
435
+ }
436
+
437
+ // International app - restrict to major markets only
438
+ phone: {
439
+ type: 'tel',
440
+ options: {
441
+ fieldSpecific: {
442
+ defaultCountry: 'US',
443
+ countries: ['US', 'GB', 'CA', 'AU'], // Only these 4 countries
444
+ }
445
+ }
446
+ }
447
+
448
+ // Simple usage - just set default country
449
+ phone: {
450
+ type: 'tel',
451
+ options: {
452
+ fieldSpecific: {
453
+ defaultCountry: 'FR',
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ **Note:** The phone field stores the full number with country code (e.g., `"+33 6 12 34 56 78"`). The component automatically handles country code selection and phone number input.
460
+
461
+ ## Usage Examples
462
+
463
+ ### Guest-Visible App (No Sensitive Fields)
464
+ ```typescript
465
+ // Guest-visible car browsing - all fields visible to everyone, direct Firestore is fine
466
+ const { data: cars, query } = useCrud('cars', { backend: 'firestore' });
467
+
468
+ useEffect(() => {
469
+ query({ where: [{ field: 'status', operator: '==', value: 'available' }] });
470
+ }, []);
471
+ ```
472
+
473
+ ### User Profile (Own Data)
474
+ ```typescript
475
+ // User's own profile - Firestore rules enforce ownership
476
+ const { data: profile, update } = useCrud('userProfiles', { backend: 'firestore' });
477
+ ```
478
+
479
+ ### Admin Panel (Mixed Visibility)
480
+ ```typescript
481
+ // Admin sees all fields including vin, purchasePrice
482
+ // Functions filter these for non-admins
483
+ const { data: cars, query } = useCrud('cars', { backend: 'functions' });
484
+ ```
485
+
486
+ ### Using EntityFormRenderer (Quick Start)
487
+
488
+ ```typescript
489
+ import { EntityFormRenderer } from '@donotdev/crud';
490
+ import { carEntity } from '../entities/car';
491
+
492
+ // Create form - excludes technical and hidden fields automatically
493
+ <EntityFormRenderer
494
+ entity={carEntity.fields}
495
+ operation="create"
496
+ onSubmit={async (data) => {
497
+ await createCar(data);
498
+ }}
499
+ />
500
+
501
+ // Edit form - shows technical fields as read-only, excludes hidden
502
+ <EntityFormRenderer
503
+ entity={carEntity.fields}
504
+ operation="edit"
505
+ defaultValues={carData} // From useCrud - already filtered by backend
506
+ onSubmit={async (data) => {
507
+ await updateCar(carId, data);
508
+ }}
509
+ />
510
+ ```
511
+
512
+ **Note:** `operation` defaults to `'create'` if `defaultValues` is missing, or `'edit'` if present. Technical fields are automatically excluded from create forms and shown as read-only in edit forms.
513
+
514
+ ### Custom Forms with useEntityForm (Production)
515
+
516
+ For custom layouts, branded forms, or complex UX, use the form building blocks:
517
+
518
+ ```typescript
519
+ import { useEntityForm, useCrud } from '@donotdev/crud';
520
+ import { carEntity } from '../entities/car';
521
+ import { Input, Button } from './ui'; // Your components
522
+
523
+ function CarForm({ carId, onSuccess }) {
524
+ const crud = useCrud('cars');
525
+ const [carData, setCarData] = useState(null);
526
+
527
+ useEffect(() => {
528
+ if (carId) crud.get(carId).then(setCarData);
529
+ }, [carId]);
530
+
531
+ const form = useEntityForm(carEntity, {
532
+ operation: carId ? 'edit' : 'create',
533
+ defaultValues: carData,
534
+ viewerRole: 'admin'
535
+ });
536
+
537
+ const onSubmit = async (data) => {
538
+ if (carId) {
539
+ await crud.update(carId, data);
540
+ } else {
541
+ await crud.add(data);
542
+ }
543
+ onSuccess();
544
+ };
545
+
546
+ return (
547
+ <form onSubmit={form.handleSubmit(onSubmit)}>
548
+ {/* Custom 2-column layout */}
549
+ <div className="grid grid-cols-2 gap-4">
550
+ {form.fields.map(({ name, config, editable }) => (
551
+ <Input
552
+ key={name}
553
+ label={config.label}
554
+ {...form.register(name)}
555
+ disabled={!editable}
556
+ error={form.formState.errors[name]?.message}
557
+ />
558
+ ))}
559
+ </div>
560
+ <Button type="submit" loading={form.formState.isSubmitting}>
561
+ {form.operation === 'create' ? 'Create' : 'Update'}
562
+ </Button>
563
+ </form>
564
+ );
565
+ }
566
+ ```
567
+
568
+ ### When to Use What
569
+
570
+ | Use Case | Solution |
571
+ |----------|----------|
572
+ | Admin panels, internal tools | `EntityFormRenderer` |
573
+ | Custom layouts, branded forms | `useEntityForm` + your components |
574
+ | Complex conditional logic | `useEntityField` for granular control |
575
+
576
+ ### Available Building Blocks
577
+
578
+ ```typescript
579
+ import {
580
+ // High-level
581
+ EntityFormRenderer, // Zero-config form rendering
582
+
583
+ // Hooks for custom forms
584
+ useEntityForm, // Main form hook (wraps React Hook Form)
585
+ useEntityField, // Single field control
586
+
587
+ // Utilities
588
+ getFieldsForOperation, // Filter fields by operation/visibility
589
+ createEntitySchema, // Generate Valibot schema
590
+ validateEntity, // Standalone validation
591
+ } from '@donotdev/crud';
592
+ ```
593
+
594
+ ---
595
+
596
+ ## Backend Security Comparison
597
+
598
+ | Aspect | `firestore` | `functions` |
599
+ |--------|-------------|-------------|
600
+ | Auth check | Firestore rules | Function + rules |
601
+ | Field filtering | No | Yes (by role) |
602
+ | Network exposure | All fields | Only visible fields |
603
+ | Latency | Direct | +50-100ms (cold start) |
604
+ | Cost | Reads only | Reads + invocation |
605
+
606
+ ---
607
+
608
+ ## Cloud Functions Setup
609
+
610
+ For `backend: 'functions'`, deploy CRUD functions. **Recommended:** Generate schemas from your entity definition using the framework's schema generation utilities.
611
+
612
+ ### Firebase Functions
613
+
614
+ ```typescript
615
+ // functions/src/crud/cars.ts
616
+ import { getEntity, listEntities, createEntity, updateEntity, deleteEntity } from '@donotdev/functions/firebase';
617
+ import { carEntity } from '../../../entities/car';
618
+ import { createSchemas } from '@donotdev/core/schemas';
619
+
620
+ // Generate schemas from entity definition
621
+ const schemas = createSchemas(carEntity);
622
+ const carSchema = schemas.GetCarSchema; // User-visible fields
623
+ const carAdminSchema = schemas.GetCarAdminSchema; // All fields (admin)
624
+
625
+ export const getCar = getEntity('cars', carSchema);
626
+ export const listCars = listEntities('cars', carSchema);
627
+ export const createCar = createEntity('cars', carSchema);
628
+ export const updateCar = updateEntity('cars', carSchema);
629
+ export const deleteCar = deleteEntity('cars', carSchema);
630
+ ```
631
+
632
+ ### Vercel API Routes
633
+
634
+ For Next.js apps, create API route handlers that wrap the framework functions:
635
+
636
+ ```typescript
637
+ // pages/api/crud/cars/[id].ts (Pages Router)
638
+ import type { NextApiRequest, NextApiResponse } from 'next';
639
+ import { getEntity as frameworkGetEntity } from '@donotdev/functions/vercel';
640
+
641
+ export default async function handler(
642
+ req: NextApiRequest,
643
+ res: NextApiResponse
644
+ ) {
645
+ return frameworkGetEntity(req, res);
646
+ }
647
+ ```
648
+
649
+ **Note:** Vercel functions are Next.js API route handlers, while Firebase uses callable functions. Both support the same schema-based field filtering. The framework handles schema validation and field filtering automatically. See the framework templates (`packages/cli/templates/functions-vercel`) for complete examples.
650
+
651
+ ---
652
+
653
+ ## Summary
654
+
655
+ | Data Type | Backend | Why |
656
+ |-----------|---------|-----|
657
+ | User's own profile | `firestore` | Rules enforce ownership |
658
+ | Public listings | `firestore` | No sensitive fields |
659
+ | Admin panel data | `functions` | Field filtering needed |
660
+ | Multi-role data | `functions` | Different visibility per role |
661
+
662
+ **Rule of thumb:** If data has `visibility: 'admin'` fields, use `backend: 'functions'`.
663
+
664
+ ---
665
+
666
+ ## Analytics & Aggregations
667
+
668
+ For dashboards and analytics, use `aggregateEntities` instead of fetching all data.
669
+
670
+ ### Why Use Backend Analytics?
671
+
672
+ | Approach | Data Transfer | Security | Scale |
673
+ |----------|--------------|----------|-------|
674
+ | Frontend (useMemo) | ~500KB (all data) | Raw data in network tab | <500 items |
675
+ | Backend (aggregateEntities) | ~5KB (metrics only) | Only aggregates sent | 10,000+ items |
676
+
677
+ ### aggregateEntities Function
678
+
679
+ Returns **only computed metrics**, never raw data.
680
+
681
+ ```typescript
682
+ // functions/src/analytics/getCarsAnalytics.ts
683
+ import { aggregateEntities } from '@donotdev/functions/firebase';
684
+ import { carSchema } from '../schemas';
685
+
686
+ export const getCarsAnalytics = aggregateEntities('cars', carSchema, {
687
+ // Top-level metrics
688
+ metrics: [
689
+ { field: '*', operation: 'count', as: 'total' },
690
+ { field: 'price', operation: 'sum', as: 'totalValue' },
691
+ { field: 'price', operation: 'avg', as: 'avgPrice' },
692
+ // Admin-only field - visibility check enforced
693
+ { field: 'purchasePrice', operation: 'sum', as: 'totalCost' },
694
+ ],
695
+ // Group by field values
696
+ groupBy: [
697
+ {
698
+ field: 'status',
699
+ metrics: [
700
+ { field: '*', operation: 'count', as: 'count' },
701
+ { field: 'price', operation: 'sum', as: 'value' },
702
+ ],
703
+ },
704
+ ],
705
+ });
706
+ ```
707
+
708
+ ### Supported Operations
709
+
710
+ | Operation | Description | Example |
711
+ |-----------|-------------|---------|
712
+ | `count` | Count documents | `{ field: '*', operation: 'count', as: 'total' }` |
713
+ | `sum` | Sum numeric field | `{ field: 'price', operation: 'sum', as: 'revenue' }` |
714
+ | `avg` | Average | `{ field: 'price', operation: 'avg', as: 'avgPrice' }` |
715
+ | `min` | Minimum value | `{ field: 'price', operation: 'min', as: 'cheapest' }` |
716
+ | `max` | Maximum value | `{ field: 'price', operation: 'max', as: 'mostExpensive' }` |
717
+
718
+ ### Filtered Metrics
719
+
720
+ Apply filters to specific metrics:
721
+
722
+ ```typescript
723
+ metrics: [
724
+ // Count only sold cars
725
+ {
726
+ field: '*',
727
+ operation: 'count',
728
+ as: 'soldCount',
729
+ filter: { field: 'status', operator: '==', value: 'Sold' },
730
+ },
731
+ // Revenue from sold cars only
732
+ {
733
+ field: 'price',
734
+ operation: 'sum',
735
+ as: 'revenue',
736
+ filter: { field: 'status', operator: '==', value: 'Sold' },
737
+ },
738
+ ]
739
+ ```
740
+
741
+ ### Response Format
742
+
743
+ ```typescript
744
+ {
745
+ metrics: {
746
+ total: 150,
747
+ totalValue: 4500000,
748
+ avgPrice: 30000,
749
+ },
750
+ groups: {
751
+ status: {
752
+ Available: { count: 80, value: 2400000 },
753
+ Reserved: { count: 20, value: 600000 },
754
+ Sold: { count: 50, value: 1500000 },
755
+ }
756
+ },
757
+ meta: {
758
+ collection: 'cars',
759
+ totalDocs: 150,
760
+ computedAt: '2024-01-15T10:30:00Z'
761
+ }
762
+ }
763
+ ```
764
+
765
+ ### Visibility Protection
766
+
767
+ `aggregateEntities` enforces field visibility:
768
+ - Admin-only fields can only be aggregated by admins
769
+ - Non-admins attempting to aggregate `visibility: 'admin'` fields get `permission-denied`
770
+
771
+ ### When NOT to Use aggregateEntities
772
+
773
+ Use custom functions for:
774
+ - **Cross-collection relationships** (e.g., "people to call" requires joining customers + inquiries)
775
+ - **Complex computed fields** (e.g., profit = revenue - cost - repairs)
776
+ - **Time-based logic** (e.g., "older than 30 days")
777
+
778
+ ```typescript
779
+ import { onCall } from 'firebase-functions/v2/https';
780
+ import { FUNCTION_CONFIG } from '@donotdev/functions/firebase';
781
+ // ⚠️ CRITICAL: Functions MUST use /server imports
782
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
783
+ import { handleError } from '@donotdev/core/server';
784
+
785
+ // Custom function for complex cross-collection logic
786
+ // Use FUNCTION_CONFIG to enable CORS automatically
787
+ export const getDashboardMetrics = onCall(FUNCTION_CONFIG, async (request) => {
788
+ const firestore = getFirebaseAdminFirestore();
789
+
790
+ // Fetch from multiple collections
791
+ // Apply custom business logic
792
+ // Return computed metrics
793
+ });
794
+ ```
795
+
796
+ **⚠️ IMPORTANT:** In functions, always use `@donotdev/firebase/server` (not `@donotdev/firebase`). Client imports will crash your function on deploy.
797
+
798
+ ---
799
+
800
+ **Choose the right backend. Define visibility. Framework handles the rest.**
801
+
802
+ ---
803
+
804
+ ## Custom Field Types
805
+
806
+ The framework provides 30+ built-in field types. For domain-specific needs, you can register custom field types.
807
+
808
+ ### When to Create Custom Fields
809
+
810
+ | Scenario | Solution |
811
+ |----------|----------|
812
+ | Standard input (text, number, date) | Use built-in types |
813
+ | Special formatting (currency, VIN) | Custom field type |
814
+ | Domain component (rating widget) | Custom field type |
815
+ | Complex data (address picker) | Custom field type |
816
+
817
+ ### Registering a Custom Field Type
818
+
819
+ **Framework provides `field` and `fieldState`** - No need to import `useController` or `Controller` from react-hook-form.
820
+
821
+ Custom fields need three things:
822
+ 1. **Controlled component** - For react-hook-form integration
823
+ 2. **Uncontrolled component** (optional) - For standalone use
824
+ 3. **Schema generator** - For Valibot validation
825
+
826
+ ```typescript
827
+ // lib/fields/currencyField.ts
828
+ import { registerFieldType, type ControlledFieldProps } from '@donotdev/crud';
829
+ import * as v from 'valibot';
830
+ import { CurrencyInput } from '../components/CurrencyInput';
831
+
832
+ // 1. Create the controlled component wrapper
833
+ function ControlledCurrencyField({
834
+ fieldConfig,
835
+ field, // ✅ Provided by framework - no need to use useController
836
+ fieldState, // ✅ Provided by framework - no need to use useController
837
+ errors,
838
+ t,
839
+ onChange
840
+ }: ControlledFieldProps) {
841
+ const { label, validation } = fieldConfig;
842
+
843
+ // Use field.value and field.onChange directly - framework handles react-hook-form integration
844
+ return (
845
+ <CurrencyInput
846
+ label={t(label)}
847
+ value={field?.value || 0}
848
+ onChange={(value) => {
849
+ field?.onChange(value);
850
+ onChange?.(value);
851
+ }}
852
+ error={fieldState?.error?.message || errors[fieldConfig.name]?.message}
853
+ currency={validation?.currency || 'USD'}
854
+ />
855
+ );
856
+ }
857
+
858
+ // 2. Register the field type
859
+ registerFieldType({
860
+ type: 'currency', // Your custom type name
861
+ controlledComponent: ControlledCurrencyField,
862
+ // Optional: uncontrolled component for non-form use
863
+ // uncontrolledComponent: CurrencyDisplay,
864
+ schemaGenerator: (field) => {
865
+ // Generate Valibot schema based on field config
866
+ const schema = v.pipe(
867
+ v.number(),
868
+ v.minValue(field.validation?.min ?? 0)
869
+ );
870
+ return field.validation?.required
871
+ ? schema
872
+ : v.optional(schema);
873
+ }
874
+ });
875
+ ```
876
+
877
+ ### Complex Custom Fields
878
+
879
+ For arrays or nested data, use `field` and `fieldState` directly:
880
+
881
+ ```typescript
882
+ import { Text } from '@donotdev/components';
883
+
884
+ function ControlledRepairOperationsField({
885
+ fieldConfig,
886
+ field, // ✅ Framework provides - use field.value, field.onChange
887
+ fieldState, // ✅ Framework provides - use fieldState.error
888
+ t
889
+ }: ControlledFieldProps) {
890
+ const value = (field?.value || []) as RepairOperation[];
891
+
892
+ return (
893
+ <div>
894
+ {/* Field label - use Text with level="body" and align="start" for non-floating labels */}
895
+ <Text level="body" align="start">
896
+ {t(fieldConfig.label)}
897
+ {fieldConfig.validation?.required && (
898
+ <span style={{ color: 'var(--destructive-foreground)', marginInlineStart: 'var(--gap-tight)' }}>*</span>
899
+ )}
900
+ </Text>
901
+
902
+ {/* Your custom UI */}
903
+ <Button onClick={() => field?.onChange([...value, { operation: '', cost: 0 }])}>
904
+ {t('add')}
905
+ </Button>
906
+
907
+ {/* Helper/error text - use Text with level="small" */}
908
+ {fieldState?.error && (
909
+ <Text variant="destructive" level="small">
910
+ {fieldState.error.message}
911
+ </Text>
912
+ )}
913
+ </div>
914
+ );
915
+ }
916
+ ```
917
+
918
+ **Text Component Guidelines:**
919
+
920
+ **Label Typography:**
921
+ - **FloatingLabels** (Input, Textarea, etc.): Framework handles automatically using `--font-size-xs` (12px) internally. You don't need to style these.
922
+ - **Non-floating labels** (ImageField, SwitchField, custom fields): Use `<Text level="body" align="start">` to match framework style (16px, same as input text)
923
+ - Required asterisk: Add automatically with `<span style={{ color: 'var(--destructive-foreground)', marginInlineStart: 'var(--gap-tight)' }}>*</span>`
924
+ - **Don't use:** `<Label>` component - it has hover state which is inappropriate for field labels
925
+
926
+ **Helper/Error Text:**
927
+ - **Helper text:** Use `<Text level="small" variant="muted">` (14px)
928
+ - **Error messages:** Use `<Text level="small" variant="destructive">` (14px)
929
+
930
+ **Framework Text Levels:**
931
+ - `level="small"` → 14px (`--font-size-sm`)
932
+ - `level="body"` → 16px (`--font-size-base`, default)
933
+ - `level="h1"`-`"h4"` → Larger headings
934
+
935
+ **Don't use:** `--font-size-xs` in inline styles - use `Text` with `level="small"` instead
936
+
937
+ ### Using Custom Fields in Entities
938
+
939
+ After registration, use your custom type like built-in types:
940
+
941
+ ```typescript
942
+ // entities/product.ts
943
+ import { defineEntity } from '@donotdev/core';
944
+ import '../lib/fields/currencyField'; // Import to register
945
+ import '../lib/fields/repairOperationsField'; // Import to register
946
+
947
+ export const productEntity = defineEntity({
948
+ name: 'Product',
949
+ collection: 'products',
950
+ fields: {
951
+ name: { type: 'text', visibility: 'guest' },
952
+ // Simple custom type
953
+ price: {
954
+ type: 'currency', // Your custom type
955
+ visibility: 'guest',
956
+ validation: {
957
+ required: true,
958
+ min: 0,
959
+ currency: 'USD' // Custom validation option
960
+ }
961
+ },
962
+ // Complex custom type with arrays
963
+ repairs: {
964
+ type: 'repairOperations',
965
+ visibility: 'admin',
966
+ validation: { required: false }
967
+ }
968
+ }
969
+ });
970
+ ```
971
+
972
+ ### Type Safety for Custom Fields
973
+
974
+ To get full TypeScript support for custom field types:
975
+
976
+ ```typescript
977
+ // types/fields.d.ts
978
+ import '@donotdev/types';
979
+
980
+ declare module '@donotdev/types' {
981
+ interface FieldTypeToValue {
982
+ // Add your custom type's value type
983
+ currency: number;
984
+ }
985
+
986
+ interface ValidationRulesExtension {
987
+ // Add custom validation options
988
+ currency?: string;
989
+ }
990
+ }
991
+ ```
992
+
993
+ ### Registration Timing
994
+
995
+ Register custom fields **before** using them in forms:
996
+
997
+ ```typescript
998
+ // app/providers.tsx (Next.js App Router)
999
+ 'use client';
1000
+ import '../lib/fields/currencyField';
1001
+ import '../lib/fields/ratingField';
1002
+ // ... other custom fields
1003
+
1004
+ export function Providers({ children }) {
1005
+ return <>{children}</>;
1006
+ }
1007
+
1008
+ // app/layout.tsx
1009
+ import { Providers } from './providers';
1010
+
1011
+ export default function RootLayout({ children }) {
1012
+ return <Providers>{children}</Providers>;
1013
+ }
1014
+ ```
1015
+
1016
+ ### Built-in Field Types Reference
1017
+
1018
+ | Type | Value | Use Case |
1019
+ |------|-------|----------|
1020
+ | `text` | string | Short text, names |
1021
+ | `textarea` | string | Long text, descriptions |
1022
+ | `email` | string | Email addresses |
1023
+ | `password` | string | Password input |
1024
+ | `url` | string | URLs |
1025
+ | `tel` | string | Phone numbers (with country code selector) |
1026
+ | `number` | number | Numeric input |
1027
+ | `range` | number | Slider input |
1028
+ | `boolean` | boolean | Yes/no switch |
1029
+ | `checkbox` | boolean | Checkbox |
1030
+ | `switch` | boolean | Toggle switch |
1031
+ | `date` | string | Date picker (ISO) |
1032
+ | `datetime-local` | string | DateTime picker |
1033
+ | `time` | string | Time picker |
1034
+ | `timestamp` | Timestamp | Firestore timestamp |
1035
+ | `select` | string | Dropdown single |
1036
+ | `multiselect` | string[] | Dropdown multi |
1037
+ | `radio` | string | Radio buttons |
1038
+ | `combobox` | string | Searchable dropdown |
1039
+ | `file` | File \| null | File upload |
1040
+ | `image` | Picture \| null | Image upload (single) |
1041
+ | `images` | Picture[] | Image upload (multiple) |
1042
+ | `reference` | string | Doc reference |
1043
+ | `geopoint` | GeoPoint | Map coordinates |
1044
+ | `address` | AddressValue | Address picker |
1045
+ | `map` | object | Key-value pairs |
1046
+ | `color` | string | Color picker |
1047
+ | `array` | unknown[] | Array of values |
1048
+
1049
+ ---
1050
+
1051
+ **Custom fields follow the same Entity → Schema → UI flow. Register once, use everywhere.**
1052
+
1053
+ ---
1054
+
1055
+ ## Status Field & Workflows
1056
+
1057
+ The `status` field enables draft saves, soft delete, and custom workflows.
1058
+
1059
+ ### Framework Support
1060
+
1061
+ The `status` field is **auto-added** by `defineEntity()` with:
1062
+ - `visibility: 'admin'` (visible in admin forms)
1063
+ - Default value: `'available'` (new documents are published by default)
1064
+ - Options: `['draft', 'available', 'deleted']`
1065
+
1066
+ | Behavior | Description |
1067
+ |----------|-------------|
1068
+ | **Default status** | New documents default to `'available'` (published) |
1069
+ | **Draft saves** | Set status to `'draft'` to save incomplete data |
1070
+ | **Soft delete** | Set status to `'deleted'` instead of hard delete |
1071
+ | **Server filtering** | `draft` and `deleted` hidden from non-admin (never in network tab) |
1072
+ | **Publish validation** | Full `required` validation when status !== `'draft'` |
1073
+ | **Hide field** | Set `visibility: 'technical'` if you don't want it in forms |
1074
+
1075
+ ### Basic Usage (No Config Needed)
1076
+
1077
+ ```typescript
1078
+ // entities/car.ts
1079
+ export const carEntity = defineEntity({
1080
+ name: 'Car',
1081
+ collection: 'cars',
1082
+ fields: {
1083
+ // Required fields - only enforced when status !== 'draft'
1084
+ make: { type: 'combobox', label: 'make', visibility: 'guest', validation: { required: true } },
1085
+ model: { type: 'text', label: 'model', visibility: 'guest', validation: { required: true } },
1086
+ price: { type: 'number', label: 'price', visibility: 'guest', validation: { required: true } },
1087
+ // Optional fields
1088
+ notes: { type: 'textarea', label: 'notes', visibility: 'user' },
1089
+ // status is auto-added with options: ['draft', 'available', 'deleted']
1090
+ }
1091
+ });
1092
+ ```
1093
+
1094
+ **Behavior:**
1095
+ - New documents default to `status: 'available'` (published)
1096
+ - Set `status: 'draft'` to save incomplete data (`required` validation skipped)
1097
+ - Set `status: 'deleted'` for soft delete
1098
+ - Public queries → `draft` and `deleted` filtered out automatically
1099
+
1100
+ ### Extending Status Options
1101
+
1102
+ Add custom statuses while keeping framework defaults:
1103
+
1104
+ ```typescript
1105
+ export const carEntity = defineEntity({
1106
+ name: 'Car',
1107
+ collection: 'cars',
1108
+ fields: {
1109
+ make: { type: 'text', visibility: 'guest', validation: { required: true } },
1110
+ // Extend status options
1111
+ status: {
1112
+ validation: {
1113
+ options: [
1114
+ { value: 'reserved', label: 'reserved' },
1115
+ { value: 'sold', label: 'sold' },
1116
+ ]
1117
+ }
1118
+ }
1119
+ }
1120
+ });
1121
+ // Result: ['draft', 'available', 'deleted', 'reserved', 'sold']
1122
+ ```
1123
+
1124
+ ### Hiding the Status Field
1125
+
1126
+ If you don't want the status field visible in forms:
1127
+
1128
+ ```typescript
1129
+ export const carEntity = defineEntity({
1130
+ name: 'Car',
1131
+ collection: 'cars',
1132
+ fields: {
1133
+ make: { type: 'text', visibility: 'guest', validation: { required: true } },
1134
+ // Hide status field from forms
1135
+ status: { visibility: 'technical' }
1136
+ }
1137
+ });
1138
+ // Field still exists in DB, just hidden from UI
1139
+ ```
1140
+
1141
+ ### Form Integration
1142
+
1143
+ ```typescript
1144
+ function CarForm({ carId, defaultValues, onSave }) {
1145
+ const form = useEntityForm(carEntity, {
1146
+ operation: carId ? 'edit' : 'create',
1147
+ // Override framework default ('available') to start as draft
1148
+ defaultValues: { status: 'draft', ...defaultValues },
1149
+ });
1150
+
1151
+ const status = form.watch('status');
1152
+ const isDraft = status === 'draft';
1153
+
1154
+ return (
1155
+ <form onSubmit={form.handleSubmit(onSave)}>
1156
+ <EntityFormRenderer entity={carEntity} form={form} />
1157
+
1158
+ <div className="flex gap-2">
1159
+ {/* Save as draft (skip validation) */}
1160
+ <Button
1161
+ type="button"
1162
+ variant="outline"
1163
+ onClick={() => {
1164
+ form.setValue('status', 'draft');
1165
+ form.handleSubmit(onSave)();
1166
+ }}
1167
+ >
1168
+ Save Draft
1169
+ </Button>
1170
+
1171
+ {/* Publish (full validation) */}
1172
+ <Button
1173
+ type="button"
1174
+ onClick={() => {
1175
+ form.setValue('status', 'available');
1176
+ form.handleSubmit(onSave)();
1177
+ }}
1178
+ >
1179
+ {isDraft ? 'Publish' : 'Save'}
1180
+ </Button>
1181
+ </div>
1182
+ </form>
1183
+ );
1184
+ }
1185
+ ```
1186
+
1187
+ ### Optional: Custom Publish Validation
1188
+
1189
+ For additional requirements beyond entity `required` fields (e.g., min images, specific formats):
1190
+
1191
+ ```typescript
1192
+ // lib/schemas/carPublishSchema.ts
1193
+ import * as v from 'valibot';
1194
+
1195
+ export const carPublishSchema = v.object({
1196
+ images: v.pipe(v.array(v.any()), v.minLength(1, 'At least one image required')),
1197
+ year: v.pipe(v.number(), v.minValue(1900, 'Year must be 1900 or later')),
1198
+ });
1199
+
1200
+ export function canPublish(data: Record<string, unknown>): boolean {
1201
+ return v.safeParse(carPublishSchema, data).success;
1202
+ }
1203
+ ```
1204
+
1205
+ ### Admin Panel: Show All Statuses
1206
+
1207
+ Admin queries include drafts and deleted documents. Add badges for visibility:
1208
+
1209
+ ```typescript
1210
+ // Admin list shows all including drafts and deleted
1211
+ const { data: allCars } = useCrud(carEntity);
1212
+
1213
+ // Status badges
1214
+ {car.status === 'draft' && <Badge variant="outline">Draft</Badge>}
1215
+ {car.status === 'deleted' && <Badge variant="destructive">Deleted</Badge>}
1216
+ ```
1217
+
1218
+ ### Soft Delete
1219
+
1220
+ Use status instead of hard delete:
1221
+
1222
+ ```typescript
1223
+ const { update } = useCrud(carEntity);
1224
+
1225
+ // Soft delete
1226
+ await update(carId, { status: 'deleted' });
1227
+
1228
+ // Restore
1229
+ await update(carId, { status: 'available' });
1230
+ ```
1231
+
1232
+ ### Summary
1233
+
1234
+ | Concern | Handled By |
1235
+ |---------|------------|
1236
+ | Default status | Framework (`'available'` - published by default) |
1237
+ | Save incomplete data | Consumer sets `status: 'draft'` (validation skipped) |
1238
+ | Soft delete | Consumer sets `status: 'deleted'` |
1239
+ | Hide draft/deleted | Framework (server-side filtering for non-admin) |
1240
+ | Publish validation | Framework (`required` enforced when status !== 'draft') |
1241
+ | Custom statuses | Consumer extends options in entity definition |
1242
+ | Hide status field | Consumer sets `visibility: 'technical'` |
1243
+
1244
+ **Status field is visible by default.** Consumer controls workflow via status values.