@donotdev/cli 0.0.5 → 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.
Files changed (102) hide show
  1. package/dependencies-matrix.json +76 -34
  2. package/dist/bin/commands/build.js +165 -161
  3. package/dist/bin/commands/bump.js +172 -160
  4. package/dist/bin/commands/cacheout.js +163 -157
  5. package/dist/bin/commands/create-app.js +205 -163
  6. package/dist/bin/commands/create-project.js +176 -161
  7. package/dist/bin/commands/deploy.js +480 -472
  8. package/dist/bin/commands/dev.js +164 -158
  9. package/dist/bin/commands/emu.js +164 -158
  10. package/dist/bin/commands/format.js +163 -157
  11. package/dist/bin/commands/lint.js +166 -157
  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 +164 -158
  17. package/dist/bin/commands/sync-secrets.js +164 -158
  18. package/dist/bin/commands/wai.d.ts +11 -0
  19. package/dist/bin/commands/wai.d.ts.map +1 -0
  20. package/dist/bin/commands/wai.js +12 -0
  21. package/dist/bin/commands/wai.js.map +1 -0
  22. package/dist/bin/dndev.js +24 -8
  23. package/dist/bin/donotdev.js +24 -8
  24. package/dist/index.js +557 -514
  25. package/package.json +1 -1
  26. package/templates/app-demo/index.html.example +4 -0
  27. package/templates/app-demo/src/App.tsx.example +28 -10
  28. package/templates/app-demo/src/config/app.ts.example +68 -0
  29. package/templates/app-next/src/app/ClientLayout.tsx.example +4 -3
  30. package/templates/app-next/src/app/layout.tsx.example +17 -25
  31. package/templates/app-next/src/config/app.ts.example +75 -48
  32. package/templates/app-next/src/globals.css.example +10 -7
  33. package/templates/app-next/src/locales/dndev_en.json.example +68 -0
  34. package/templates/app-next/src/pages/locales/example_en.json.example +5 -0
  35. package/templates/app-vite/index.html.example +71 -34
  36. package/templates/app-vite/src/config/app.ts.example +75 -47
  37. package/templates/app-vite/src/globals.css.example +14 -6
  38. package/templates/app-vite/src/locales/dndev_en.json.example +68 -0
  39. package/templates/app-vite/src/pages/FormPageExample.tsx.example +152 -0
  40. package/templates/app-vite/src/pages/HomePage.tsx.example +81 -134
  41. package/templates/app-vite/src/pages/ListPageExample.tsx.example +88 -0
  42. package/templates/functions-firebase/README.md.example +25 -0
  43. package/templates/functions-firebase/build.mjs.example +8 -1
  44. package/templates/functions-firebase/functions-firebase/build.mjs.example +8 -1
  45. package/templates/functions-firebase/functions-firebase/src/index.ts.example +19 -25
  46. package/templates/functions-firebase/functions.config.js.example +35 -0
  47. package/templates/functions-firebase/tsconfig.json.example +3 -13
  48. package/templates/functions-vercel/tsconfig.json.example +1 -13
  49. package/templates/root-consumer/entities/ExampleEntity.ts.example +223 -0
  50. package/templates/root-consumer/entities/demo.ts.example +562 -0
  51. package/templates/root-consumer/entities/index.ts.example +15 -0
  52. package/templates/root-consumer/firebase.json.example +1 -1
  53. package/templates/root-consumer/guides/{AGENT_START_HERE.md.example → dndev/AGENT_START_HERE.md.example} +22 -0
  54. package/templates/root-consumer/guides/{COMPONENTS_ADV.md.example → dndev/COMPONENTS_ADV.md.example} +456 -360
  55. package/templates/root-consumer/guides/{COMPONENTS_ATOMIC.md.example → dndev/COMPONENTS_ATOMIC.md.example} +42 -0
  56. package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +231 -0
  57. package/templates/root-consumer/guides/{INDEX.md.example → dndev/INDEX.md.example} +3 -0
  58. package/templates/root-consumer/guides/{SETUP_APP_CONFIG.md.example → dndev/SETUP_APP_CONFIG.md.example} +5 -2
  59. package/templates/root-consumer/guides/{SETUP_AUTH.md.example → dndev/SETUP_AUTH.md.example} +30 -0
  60. package/templates/root-consumer/guides/{SETUP_BILLING.md.example → dndev/SETUP_BILLING.md.example} +44 -4
  61. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +473 -0
  62. package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +116 -0
  63. package/templates/root-consumer/guides/{SETUP_PAGES.md.example → dndev/SETUP_PAGES.md.example} +17 -0
  64. package/templates/root-consumer/guides/dndev/SETUP_PWA.md.example +213 -0
  65. package/templates/root-consumer/guides/dndev/USE_ROUTING.md.example +503 -0
  66. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +404 -0
  67. package/templates/root-consumer/guides/wai-way/agents/architect.md.example +78 -0
  68. package/templates/root-consumer/guides/wai-way/agents/builder.md.example +87 -0
  69. package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +325 -0
  70. package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +100 -0
  71. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +281 -0
  72. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +77 -0
  73. package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +104 -0
  74. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +124 -0
  75. package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +165 -0
  76. package/templates/root-consumer/guides/wai-way/context_map.json.example +95 -0
  77. package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +840 -0
  78. package/templates/root-consumer/guides/wai-way/page_patterns.md.example +686 -0
  79. package/templates/root-consumer/guides/wai-way/presets_guide.md.example +217 -0
  80. package/templates/root-consumer/guides/wai-way/spec_template.md.example +312 -0
  81. package/templates/root-consumer/vercel.json.example +315 -20
  82. package/templates/app-demo/src/Routes.tsx.example +0 -20
  83. package/templates/app-vite/src/Routes.tsx.example +0 -16
  84. package/templates/app-vite/src/pages/locales/README.md.example +0 -1
  85. package/templates/functions-firebase/functions-firebase/src/crud/createEntity.ts.example +0 -19
  86. package/templates/functions-firebase/functions-firebase/src/crud/deleteEntity.ts.example +0 -14
  87. package/templates/functions-firebase/functions-firebase/src/crud/getEntity.ts.example +0 -14
  88. package/templates/functions-firebase/functions-firebase/src/crud/index.ts.example +0 -12
  89. package/templates/functions-firebase/functions-firebase/src/crud/listEntities.ts.example +0 -14
  90. package/templates/functions-firebase/functions-firebase/src/crud/updateEntity.ts.example +0 -14
  91. package/templates/root-consumer/guides/COMPONENTS_CRUD.md.example +0 -70
  92. package/templates/root-consumer/guides/SETUP_FUNCTIONS.md.example +0 -62
  93. /package/templates/root-consumer/guides/{COMPONENTS_UI.md.example → dndev/COMPONENTS_UI.md.example} +0 -0
  94. /package/templates/root-consumer/guides/{ENV_SETUP.md.example → dndev/ENV_SETUP.md.example} +0 -0
  95. /package/templates/root-consumer/guides/{SETUP_I18N.md.example → dndev/SETUP_I18N.md.example} +0 -0
  96. /package/templates/root-consumer/guides/{SETUP_LAYOUTS.md.example → dndev/SETUP_LAYOUTS.md.example} +0 -0
  97. /package/templates/root-consumer/guides/{SETUP_OAUTH.md.example → dndev/SETUP_OAUTH.md.example} +0 -0
  98. /package/templates/root-consumer/guides/{SETUP_THEMES.md.example → dndev/SETUP_THEMES.md.example} +0 -0
  99. /package/templates/root-consumer/guides/{advanced → dndev/advanced}/APP_CHECK.md.example +0 -0
  100. /package/templates/root-consumer/guides/{advanced → dndev/advanced}/COOKIE_REFERENCE.md.example +0 -0
  101. /package/templates/root-consumer/guides/{advanced → dndev/advanced}/EMULATORS.md.example +0 -0
  102. /package/templates/root-consumer/guides/{advanced → dndev/advanced}/VERSION_CONTROL.md.example +0 -0
