@donotdev/cli 0.0.6 → 0.0.7
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/dependencies-matrix.json +25 -7
- package/dist/bin/commands/build.js +156 -158
- package/dist/bin/commands/bump.js +153 -153
- package/dist/bin/commands/cacheout.js +154 -154
- package/dist/bin/commands/create-app.js +184 -156
- package/dist/bin/commands/create-project.js +154 -154
- package/dist/bin/commands/deploy.js +470 -470
- package/dist/bin/commands/dev.js +155 -155
- package/dist/bin/commands/emu.js +155 -155
- package/dist/bin/commands/format.js +154 -154
- package/dist/bin/commands/lint.js +157 -154
- package/dist/bin/commands/preview.js +155 -155
- package/dist/bin/commands/sync-secrets.js +155 -155
- package/dist/bin/commands/wai.d.ts +11 -0
- package/dist/bin/commands/wai.d.ts.map +1 -0
- package/dist/bin/commands/wai.js +12 -0
- package/dist/bin/commands/wai.js.map +1 -0
- package/dist/bin/dndev.js +24 -8
- package/dist/bin/donotdev.js +24 -8
- package/dist/index.js +524 -497
- package/package.json +1 -1
- package/templates/app-demo/src/config/app.ts.example +12 -0
- package/templates/app-next/src/config/app.ts.example +75 -48
- package/templates/app-vite/index.html.example +71 -37
- package/templates/app-vite/src/config/app.ts.example +75 -47
- package/templates/app-vite/src/pages/FormPageExample.tsx.example +152 -0
- package/templates/app-vite/src/pages/HomePage.tsx.example +81 -134
- package/templates/app-vite/src/pages/ListPageExample.tsx.example +88 -0
- package/templates/functions-firebase/build.mjs.example +8 -1
- package/templates/functions-firebase/functions-firebase/build.mjs.example +8 -1
- package/templates/functions-firebase/functions-firebase/src/index.ts.example +19 -25
- package/templates/functions-firebase/functions.config.js.example +35 -0
- package/templates/root-consumer/entities/ExampleEntity.ts.example +223 -0
- package/templates/root-consumer/entities/demo.ts.example +562 -0
- package/templates/root-consumer/entities/index.ts.example +15 -0
- package/templates/root-consumer/guides/{AGENT_START_HERE.md.example → dndev/AGENT_START_HERE.md.example} +22 -0
- package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +231 -0
- package/templates/root-consumer/guides/{SETUP_AUTH.md.example → dndev/SETUP_AUTH.md.example} +30 -0
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +473 -0
- package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +116 -0
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +404 -0
- package/templates/root-consumer/guides/wai-way/agents/architect.md.example +78 -0
- package/templates/root-consumer/guides/wai-way/agents/builder.md.example +87 -0
- package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +325 -0
- package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +100 -0
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +281 -0
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +77 -0
- package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +104 -0
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +124 -0
- package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +165 -0
- package/templates/root-consumer/guides/wai-way/context_map.json.example +95 -0
- package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +840 -0
- package/templates/root-consumer/guides/wai-way/page_patterns.md.example +686 -0
- package/templates/root-consumer/guides/wai-way/presets_guide.md.example +217 -0
- package/templates/root-consumer/guides/wai-way/spec_template.md.example +312 -0
- package/templates/functions-firebase/functions-firebase/src/crud/createEntity.ts.example +0 -19
- package/templates/functions-firebase/functions-firebase/src/crud/deleteEntity.ts.example +0 -14
- package/templates/functions-firebase/functions-firebase/src/crud/getEntity.ts.example +0 -14
- package/templates/functions-firebase/functions-firebase/src/crud/index.ts.example +0 -12
- package/templates/functions-firebase/functions-firebase/src/crud/listEntities.ts.example +0 -14
- package/templates/functions-firebase/functions-firebase/src/crud/updateEntity.ts.example +0 -14
- package/templates/root-consumer/guides/COMPONENTS_CRUD.md.example +0 -70
- package/templates/root-consumer/guides/SETUP_CRUD.md.example +0 -1244
- package/templates/root-consumer/guides/SETUP_FUNCTIONS.md.example +0 -114
- /package/templates/root-consumer/guides/{COMPONENTS_ADV.md.example → dndev/COMPONENTS_ADV.md.example} +0 -0
- /package/templates/root-consumer/guides/{COMPONENTS_ATOMIC.md.example → dndev/COMPONENTS_ATOMIC.md.example} +0 -0
- /package/templates/root-consumer/guides/{COMPONENTS_UI.md.example → dndev/COMPONENTS_UI.md.example} +0 -0
- /package/templates/root-consumer/guides/{ENV_SETUP.md.example → dndev/ENV_SETUP.md.example} +0 -0
- /package/templates/root-consumer/guides/{INDEX.md.example → dndev/INDEX.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_APP_CONFIG.md.example → dndev/SETUP_APP_CONFIG.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_BILLING.md.example → dndev/SETUP_BILLING.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_I18N.md.example → dndev/SETUP_I18N.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_LAYOUTS.md.example → dndev/SETUP_LAYOUTS.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_OAUTH.md.example → dndev/SETUP_OAUTH.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_PAGES.md.example → dndev/SETUP_PAGES.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_PWA.md.example → dndev/SETUP_PWA.md.example} +0 -0
- /package/templates/root-consumer/guides/{SETUP_THEMES.md.example → dndev/SETUP_THEMES.md.example} +0 -0
- /package/templates/root-consumer/guides/{USE_ROUTING.md.example → dndev/USE_ROUTING.md.example} +0 -0
- /package/templates/root-consumer/guides/{advanced → dndev/advanced}/APP_CHECK.md.example +0 -0
- /package/templates/root-consumer/guides/{advanced → dndev/advanced}/COOKIE_REFERENCE.md.example +0 -0
- /package/templates/root-consumer/guides/{advanced → dndev/advanced}/EMULATORS.md.example +0 -0
- /package/templates/root-consumer/guides/{advanced → dndev/advanced}/VERSION_CONTROL.md.example +0 -0
|
@@ -1,1244 +0,0 @@
|
|
|
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.
|