fauxbase 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +630 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# Fauxbase
|
|
2
|
+
|
|
3
|
+
**Start with fake. Ship with real. Change nothing.**
|
|
4
|
+
|
|
5
|
+
A frontend data layer that simulates your backend — entities, services, business logic, 13 query operators, auth, role-based access — all running in the browser. When your backend is ready, swap one config line.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @fauxbase/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The Problem
|
|
14
|
+
|
|
15
|
+
Every frontend project does this:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
1. Backend not ready → hardcode mock data in components
|
|
19
|
+
2. Mock data grows → copy-paste, no structure, spaghetti
|
|
20
|
+
3. Need filtering/search → hack together Array.filter()
|
|
21
|
+
4. Need pagination → hack together Array.slice()
|
|
22
|
+
5. Need auth → fake login with useState
|
|
23
|
+
6. Backend arrives → REWRITE all data fetching
|
|
24
|
+
7. Partial backend → half mock, half real, chaos
|
|
25
|
+
8. Migration bugs → deadline missed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Existing tools don't solve this:
|
|
29
|
+
|
|
30
|
+
| Tool | What it does | The gap |
|
|
31
|
+
|------|-------------|---------|
|
|
32
|
+
| **MSW** | Intercepts HTTP at network level | You mock the *transport*, not the *data layer*. No query engine, no auth simulation. |
|
|
33
|
+
| **json-server** | Fake REST from a JSON file | No query operators, no auth, no hooks, separate process. |
|
|
34
|
+
| **MirageJS** | In-browser mock server | No typed entities, limited query operators, no auth sim, largely abandoned. |
|
|
35
|
+
| **Zustand/Redux** | State management | Just state — no CRUD contract, no query engine, no migration path. |
|
|
36
|
+
|
|
37
|
+
**Fauxbase fills the gap**: a structured, type-safe data layer with query capabilities and auth that runs locally during development and transparently switches to your real backend.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
graph TB
|
|
45
|
+
subgraph "Your React App"
|
|
46
|
+
C[Components]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph "Fauxbase"
|
|
50
|
+
S[Service Layer<br/>hooks, validation, CRUD]
|
|
51
|
+
QE[QueryEngine<br/>13 operators, sort, paginate]
|
|
52
|
+
|
|
53
|
+
subgraph "Driver Abstraction"
|
|
54
|
+
LD[LocalDriver<br/>memory / localStorage / indexedDB]
|
|
55
|
+
HD[HttpDriver<br/>fetch + preset]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
subgraph "Storage"
|
|
60
|
+
MEM[(Memory)]
|
|
61
|
+
LS[(localStorage)]
|
|
62
|
+
API[(REST API)]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
C -->|fb.product.list, create, ...| S
|
|
66
|
+
S --> LD
|
|
67
|
+
S --> HD
|
|
68
|
+
LD --> QE
|
|
69
|
+
LD --> MEM
|
|
70
|
+
LD --> LS
|
|
71
|
+
HD -->|preset translates| API
|
|
72
|
+
|
|
73
|
+
style C fill:#61dafb,color:#000
|
|
74
|
+
style S fill:#f5c842,color:#000
|
|
75
|
+
style QE fill:#f5c842,color:#000
|
|
76
|
+
style LD fill:#4caf50,color:#fff
|
|
77
|
+
style HD fill:#2196f3,color:#fff
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Key insight**: Components always talk to the Service layer. The Service delegates to a Driver. The Driver is swappable. Your components never know whether they're hitting localStorage or a REST API.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Define your entities
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { Entity, field, relation, computed } from '@fauxbase/core';
|
|
90
|
+
|
|
91
|
+
export class Product extends Entity {
|
|
92
|
+
@field({ required: true }) name!: string;
|
|
93
|
+
@field({ required: true, min: 0 }) price!: number;
|
|
94
|
+
@field({ default: 0 }) stock!: number;
|
|
95
|
+
@field({ default: true }) isActive!: boolean;
|
|
96
|
+
|
|
97
|
+
@relation('category') categoryId!: string;
|
|
98
|
+
|
|
99
|
+
@computed(p => p.stock > 0 && p.isActive)
|
|
100
|
+
available!: boolean;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `Entity` base class gives you these fields automatically:
|
|
105
|
+
|
|
106
|
+
| Field | Type | Description |
|
|
107
|
+
|-------|------|-------------|
|
|
108
|
+
| `id` | `string` | Auto-generated UUID |
|
|
109
|
+
| `createdAt` | `string` | ISO timestamp, set on create |
|
|
110
|
+
| `updatedAt` | `string` | ISO timestamp, updated on every change |
|
|
111
|
+
| `createdById` | `string?` | User who created (when auth active) |
|
|
112
|
+
| `createdByName` | `string?` | Display name of creator |
|
|
113
|
+
| `updatedById` | `string?` | User who last updated |
|
|
114
|
+
| `updatedByName` | `string?` | Display name of updater |
|
|
115
|
+
| `deletedAt` | `string?` | Soft delete timestamp (null = not deleted) |
|
|
116
|
+
| `version` | `number` | Auto-incremented on every update |
|
|
117
|
+
|
|
118
|
+
### 2. Define services with business logic
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { Service, beforeCreate, beforeUpdate } from '@fauxbase/core';
|
|
122
|
+
import { Product } from './entities/product';
|
|
123
|
+
|
|
124
|
+
export class ProductService extends Service<Product> {
|
|
125
|
+
entity = Product;
|
|
126
|
+
endpoint = '/products';
|
|
127
|
+
|
|
128
|
+
@beforeCreate()
|
|
129
|
+
ensureUniqueName(data: Partial<Product>, existing: Product[]) {
|
|
130
|
+
if (existing.some(p => p.name === data.name)) {
|
|
131
|
+
throw new ConflictError(`Product "${data.name}" already exists`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@beforeUpdate()
|
|
136
|
+
preventNegativeStock(_id: string, data: Partial<Product>) {
|
|
137
|
+
if (data.stock !== undefined && data.stock < 0) {
|
|
138
|
+
throw new ValidationError('Stock cannot be negative');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Custom methods — available on both drivers
|
|
143
|
+
async getByCategory(categoryId: string, page = 1) {
|
|
144
|
+
return this.list({
|
|
145
|
+
filter: { categoryId, isActive: true },
|
|
146
|
+
sort: { field: 'name', direction: 'asc' },
|
|
147
|
+
page, size: 20,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Services give you:
|
|
154
|
+
|
|
155
|
+
| Method | Description |
|
|
156
|
+
|--------|-------------|
|
|
157
|
+
| `list(query)` | Filtered, sorted, paginated list |
|
|
158
|
+
| `get(id)` | Get by ID |
|
|
159
|
+
| `create(data)` | Create with hooks + validation |
|
|
160
|
+
| `update(id, data)` | Update with hooks |
|
|
161
|
+
| `delete(id)` | Soft delete |
|
|
162
|
+
| `count(filter?)` | Count matching records |
|
|
163
|
+
| `bulk.create([])` | Batch insert |
|
|
164
|
+
| `bulk.update([])` | Batch update |
|
|
165
|
+
| `bulk.delete([])` | Batch delete |
|
|
166
|
+
|
|
167
|
+
### 3. Define seed data
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { seed } from '@fauxbase/core';
|
|
171
|
+
import { Product } from './entities/product';
|
|
172
|
+
|
|
173
|
+
export const productSeed = seed(Product, [
|
|
174
|
+
{ name: 'Hair Clay', price: 185000, categoryId: 'seed:category:0', stock: 50 },
|
|
175
|
+
{ name: 'Beard Oil', price: 125000, categoryId: 'seed:category:1', stock: 30 },
|
|
176
|
+
]);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 4. Create the client
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { createClient } from '@fauxbase/core';
|
|
183
|
+
|
|
184
|
+
export const fb = createClient({
|
|
185
|
+
driver: import.meta.env.VITE_API_URL
|
|
186
|
+
? { type: 'http', baseUrl: import.meta.env.VITE_API_URL, preset: 'lightwind' }
|
|
187
|
+
: { type: 'local', persist: 'localStorage' },
|
|
188
|
+
|
|
189
|
+
services: {
|
|
190
|
+
product: ProductService,
|
|
191
|
+
category: CategoryService,
|
|
192
|
+
order: OrderService,
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
seeds: [categorySeed, productSeed],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Type-safe:
|
|
199
|
+
// fb.product.list(...) → ProductService
|
|
200
|
+
// fb.category.get(...) → CategoryService
|
|
201
|
+
// fb.product.getByCategory() → custom method, fully typed
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 5. Use it
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// List with filtering
|
|
208
|
+
const result = await fb.product.list({
|
|
209
|
+
filter: { price__gte: 100000, name__contains: 'hair' },
|
|
210
|
+
sort: { field: 'price', direction: 'desc' },
|
|
211
|
+
page: 1,
|
|
212
|
+
size: 20,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Create
|
|
216
|
+
const { data } = await fb.product.create({
|
|
217
|
+
name: 'New Product',
|
|
218
|
+
price: 150000,
|
|
219
|
+
categoryId: 'seed:category:0',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Update
|
|
223
|
+
await fb.product.update(data.id, { stock: 100 });
|
|
224
|
+
|
|
225
|
+
// Delete (soft)
|
|
226
|
+
await fb.product.delete(data.id);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Query Engine — 13 Operators
|
|
232
|
+
|
|
233
|
+
Every filter operator works identically on the local driver (in-memory) and the HTTP driver (translated to URL params).
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const result = await fb.product.list({
|
|
237
|
+
filter: {
|
|
238
|
+
price__gte: 100000, // price >= 100000
|
|
239
|
+
name__contains: 'pomade', // case-insensitive substring
|
|
240
|
+
categoryId__in: ['cat-1', 'cat-2'], // value in list
|
|
241
|
+
stock__between: [10, 100], // 10 <= stock <= 100
|
|
242
|
+
isActive: true, // exact match (eq implied)
|
|
243
|
+
description__isnull: false, // is not null
|
|
244
|
+
},
|
|
245
|
+
sort: { field: 'price', direction: 'desc' },
|
|
246
|
+
page: 1,
|
|
247
|
+
size: 20,
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### All operators
|
|
252
|
+
|
|
253
|
+
| Operator | Syntax | Description | Example |
|
|
254
|
+
|----------|--------|-------------|---------|
|
|
255
|
+
| `eq` | `field` or `field__eq` | Exact match | `{ isActive: true }` |
|
|
256
|
+
| `ne` | `field__ne` | Not equal | `{ status__ne: 'deleted' }` |
|
|
257
|
+
| `gt` | `field__gt` | Greater than | `{ price__gt: 100 }` |
|
|
258
|
+
| `gte` | `field__gte` | Greater than or equal | `{ price__gte: 100 }` |
|
|
259
|
+
| `lt` | `field__lt` | Less than | `{ stock__lt: 10 }` |
|
|
260
|
+
| `lte` | `field__lte` | Less than or equal | `{ stock__lte: 100 }` |
|
|
261
|
+
| `like` | `field__like` | Case-insensitive substring | `{ name__like: 'hair' }` |
|
|
262
|
+
| `contains` | `field__contains` | Same as `like` | `{ name__contains: 'hair' }` |
|
|
263
|
+
| `startswith` | `field__startswith` | Case-insensitive prefix | `{ name__startswith: 'ha' }` |
|
|
264
|
+
| `endswith` | `field__endswith` | Case-insensitive suffix | `{ email__endswith: '@gmail.com' }` |
|
|
265
|
+
| `between` | `field__between` | Inclusive range | `{ price__between: [100, 500] }` |
|
|
266
|
+
| `in` | `field__in` | Value in list | `{ status__in: ['active', 'pending'] }` |
|
|
267
|
+
| `isnull` | `field__isnull` | Null/undefined check | `{ deletedAt__isnull: true }` |
|
|
268
|
+
|
|
269
|
+
### How queries flow
|
|
270
|
+
|
|
271
|
+
```mermaid
|
|
272
|
+
flowchart LR
|
|
273
|
+
subgraph "Input"
|
|
274
|
+
Q["{ filter: { price__gte: 100 },<br/>sort: { field: 'name', direction: 'asc' },<br/>page: 1, size: 20 }"]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
subgraph "QueryEngine"
|
|
278
|
+
F[Filter<br/>13 operators]
|
|
279
|
+
S[Sort<br/>asc/desc, null-safe]
|
|
280
|
+
P[Paginate<br/>slice + meta]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
subgraph "Output"
|
|
284
|
+
R["{ items: [...],<br/>meta: { page, size,<br/>totalItems, totalPages } }"]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
Q --> F --> S --> P --> R
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Soft-deleted records (where `deletedAt` is set) are automatically excluded before any filtering.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Drivers
|
|
295
|
+
|
|
296
|
+
### Local Driver (development)
|
|
297
|
+
|
|
298
|
+
Runs entirely in the browser. No server needed.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
driver: { type: 'local', persist: 'memory' } // volatile, fastest
|
|
302
|
+
driver: { type: 'local', persist: 'localStorage' } // persists across refresh
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Store | Persists? | Limit | Best for |
|
|
306
|
+
|-------|-----------|-------|----------|
|
|
307
|
+
| `memory` | No | RAM | Unit tests, throwaway demos |
|
|
308
|
+
| `localStorage` | Yes | ~5MB | Default, small-medium datasets |
|
|
309
|
+
|
|
310
|
+
### HTTP Driver (production)
|
|
311
|
+
|
|
312
|
+
Translates Fauxbase calls to fetch requests. Uses **presets** to speak your backend's language.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
driver: {
|
|
316
|
+
type: 'http',
|
|
317
|
+
baseUrl: 'https://api.example.com',
|
|
318
|
+
preset: 'lightwind',
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Hybrid Driver (gradual migration)
|
|
323
|
+
|
|
324
|
+
When your backend is partially ready — migrate one service at a time:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
const fb = createClient({
|
|
328
|
+
driver: { type: 'local' }, // default for all
|
|
329
|
+
|
|
330
|
+
overrides: {
|
|
331
|
+
product: { driver: { type: 'http', baseUrl: 'https://api.example.com', preset: 'lightwind' } },
|
|
332
|
+
// category, order → still local
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Driver flow
|
|
338
|
+
|
|
339
|
+
```mermaid
|
|
340
|
+
flowchart TB
|
|
341
|
+
C[Component<br/>fb.product.list]
|
|
342
|
+
|
|
343
|
+
subgraph "Local Driver"
|
|
344
|
+
LS[(Storage<br/>memory/localStorage)]
|
|
345
|
+
QE1[QueryEngine<br/>in-memory processing]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
subgraph "HTTP Driver"
|
|
349
|
+
PR[Preset<br/>serialize/parse]
|
|
350
|
+
FE[fetch<br/>GET /products?...]
|
|
351
|
+
API[(REST API)]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
C -->|dev| LS --> QE1 --> R1[Normalized Response]
|
|
355
|
+
C -->|prod| PR --> FE --> API --> PR --> R2[Normalized Response]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Components always get the same normalized response shape, regardless of driver.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Backend Presets
|
|
363
|
+
|
|
364
|
+
A preset tells the HTTP driver how to talk to your backend — how to serialize queries, parse responses, and handle auth.
|
|
365
|
+
|
|
366
|
+
### Built-in presets
|
|
367
|
+
|
|
368
|
+
| Preset | Framework | Filter Style | Auth |
|
|
369
|
+
|--------|-----------|-------------|------|
|
|
370
|
+
| `lightwind` | Lightwind (Quarkus) | `?price__gte=100` | `/auth/login` |
|
|
371
|
+
| `spring-boot` | Spring Boot | `?price.gte=100` | `/api/auth/signin` |
|
|
372
|
+
| `nestjs` | NestJS | `?filter.price.$gte=100` | `/auth/login` |
|
|
373
|
+
| `laravel` | Laravel | `?filter[price_gte]=100` | `/api/login` |
|
|
374
|
+
| `django` | Django REST Framework | `?price__gte=100` | `/api/token/` |
|
|
375
|
+
| `express` | Express.js | `?price__gte=100` | `/auth/login` |
|
|
376
|
+
| `fastapi` | FastAPI | `?price__gte=100` | `/api/auth/token` |
|
|
377
|
+
| `rails` | Ruby on Rails | `?q[price_gteq]=100` | `/api/login` |
|
|
378
|
+
| `go-gin` | Go (Gin) | `?price__gte=100` | `/api/auth/login` |
|
|
379
|
+
|
|
380
|
+
### Custom presets
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { definePreset } from '@fauxbase/core';
|
|
384
|
+
|
|
385
|
+
const fb = createClient({
|
|
386
|
+
driver: {
|
|
387
|
+
type: 'http',
|
|
388
|
+
baseUrl: 'https://api.myapp.com',
|
|
389
|
+
preset: definePreset({
|
|
390
|
+
name: 'my-backend',
|
|
391
|
+
response: {
|
|
392
|
+
single: (raw) => ({ data: raw.result }),
|
|
393
|
+
list: (raw) => ({
|
|
394
|
+
items: raw.results,
|
|
395
|
+
meta: {
|
|
396
|
+
page: raw.page,
|
|
397
|
+
size: raw.per_page,
|
|
398
|
+
totalItems: raw.total,
|
|
399
|
+
totalPages: raw.pages,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
403
|
+
query: {
|
|
404
|
+
filterStyle: 'django',
|
|
405
|
+
pageParam: 'page',
|
|
406
|
+
sizeParam: 'per_page',
|
|
407
|
+
},
|
|
408
|
+
auth: {
|
|
409
|
+
loginUrl: '/api/v1/login',
|
|
410
|
+
tokenField: 'access_token',
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### How presets work
|
|
418
|
+
|
|
419
|
+
```mermaid
|
|
420
|
+
sequenceDiagram
|
|
421
|
+
participant Component
|
|
422
|
+
participant Service
|
|
423
|
+
participant HttpDriver
|
|
424
|
+
participant Preset
|
|
425
|
+
participant Backend
|
|
426
|
+
|
|
427
|
+
Component->>Service: fb.product.list({ filter: { price__gte: 100 } })
|
|
428
|
+
Service->>HttpDriver: list("product", query)
|
|
429
|
+
HttpDriver->>Preset: serialize query
|
|
430
|
+
Preset-->>HttpDriver: GET /products?price__gte=100&page=1&size=20
|
|
431
|
+
HttpDriver->>Backend: fetch(url, { headers: { Authorization } })
|
|
432
|
+
Backend-->>HttpDriver: { code: 200, data: { items, meta } }
|
|
433
|
+
HttpDriver->>Preset: parse response
|
|
434
|
+
Preset-->>HttpDriver: { items: [...], meta: { page, size, totalItems, totalPages } }
|
|
435
|
+
HttpDriver-->>Service: normalized PagedResponse
|
|
436
|
+
Service-->>Component: same shape as local driver
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Seeding Strategy
|
|
442
|
+
|
|
443
|
+
Seed data and runtime data are tracked separately.
|
|
444
|
+
|
|
445
|
+
```mermaid
|
|
446
|
+
stateDiagram-v2
|
|
447
|
+
[*] --> FirstLoad : App starts
|
|
448
|
+
FirstLoad --> HasData : Seeds applied
|
|
449
|
+
|
|
450
|
+
HasData --> HasData : Reloads (seeds unchanged)
|
|
451
|
+
note right of HasData : Seeds SKIPPED, data preserved
|
|
452
|
+
|
|
453
|
+
HasData --> SeedChanged : Reloads (seeds modified)
|
|
454
|
+
SeedChanged --> HasData : Upsert seed records
|
|
455
|
+
|
|
456
|
+
HasData --> ResetSeeds : Reset Seeds
|
|
457
|
+
ResetSeeds --> HasData : Wipe + re-seed
|
|
458
|
+
|
|
459
|
+
HasData --> RefreshSeeds : Refresh Seeds
|
|
460
|
+
RefreshSeeds --> HasData : Re-seed only
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**How it works:**
|
|
464
|
+
|
|
465
|
+
- Seed records get deterministic IDs: `seed:product:0`, `seed:product:1`, ...
|
|
466
|
+
- Runtime records (created during development) get normal UUIDs: `a3f1b2c4-...`
|
|
467
|
+
- Fauxbase tracks a `_seedVersion` hash — if your seed definitions change, only seed records are re-applied
|
|
468
|
+
- Runtime records are never touched during re-seeding
|
|
469
|
+
- On HTTP driver, seeding is disabled entirely — the backend owns the data
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Response Format
|
|
474
|
+
|
|
475
|
+
All operations return a normalized format. Components always see the same shape regardless of driver.
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// Single item (get, create, update)
|
|
479
|
+
interface ApiResponse<T> {
|
|
480
|
+
data: T;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// List (list)
|
|
484
|
+
interface PagedResponse<T> {
|
|
485
|
+
items: T[];
|
|
486
|
+
meta: {
|
|
487
|
+
page: number;
|
|
488
|
+
size: number;
|
|
489
|
+
totalItems: number;
|
|
490
|
+
totalPages: number;
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Errors (thrown as exceptions)
|
|
495
|
+
class NotFoundError // code: 'NOT_FOUND'
|
|
496
|
+
class ConflictError // code: 'CONFLICT'
|
|
497
|
+
class ValidationError // code: 'VALIDATION', details: { field: message }
|
|
498
|
+
class ForbiddenError // code: 'FORBIDDEN'
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Decorators
|
|
504
|
+
|
|
505
|
+
### Entity decorators
|
|
506
|
+
|
|
507
|
+
| Decorator | Purpose | Example |
|
|
508
|
+
|-----------|---------|---------|
|
|
509
|
+
| `@field(options?)` | Mark entity field with validation | `@field({ required: true, min: 0 })` |
|
|
510
|
+
| `@relation(entity)` | Foreign key relation | `@relation('category')` |
|
|
511
|
+
| `@computed(fn)` | Derived value, recalculated on access | `@computed(p => p.stock > 0)` |
|
|
512
|
+
|
|
513
|
+
### Service hook decorators
|
|
514
|
+
|
|
515
|
+
| Decorator | Signature | Purpose |
|
|
516
|
+
|-----------|-----------|---------|
|
|
517
|
+
| `@beforeCreate()` | `(data, existingItems) => void` | Validate/mutate before create |
|
|
518
|
+
| `@beforeUpdate()` | `(id, data) => void` | Validate/mutate before update |
|
|
519
|
+
| `@afterCreate()` | `(entity) => void` | Side effects after create |
|
|
520
|
+
| `@afterUpdate()` | `(entity) => void` | Side effects after update |
|
|
521
|
+
|
|
522
|
+
Hooks can throw errors to abort operations:
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
@beforeCreate()
|
|
526
|
+
ensureUnique(data: Partial<Product>, existing: Product[]) {
|
|
527
|
+
if (existing.some(p => p.name === data.name)) {
|
|
528
|
+
throw new ConflictError('Name must be unique');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## The Migration Timeline
|
|
536
|
+
|
|
537
|
+
```mermaid
|
|
538
|
+
gantt
|
|
539
|
+
title Frontend Development with Fauxbase
|
|
540
|
+
dateFormat YYYY-MM-DD
|
|
541
|
+
axisFormat Week %W
|
|
542
|
+
|
|
543
|
+
section Setup
|
|
544
|
+
Install Fauxbase + define entities/services :a1, 2024-01-01, 1d
|
|
545
|
+
Define seed data :a2, after a1, 1d
|
|
546
|
+
|
|
547
|
+
section Build (no backend)
|
|
548
|
+
Build full UI with local driver :b1, after a2, 28d
|
|
549
|
+
CRUD, filtering, pagination, auth — all work :b2, after a2, 28d
|
|
550
|
+
|
|
551
|
+
section Migrate (backend arrives)
|
|
552
|
+
Products API ready → hybrid mode :c1, after b1, 7d
|
|
553
|
+
All APIs ready → full HTTP driver :c2, after c1, 7d
|
|
554
|
+
Remove local driver config :c3, after c2, 1d
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
Week 1: npm install @fauxbase/core
|
|
559
|
+
Define entities, services, seeds
|
|
560
|
+
Build UI with local driver — everything works
|
|
561
|
+
|
|
562
|
+
Week 2-4: Building features at full speed
|
|
563
|
+
No blocking on backend, no mock data spaghetti
|
|
564
|
+
|
|
565
|
+
Week 5: "Products API is ready"
|
|
566
|
+
→ switch products to HTTP driver (hybrid mode)
|
|
567
|
+
→ zero component changes
|
|
568
|
+
|
|
569
|
+
Week 6: "All APIs ready"
|
|
570
|
+
→ set VITE_API_URL globally
|
|
571
|
+
→ done
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Project Structure
|
|
577
|
+
|
|
578
|
+
Recommended structure in your app:
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
src/
|
|
582
|
+
├── fauxbase/
|
|
583
|
+
│ ├── entities/
|
|
584
|
+
│ │ ├── product.ts ← class Product extends Entity
|
|
585
|
+
│ │ ├── category.ts
|
|
586
|
+
│ │ └── user.ts
|
|
587
|
+
│ ├── services/
|
|
588
|
+
│ │ ├── product.ts ← class ProductService extends Service<Product>
|
|
589
|
+
│ │ ├── category.ts
|
|
590
|
+
│ │ └── user.ts
|
|
591
|
+
│ ├── seeds/
|
|
592
|
+
│ │ ├── product.ts ← seed(Product, [...])
|
|
593
|
+
│ │ └── category.ts
|
|
594
|
+
│ └── index.ts ← createClient({ ... })
|
|
595
|
+
├── components/
|
|
596
|
+
│ └── ProductList.tsx ← fb.product.list({ ... })
|
|
597
|
+
└── main.tsx
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Technical Details
|
|
603
|
+
|
|
604
|
+
| Aspect | Detail |
|
|
605
|
+
|--------|--------|
|
|
606
|
+
| **Package** | `@fauxbase/core` (~8KB gzipped) |
|
|
607
|
+
| **Runtime deps** | Zero |
|
|
608
|
+
| **TypeScript** | First-class, full type inference |
|
|
609
|
+
| **Decorators** | `experimentalDecorators` (legacy TS decorators) |
|
|
610
|
+
| **Query operators** | 13: eq, ne, gt, gte, lt, lte, like, contains, startswith, endswith, between, in, isnull |
|
|
611
|
+
| **Storage** | memory, localStorage (indexedDB planned) |
|
|
612
|
+
| **Auth** | Planned (Phase 2) |
|
|
613
|
+
| **Presets** | lightwind, spring-boot, nestjs, laravel, express, django, fastapi, rails, go-gin, custom |
|
|
614
|
+
| **Build** | tsup (ESM + CJS + DTS) |
|
|
615
|
+
| **Tests** | vitest, 140+ tests, 97%+ coverage |
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Roadmap
|
|
620
|
+
|
|
621
|
+
- [x] **v0.1** — Core: Entity, Service, QueryEngine, LocalDriver, Seeds
|
|
622
|
+
- [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`, `useAuth`) + Auth simulation
|
|
623
|
+
- [ ] **v0.3** — HTTP Driver + Backend Presets + DevTools
|
|
624
|
+
- [ ] **v0.4** — IndexedDB, CLI (`npx fauxbase init`), Vue/Svelte adapters
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## License
|
|
629
|
+
|
|
630
|
+
MIT
|