@@ -0,0 +1,473 @@
1
+ # Setup: CRUD Operations
2
+
3
+ **CRUD = Create, Read, Update, Delete.** Framework handles data ops with built-in security.
4
+
5
+ ---
6
+
7
+ ## 1. Define Entity
8
+
9
+ ```typescript
10
+ // entities/product.ts
11
+ import { defineEntity } from '@donotdev/core';
12
+
13
+ export const productEntity = defineEntity({
14
+ name: 'Product',
15
+ collection: 'products',
16
+ fields: {
17
+ name: {
18
+ name: 'name',
19
+ label: 'name',
20
+ type: 'text',
21
+ visibility: 'guest',
22
+ validation: { required: true }
23
+ },
24
+ price: {
25
+ name: 'price',
26
+ label: 'price',
27
+ type: 'number',
28
+ visibility: 'guest',
29
+ validation: { required: true }
30
+ },
31
+ image: {
32
+ name: 'image',
33
+ label: 'image',
34
+ type: 'image',
35
+ visibility: 'guest'
36
+ },
37
+ customerContact: {
38
+ name: 'customerContact',
39
+ label: 'customerContact',
40
+ type: 'email',
41
+ visibility: 'admin',
42
+ editable: 'admin'
43
+ }
44
+ }
45
+ });
46
+ ```
47
+
48
+ **Auto-added fields:** `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status`
49
+
50
+ ---
51
+
52
+ ## 2. Visibility & Access
53
+
54
+ ### Field Visibility (who SEES)
55
+ | Level | Who |
56
+ |-------|-----|
57
+ | `'guest'` | Everyone |
58
+ | `'user'` | Authenticated |
59
+ | `'admin'` | Admins |
60
+ | `'super'` | Super admins |
61
+ | `'hidden'` | Never |
62
+
63
+ ### Entity Access (who CAN DO)
64
+ ```typescript
65
+ access: {
66
+ create: 'admin', // default
67
+ read: 'guest', // default
68
+ update: 'admin', // default
69
+ delete: 'admin', // default
70
+ }
71
+ ```
72
+
73
+ **Override:**
74
+ ```typescript
75
+ // Contact form - guests submit, admins read
76
+ access: { create: 'guest', read: 'admin' }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 3. Register Functions (Backend)
82
+
83
+ ```typescript
84
+ // functions/src/index.ts
85
+ import { initializeApp } from 'firebase-admin/app';
86
+ import { createCrudFunctions } from '@donotdev/functions/firebase';
87
+ import * as entities from 'entities';
88
+
89
+ initializeApp();
90
+ export const crud = createCrudFunctions(entities);
91
+ ```
92
+
93
+ **Deploy:** `dndev deploy`
94
+
95
+ ---
96
+
97
+ ## 4. Frontend Usage
98
+
99
+ ### Create/Edit Page
100
+
101
+ ```tsx
102
+ import { EntityFormRenderer, useCrud } from '@donotdev/crud';
103
+ import { productEntity } from 'entities';
104
+
105
+ export default function ProductPage() {
106
+ const { id } = useParams<{ id: string }>();
107
+ const { add, update, get } = useCrud(productEntity);
108
+ const isNew = id === 'new';
109
+ const [data, setData] = useState<any>(null);
110
+
111
+ useEffect(() => {
112
+ if (!isNew && id) get(id).then(setData);
113
+ }, [id]);
114
+
115
+ const handleSubmit = async (formData: any) => {
116
+ isNew ? await add(formData) : await update(id!, formData);
117
+ navigate('/products');
118
+ };
119
+
120
+ // DON'T mount form until data ready (edit mode)
121
+ if (!isNew && !data) return <Spinner />;
122
+
123
+ return (
124
+ <EntityFormRenderer
125
+ entity={productEntity}
126
+ operation={isNew ? 'create' : 'edit'}
127
+ defaultValues={data}
128
+ onSubmit={handleSubmit}
129
+ />
130
+ );
131
+ }
132
+ ```
133
+
134
+ ### List Page
135
+
136
+ ```tsx
137
+ import { EntityList } from '@donotdev/crud';
138
+ import { productEntity } from 'entities';
139
+
140
+ export default function ProductsPage() {
141
+ return <EntityList entity={productEntity} onRowClick={(p) => navigate(`/products/${p.id}`)} />;
142
+ }
143
+ ```
144
+
145
+ ### Card Grid
146
+
147
+ ```tsx
148
+ import { EntityCardList } from '@donotdev/crud';
149
+
150
+ export default function ShopPage() {
151
+ return <EntityCardList entity={productEntity} />;
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 5. Field Types
158
+
159
+ ### Text Inputs
160
+ - `text` - Single-line text input
161
+ - `email` - Email input with validation
162
+ - `tel` - Phone number input
163
+ - `url` - URL input
164
+ - `color` - Color picker
165
+ - `password` - Password input (masked)
166
+ - `textarea` - Multi-line text input
167
+ - `richtext` - Rich text editor
168
+
169
+ ### Numbers
170
+ - `number` - Numeric input
171
+ - `range` - Slider input
172
+
173
+ ### Boolean
174
+ - `checkbox` - Checkbox input
175
+ - `boolean` - Alias for checkbox
176
+ - `switch` - Toggle switch
177
+
178
+ ### Dates & Time
179
+ - `date` - Date picker
180
+ - `datetime-local` - Date and time picker
181
+ - `time` - Time picker
182
+ - `week` - Week picker
183
+ - `month` - Month picker
184
+ - `timestamp` - Timestamp (Firestore Timestamp)
185
+
186
+ ### Selection
187
+ - `select` - Dropdown select
188
+ - `combobox` - Searchable dropdown
189
+ - `multiselect` - Multiple selection dropdown
190
+ - `radio` - Radio button group
191
+
192
+ ### Files & Media
193
+ - `file` - Single file upload (deferred: uploads on form submit)
194
+ - `files` - Multiple file uploads (deferred: uploads on form submit)
195
+ - `document` - Document upload (PDF, etc.) (deferred: uploads on form submit)
196
+ - `documents` - Multiple document uploads (deferred: uploads on form submit)
197
+ - `image` - Single image upload (deferred: uploads on form submit, optimistic UI updates)
198
+ - `images` - Multiple image uploads (deferred: uploads on form submit, optimistic UI updates)
199
+
200
+ **Note:** File uploads use deferred uploads when inside `EntityFormRenderer` - files upload to storage when you submit the form. Images show immediately with optimistic updates (blob URLs) and automatically replace with storage URLs after upload completes.
201
+
202
+ ### Complex Types
203
+ - `geopoint` - Geographic coordinates (lat/lng)
204
+ - `address` - Address input with autocomplete
205
+ - `map` - Map picker
206
+ - `array` - Array of text inputs
207
+
208
+ ### Special
209
+ - `avatar` - Avatar image upload
210
+ - `badge` - Badge display
211
+ - `hidden` - Hidden field (not displayed)
212
+ - `submit` - Submit button (uncontrolled)
213
+ - `reset` - Reset button (uncontrolled)
214
+
215
+ ### Select Options
216
+
217
+ ```typescript
218
+ category: {
219
+ type: 'select',
220
+ validation: {
221
+ options: [
222
+ { value: 'electronics', label: 'Electronics' },
223
+ { value: 'clothing', label: 'Clothing' }
224
+ ]
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### Year Dropdown
230
+
231
+ ```typescript
232
+ import { yearOptions } from '@donotdev/crud';
233
+
234
+ year: {
235
+ type: 'select',
236
+ validation: { options: yearOptions(1990) } // 1990 to now, descending
237
+ }
238
+ ```
239
+
240
+ ### Switch with Custom Values
241
+
242
+ ```typescript
243
+ transmission: {
244
+ type: 'switch',
245
+ options: {
246
+ fieldSpecific: {
247
+ uncheckedValue: 'Manual',
248
+ checkedValue: 'Automatic',
249
+ }
250
+ }
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ## 6. Custom Fields & Schemas
257
+
258
+ ### Custom Field Types with UI Components
259
+
260
+ For custom field types that need custom UI components:
261
+
262
+ ```typescript
263
+ import { registerFieldType, type ControlledFieldProps } from '@donotdev/crud';
264
+ import { useController } from 'react-hook-form';
265
+ import * as v from 'valibot';
266
+
267
+ // Custom controlled component MUST use framework's useController
268
+ import { useController, registerFieldType } from '@donotdev/crud';
269
+ import type { ControlledFieldProps } from '@donotdev/crud';
270
+
271
+ function RepairOperationsField({
272
+ fieldConfig,
273
+ control,
274
+ errors,
275
+ t,
276
+ }: ControlledFieldProps) {
277
+ // REQUIRED: Use framework's useController (not react-hook-form's) - ensures type compatibility
278
+ const { field, fieldState } = useController({
279
+ name: fieldConfig.name,
280
+ control: control,
281
+ });
282
+
283
+ // Use field.value and field.onChange for form state
284
+ const value = (field.value as any) || [];
285
+
286
+ return (
287
+ <div>
288
+ <label>{t(fieldConfig.label)}</label>
289
+ {/* Your custom UI here */}
290
+ {fieldState?.error && (
291
+ <span className="error">{fieldState.error.message}</span>
292
+ )}
293
+ </div>
294
+ );
295
+ }
296
+
297
+ // Register UI component
298
+ registerFieldType({
299
+ type: 'repairOperations',
300
+ controlledComponent: RepairOperationsField,
301
+ });
302
+
303
+ // Then define schema in entity
304
+ export const carEntity = defineEntity({
305
+ name: 'Car',
306
+ collection: 'cars',
307
+ fields: {
308
+ repairs: {
309
+ name: 'repairs',
310
+ label: 'repairs',
311
+ type: 'repairOperations' as any,
312
+ visibility: 'admin',
313
+ validation: {
314
+ required: false,
315
+ // Custom Valibot schema - single source of truth
316
+ schema: v.nullish(v.array(v.object({
317
+ operation: v.string(),
318
+ cost: v.number(),
319
+ })))
320
+ }
321
+ }
322
+ }
323
+ });
324
+ ```
325
+
326
+ **Important:**
327
+ - Custom controlled components receive `control` prop, NOT `field` prop
328
+ - You MUST use `useController` hook to get `field` and `fieldState`
329
+ - Define schema in `validation.schema` - it's the single source of truth
330
+
331
+ ### Custom Schemas (No Custom UI)
332
+
333
+ For custom validation without custom UI, just define the schema:
334
+
335
+ ```typescript
336
+ import * as v from 'valibot';
337
+
338
+ export const carEntity = defineEntity({
339
+ name: 'Car',
340
+ collection: 'cars',
341
+ fields: {
342
+ // Custom array field with inline schema
343
+ repairs: {
344
+ name: 'repairs',
345
+ label: 'repairs',
346
+ type: 'text', // Can be any type, schema takes precedence
347
+ visibility: 'admin',
348
+ validation: {
349
+ required: false,
350
+ // Custom Valibot schema - takes priority over type-based schema
351
+ schema: v.array(
352
+ v.object({
353
+ operation: v.string(),
354
+ cost: v.number(),
355
+ date: v.string(), // ISO date string
356
+ })
357
+ )
358
+ }
359
+ },
360
+
361
+ // Custom object field
362
+ metadata: {
363
+ name: 'metadata',
364
+ label: 'metadata',
365
+ type: 'map',
366
+ visibility: 'admin',
367
+ validation: {
368
+ schema: v.object({
369
+ source: v.string(),
370
+ importedAt: v.string(),
371
+ tags: v.array(v.string()),
372
+ })
373
+ }
374
+ }
375
+ }
376
+ });
377
+ ```
378
+
379
+ **How It Works:**
380
+ - `validation.schema` is the **single source of truth** - works everywhere (client + server)
381
+ - Takes priority over built-in type schemas
382
+ - Entity is self-contained - no separate registrations needed
383
+
384
+ ---
385
+
386
+ ## 7. Hooks API
387
+
388
+ ```typescript
389
+ // Actions
390
+ const { add, update, delete: remove, get } = useCrud(productEntity);
391
+
392
+ // List (auto-fetch for tables)
393
+ const { items, loading } = useCrudList(productEntity);
394
+
395
+ // Cards (optimized for public)
396
+ const { items, loading, loadMore } = useCrudCardList(productEntity);
397
+ ```
398
+
399
+ ---
400
+
401
+ ## 8. Custom Form Layouts
402
+
403
+ Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
404
+
405
+ ```tsx
406
+ import { useId, useEffect } from 'react';
407
+ import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
408
+ import { productEntity } from 'entities';
409
+
410
+ export function CustomProductForm() {
411
+ const formId = useId();
412
+ const { add } = useCrud(productEntity);
413
+ const {
414
+ control,
415
+ handleSubmit,
416
+ fields,
417
+ formStatus, // 'idle' | 'uploading' | 'submitting' | ...
418
+ uploadProgress, // 0-100 during uploads
419
+ cleanup
420
+ } = useEntityForm(productEntity, { formId });
421
+
422
+ useEffect(() => cleanup, [cleanup]);
423
+
424
+ return (
425
+ <UploadProvider formId={formId}>
426
+ <form onSubmit={handleSubmit(add)}>
427
+ {/* Your custom layout here */}
428
+ <Controller control={control} name="name" render={({ field }) => (
429
+ <input {...field} placeholder="Product name" />
430
+ )} />
431
+
432
+ <button type="submit" disabled={formStatus !== 'idle'}>
433
+ {formStatus === 'uploading' ? `Uploading ${uploadProgress}%...` : 'Save'}
434
+ </button>
435
+ </form>
436
+ </UploadProvider>
437
+ );
438
+ }
439
+ ```
440
+
441
+ **Key points:**
442
+ - Pass `formId` to get automatic upload handling and loading states
443
+ - Wrap with `UploadProvider` if using file/image fields
444
+ - `handleSubmit` automatically uploads files before validation
445
+
446
+ ---
447
+
448
+ ## 9. i18n
449
+
450
+ **Namespace:** `entity-${entity.name.toLowerCase()}`
451
+
452
+ **File:** `locales/entity-product_en.json`
453
+ ```json
454
+ {
455
+ "fields": {
456
+ "name": "Product Name",
457
+ "price": "Price"
458
+ },
459
+ "name": "Product"
460
+ }
461
+ ```
462
+
463
+ Field `label` → translation key in `fields.*`
464
+
465
+ ---
466
+
467
+ ## 10. Routing Convention
468
+
469
+ | Route | Purpose |
470
+ |-------|---------|
471
+ | `/${collection}` | List |
472
+ | `/${collection}/new` | Create |
473
+ | `/${collection}/:id` | Detail/Edit |
@@ -0,0 +1,116 @@
1
+ # Setup: Firebase Functions
2
+
3
+ **Most is pre-configured.** Functions scaffolded. Just add config and deploy.
4
+
5
+ ---
6
+
7
+ ## ⚠️ CRITICAL: Import Rules
8
+
9
+ **Functions run on Node.js - you MUST use `/server` imports or your function will crash.**
10
+
11
+ ```typescript
12
+ // ✅ CORRECT
13
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
14
+ import { handleError } from '@donotdev/core/server';
15
+
16
+ // ❌ WRONG - crashes on deploy
17
+ import { getFirestore } from '@donotdev/firebase';
18
+ import { handleError } from '@donotdev/core';
19
+ ```
20
+
21
+ **Rule:** Always use `/server` suffix for:
22
+ - `@donotdev/firebase/server`
23
+ - `@donotdev/core/server`
24
+ - `@donotdev/utils/server`
25
+
26
+ ---
27
+
28
+ ## CRUD Functions Setup
29
+
30
+ **One line to register all entity functions:**
31
+
32
+ ```typescript
33
+ // functions/src/index.ts
34
+ import { initializeApp } from 'firebase-admin/app';
35
+ import { createCrudFunctions } from '@donotdev/functions/firebase';
36
+ import * as entities from 'entities';
37
+
38
+ initializeApp();
39
+ export const crud = createCrudFunctions(entities);
40
+ ```
41
+
42
+ **Result:** Auto-generates `create_products`, `get_products`, `list_products`, `update_products`, `delete_products` for each entity.
43
+
44
+ **Access control:** Configured via `entity.access` (see SETUP_CRUD.md). All functions use `invoker: 'public'` - security enforced via role-based access in code.
45
+
46
+ ---
47
+
48
+ ## Custom Functions
49
+
50
+ **When writing custom functions, use `/server` imports:**
51
+
52
+ ```typescript
53
+ import { onCall } from 'firebase-functions/v2/https';
54
+ import { FUNCTION_CONFIG } from '@donotdev/functions/firebase';
55
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
56
+ import { handleError } from '@donotdev/core/server';
57
+
58
+ export const myFunction = onCall(FUNCTION_CONFIG, async (request) => {
59
+ const db = getFirebaseAdminFirestore();
60
+ // Your logic here
61
+ return { data: 'result' };
62
+ });
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Post-Deployment: Cloud Run IAM
68
+
69
+ **After deploying, you may see `403 Forbidden` on CORS preflight. Fix:**
70
+
71
+ ```bash
72
+ # PowerShell
73
+ gcloud run services update create-products --region=europe-west1 --no-invoker-iam-check --project=myproject
74
+ ```
75
+
76
+ **For all CRUD functions:**
77
+ ```powershell
78
+ $services = @('create-products', 'get-products', 'list-products', 'update-products', 'delete-products');
79
+ foreach ($service in $services) {
80
+ gcloud run services update $service --region=europe-west1 --no-invoker-iam-check --project=myproject
81
+ }
82
+ ```
83
+
84
+ **Why?** Cloud Run blocks unauthenticated OPTIONS (CORS preflight) by default. Your function still validates Firebase Auth - this only allows the preflight to pass.
85
+
86
+ ---
87
+
88
+ ## Function Naming
89
+
90
+ **Export name (camelCase):**
91
+ ```typescript
92
+ export const getDashboardMetrics = onCall(...);
93
+ // Client: httpsCallable(functions, 'getDashboardMetrics')
94
+ ```
95
+
96
+ **Operation ID (snake_case):**
97
+ ```typescript
98
+ createBaseFunction(config, handler, 'get_dashboard_metrics')
99
+ // Used for logging, rate limiting
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Environment
105
+
106
+ ```bash
107
+ # .env.local (local)
108
+ STRIPE_SECRET_KEY=sk_test_...
109
+
110
+ # .env (production, synced via dndev sync-secrets)
111
+ STRIPE_SECRET_KEY=sk_live_...
112
+ ```
113
+
114
+ ---
115
+
116
+ **Add functions, get backend. Framework handles the rest.**
@@ -111,8 +111,25 @@ import { useNavigate, Link } from '@donotdev/ui';
111
111
  const navigate = useNavigate();
112
112
  navigate('/about');
113
113
  <Link path="/about">About</Link>
114
+
115
+ // Card with navigation (supports middle-click)
116
+ import { Card } from '@donotdev/components';
117
+ import { Link } from '@donotdev/ui';
118
+
119
+ <Link
120
+ path="/about"
121
+ style={{ display: 'block', textDecoration: 'none', height: '100%' }}
122
+ aria-label="Learn more about About"
123
+ >
124
+ <Card title="About" subtitle="Learn more" />
125
+ </Link>
114
126
  ```
115
127
 
128
+ **Navigation patterns:**
129
+ - **Link component:** Use for text links, buttons, or wrapping other components
130
+ - **Card navigation:** Wrap Card in Link for clickable cards with middle-click support
131
+ - **onClick only:** Use `onClick={() => navigate()}` for actions that don't need middle-click
132
+
116
133
  **Pre-configured:** Navigation menu auto-generated, sitemap auto-built (if `generateSitemap: true`).
117
134
 
118
135
  ---