fauxbase 0.1.1 → 0.4.0
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 +187 -513
- package/dist/index.cjs +937 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -4
- package/dist/index.d.ts +227 -4
- package/dist/index.js +924 -9
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -2,624 +2,298 @@
|
|
|
2
2
|
|
|
3
3
|
**Start with fake. Ship with real. Change nothing.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Fauxbase is a frontend data layer that simulates your backend during development, then connects to your real API without changing your components.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
npm install
|
|
8
|
+
npm install fauxbase
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
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
|
-
---
|
|
13
|
+
## 15-Second Example
|
|
83
14
|
|
|
84
|
-
|
|
15
|
+
Define your data:
|
|
85
16
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
17
|
+
```ts
|
|
18
|
+
class Product extends Entity {
|
|
19
|
+
@field({ required: true }) name!: string;
|
|
20
|
+
@field({ min: 0 }) price!: number;
|
|
21
|
+
@field({ default: 0 }) stock!: number;
|
|
101
22
|
}
|
|
102
23
|
```
|
|
103
24
|
|
|
104
|
-
|
|
25
|
+
Use it:
|
|
105
26
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
| `updatedByName` | `string?` | Display name of updater |
|
|
115
|
-
| `deletedAt` | `string?` | Soft delete timestamp (null = not deleted) |
|
|
116
|
-
| `version` | `number` | Auto-incremented on every update |
|
|
27
|
+
```ts
|
|
28
|
+
const fb = createClient({
|
|
29
|
+
services: { product: ProductService },
|
|
30
|
+
seeds: [seed(Product, [
|
|
31
|
+
{ name: 'Hair Clay', price: 185000, stock: 50 },
|
|
32
|
+
{ name: 'Beard Oil', price: 125000, stock: 30 },
|
|
33
|
+
])],
|
|
34
|
+
});
|
|
117
35
|
|
|
118
|
-
|
|
36
|
+
// Filter, sort, paginate — all work locally
|
|
37
|
+
const result = await fb.product.list({
|
|
38
|
+
filter: { price__gte: 100000 },
|
|
39
|
+
sort: { field: 'price', direction: 'desc' },
|
|
40
|
+
page: 1, size: 20,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
119
43
|
|
|
120
|
-
|
|
121
|
-
import { Service, beforeCreate, beforeUpdate } from '@fauxbase/core';
|
|
122
|
-
import { Product } from './entities/product';
|
|
44
|
+
That's it.
|
|
123
45
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
46
|
+
- Works locally with fake data
|
|
47
|
+
- Switch to real backend later
|
|
48
|
+
- No component changes
|
|
127
49
|
|
|
128
|
-
|
|
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
|
-
}
|
|
50
|
+
---
|
|
134
51
|
|
|
135
|
-
|
|
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
|
-
}
|
|
52
|
+
## How It Works
|
|
141
53
|
|
|
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
54
|
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
]);
|
|
55
|
+
Development Production
|
|
56
|
+
|
|
57
|
+
Your App Your App
|
|
58
|
+
↓ ↓
|
|
59
|
+
Fauxbase Fauxbase
|
|
60
|
+
↓ ↓
|
|
61
|
+
Local Driver HTTP Driver
|
|
62
|
+
(memory / localStorage) ↓
|
|
63
|
+
Your Backend API
|
|
177
64
|
```
|
|
178
65
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import { createClient } from '@fauxbase/core';
|
|
66
|
+
Components always talk to Fauxbase. Fauxbase talks to a driver. The driver is swappable. Your components never know whether they're hitting localStorage or a REST API.
|
|
183
67
|
|
|
184
|
-
|
|
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' },
|
|
68
|
+
---
|
|
188
69
|
|
|
189
|
-
|
|
190
|
-
product: ProductService,
|
|
191
|
-
category: CategoryService,
|
|
192
|
-
order: OrderService,
|
|
193
|
-
},
|
|
70
|
+
## The Problem
|
|
194
71
|
|
|
195
|
-
|
|
196
|
-
});
|
|
72
|
+
Every frontend project does this:
|
|
197
73
|
|
|
198
|
-
// Type-safe:
|
|
199
|
-
// fb.product.list(...) → ProductService
|
|
200
|
-
// fb.category.get(...) → CategoryService
|
|
201
|
-
// fb.product.getByCategory() → custom method, fully typed
|
|
202
74
|
```
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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);
|
|
75
|
+
Step 1: Backend not ready → hardcode mock data
|
|
76
|
+
Step 2: Mock data grows → copy-paste everywhere
|
|
77
|
+
Step 3: Need filtering → hack together Array.filter()
|
|
78
|
+
Step 4: Need pagination → hack together Array.slice()
|
|
79
|
+
Step 5: Need auth → fake login with useState
|
|
80
|
+
Step 6: Backend arrives → rewrite everything
|
|
227
81
|
```
|
|
228
82
|
|
|
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
|
-
```
|
|
83
|
+
Existing tools don't solve this:
|
|
250
84
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
|
254
|
-
|
|
255
|
-
|
|
|
256
|
-
|
|
|
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
|
-
```
|
|
85
|
+
| Tool | What it does | The gap |
|
|
86
|
+
|------|-------------|---------|
|
|
87
|
+
| **MSW** | Intercepts HTTP | No query engine, no auth, mocks transport not data |
|
|
88
|
+
| **json-server** | Fake REST from JSON | No query operators, no hooks, separate process |
|
|
89
|
+
| **MirageJS** | In-browser server | Limited operators, no typed entities, largely abandoned |
|
|
90
|
+
| **Zustand/Redux** | State management | No CRUD contract, no query engine, no migration path |
|
|
289
91
|
|
|
290
|
-
|
|
92
|
+
**Fauxbase removes the rewrite.**
|
|
291
93
|
|
|
292
94
|
---
|
|
293
95
|
|
|
294
|
-
##
|
|
96
|
+
## The Key Idea
|
|
295
97
|
|
|
296
|
-
|
|
98
|
+
Fauxbase runs your backend contract in the browser.
|
|
297
99
|
|
|
298
|
-
|
|
100
|
+
Entities define your data model. Services define business logic. A query engine handles filtering, sorting, and pagination.
|
|
299
101
|
|
|
300
|
-
|
|
301
|
-
driver: { type: 'local', persist: 'memory' } // volatile, fastest
|
|
302
|
-
driver: { type: 'local', persist: 'localStorage' } // persists across refresh
|
|
303
|
-
```
|
|
102
|
+
During development, it runs locally. When your backend is ready, it forwards the same calls to your API.
|
|
304
103
|
|
|
305
|
-
|
|
306
|
-
|-------|-----------|-------|----------|
|
|
307
|
-
| `memory` | No | RAM | Unit tests, throwaway demos |
|
|
308
|
-
| `localStorage` | Yes | ~5MB | Default, small-medium datasets |
|
|
104
|
+
---
|
|
309
105
|
|
|
310
|
-
|
|
106
|
+
## Core Features
|
|
311
107
|
|
|
312
|
-
|
|
108
|
+
- Entity system with decorators (`@field`, `@relation`, `@computed`)
|
|
109
|
+
- Service layer with lifecycle hooks (`@beforeCreate`, `@afterUpdate`, ...)
|
|
110
|
+
- 13 query operators (`eq`, `gte`, `contains`, `between`, `in`, ...)
|
|
111
|
+
- Seed data with deterministic IDs
|
|
112
|
+
- Local driver (memory / localStorage)
|
|
113
|
+
- HTTP driver for real backends
|
|
114
|
+
- Hybrid mode for gradual migration
|
|
115
|
+
- Backend presets (Spring Boot, NestJS, Laravel, Django, Rails, ...)
|
|
116
|
+
- Zero runtime dependencies (~8KB gzipped)
|
|
313
117
|
|
|
314
|
-
|
|
315
|
-
driver: {
|
|
316
|
-
type: 'http',
|
|
317
|
-
baseUrl: 'https://api.example.com',
|
|
318
|
-
preset: 'lightwind',
|
|
319
|
-
}
|
|
320
|
-
```
|
|
118
|
+
---
|
|
321
119
|
|
|
322
|
-
|
|
120
|
+
## Hybrid Mode
|
|
323
121
|
|
|
324
|
-
|
|
122
|
+
This is the killer feature. Migrate one service at a time:
|
|
325
123
|
|
|
326
|
-
```
|
|
124
|
+
```ts
|
|
327
125
|
const fb = createClient({
|
|
328
|
-
driver: { type: 'local' },
|
|
126
|
+
driver: { type: 'local' },
|
|
329
127
|
|
|
330
128
|
overrides: {
|
|
331
|
-
product: { driver: { type: 'http', baseUrl: '
|
|
332
|
-
// category, order → still local
|
|
129
|
+
product: { driver: { type: 'http', baseUrl: '/api', preset: 'spring-boot' } },
|
|
333
130
|
},
|
|
334
131
|
});
|
|
335
132
|
```
|
|
336
133
|
|
|
337
|
-
|
|
134
|
+
Products use the real API. Everything else stays local. Migrate at your own pace.
|
|
338
135
|
|
|
339
|
-
|
|
340
|
-
flowchart TB
|
|
341
|
-
C[Component<br/>fb.product.list]
|
|
136
|
+
---
|
|
342
137
|
|
|
343
|
-
|
|
344
|
-
LS[(Storage<br/>memory/localStorage)]
|
|
345
|
-
QE1[QueryEngine<br/>in-memory processing]
|
|
346
|
-
end
|
|
138
|
+
## AI Prototypes → Production
|
|
347
139
|
|
|
348
|
-
|
|
349
|
-
PR[Preset<br/>serialize/parse]
|
|
350
|
-
FE[fetch<br/>GET /products?...]
|
|
351
|
-
API[(REST API)]
|
|
352
|
-
end
|
|
140
|
+
Many AI-generated prototypes hardcode arrays:
|
|
353
141
|
|
|
354
|
-
|
|
355
|
-
|
|
142
|
+
```ts
|
|
143
|
+
const products = [
|
|
144
|
+
{ name: 'Hair Clay', price: 185000 },
|
|
145
|
+
{ name: 'Beard Oil', price: 125000 },
|
|
146
|
+
];
|
|
356
147
|
```
|
|
357
148
|
|
|
358
|
-
|
|
149
|
+
When the prototype becomes real, engineers must rewrite the data layer.
|
|
359
150
|
|
|
360
|
-
|
|
151
|
+
Fauxbase lets prototypes start with a real data contract:
|
|
361
152
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
153
|
+
```
|
|
154
|
+
Claude / Cursor prototype
|
|
155
|
+
↓
|
|
156
|
+
Fauxbase local driver (works immediately)
|
|
157
|
+
↓
|
|
158
|
+
Real backend later (no rewrite)
|
|
159
|
+
```
|
|
367
160
|
|
|
368
|
-
|
|
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` |
|
|
161
|
+
---
|
|
379
162
|
|
|
380
|
-
|
|
163
|
+
## Query Engine — 13 Operators
|
|
381
164
|
|
|
382
|
-
|
|
383
|
-
import { definePreset } from '@fauxbase/core';
|
|
165
|
+
Every operator works identically on local and HTTP drivers.
|
|
384
166
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
}),
|
|
167
|
+
```ts
|
|
168
|
+
const result = await fb.product.list({
|
|
169
|
+
filter: {
|
|
170
|
+
price__gte: 100000,
|
|
171
|
+
name__contains: 'pomade',
|
|
172
|
+
categoryId__in: ['cat-1', 'cat-2'],
|
|
173
|
+
stock__between: [10, 100],
|
|
174
|
+
isActive: true,
|
|
413
175
|
},
|
|
176
|
+
sort: { field: 'price', direction: 'desc' },
|
|
177
|
+
page: 1,
|
|
178
|
+
size: 20,
|
|
414
179
|
});
|
|
415
180
|
```
|
|
416
181
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
```
|
|
182
|
+
| Operator | Syntax | Example |
|
|
183
|
+
|----------|--------|---------|
|
|
184
|
+
| `eq` | `field` or `field__eq` | `{ isActive: true }` |
|
|
185
|
+
| `ne` | `field__ne` | `{ status__ne: 'deleted' }` |
|
|
186
|
+
| `gt` | `field__gt` | `{ price__gt: 100 }` |
|
|
187
|
+
| `gte` | `field__gte` | `{ price__gte: 100 }` |
|
|
188
|
+
| `lt` | `field__lt` | `{ stock__lt: 10 }` |
|
|
189
|
+
| `lte` | `field__lte` | `{ stock__lte: 100 }` |
|
|
190
|
+
| `like` | `field__like` | `{ name__like: 'hair' }` |
|
|
191
|
+
| `contains` | `field__contains` | `{ name__contains: 'hair' }` |
|
|
192
|
+
| `startswith` | `field__startswith` | `{ name__startswith: 'ha' }` |
|
|
193
|
+
| `endswith` | `field__endswith` | `{ email__endswith: '@gmail.com' }` |
|
|
194
|
+
| `between` | `field__between` | `{ price__between: [100, 500] }` |
|
|
195
|
+
| `in` | `field__in` | `{ status__in: ['active', 'pending'] }` |
|
|
196
|
+
| `isnull` | `field__isnull` | `{ deletedAt__isnull: true }` |
|
|
438
197
|
|
|
439
198
|
---
|
|
440
199
|
|
|
441
|
-
##
|
|
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
|
|
200
|
+
## Services & Hooks
|
|
449
201
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
SeedChanged --> HasData : Upsert seed records
|
|
202
|
+
```ts
|
|
203
|
+
class ProductService extends Service<Product> {
|
|
204
|
+
entity = Product;
|
|
205
|
+
endpoint = '/products';
|
|
455
206
|
|
|
456
|
-
|
|
457
|
-
|
|
207
|
+
@beforeCreate()
|
|
208
|
+
ensureUniqueName(data: Partial<Product>, existing: Product[]) {
|
|
209
|
+
if (existing.some(p => p.name === data.name)) {
|
|
210
|
+
throw new ConflictError(`Product "${data.name}" already exists`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
458
213
|
|
|
459
|
-
|
|
460
|
-
|
|
214
|
+
async getByCategory(categoryId: string) {
|
|
215
|
+
return this.list({
|
|
216
|
+
filter: { categoryId, isActive: true },
|
|
217
|
+
sort: { field: 'name', direction: 'asc' },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
461
221
|
```
|
|
462
222
|
|
|
463
|
-
|
|
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
|
|
223
|
+
Every service gets: `list`, `get`, `create`, `update`, `delete`, `count`, `bulk.create`, `bulk.update`, `bulk.delete`.
|
|
470
224
|
|
|
471
225
|
---
|
|
472
226
|
|
|
473
|
-
##
|
|
227
|
+
## Seeding
|
|
474
228
|
|
|
475
|
-
|
|
229
|
+
Seed data has deterministic IDs. Runtime data has UUIDs. They never collide.
|
|
476
230
|
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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'
|
|
231
|
+
```ts
|
|
232
|
+
const productSeed = seed(Product, [
|
|
233
|
+
{ name: 'Hair Clay', price: 185000, stock: 50 }, // → seed:product:0
|
|
234
|
+
{ name: 'Beard Oil', price: 125000, stock: 30 }, // → seed:product:1
|
|
235
|
+
]);
|
|
499
236
|
```
|
|
500
237
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
238
|
+
- Seeds auto-apply on first load
|
|
239
|
+
- Fauxbase tracks a version hash — if seeds change, only seed records are re-applied
|
|
240
|
+
- Runtime records are never touched during re-seeding
|
|
241
|
+
- On HTTP driver, seeding is disabled — the backend owns the data
|
|
504
242
|
|
|
505
|
-
|
|
243
|
+
---
|
|
506
244
|
|
|
507
|
-
|
|
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)` |
|
|
245
|
+
## Backend Presets
|
|
512
246
|
|
|
513
|
-
|
|
247
|
+
Works with any REST backend. Presets tell the HTTP driver how to serialize queries and parse responses.
|
|
514
248
|
|
|
515
|
-
|
|
|
516
|
-
|
|
517
|
-
|
|
|
518
|
-
|
|
|
519
|
-
|
|
|
520
|
-
|
|
|
249
|
+
| Preset | Framework | Filter Style |
|
|
250
|
+
|--------|-----------|-------------|
|
|
251
|
+
| `lightwind` | Lightwind (Quarkus) | `?price__gte=100` |
|
|
252
|
+
| `spring-boot` | Spring Boot | `?price.gte=100` |
|
|
253
|
+
| `nestjs` | NestJS | `?filter.price.$gte=100` |
|
|
254
|
+
| `laravel` | Laravel | `?filter[price_gte]=100` |
|
|
255
|
+
| `django` | Django REST Framework | `?price__gte=100` |
|
|
256
|
+
| `express` | Express.js | `?price__gte=100` |
|
|
257
|
+
| `fastapi` | FastAPI | `?price__gte=100` |
|
|
258
|
+
| `rails` | Ruby on Rails | `?q[price_gteq]=100` |
|
|
259
|
+
| `go-gin` | Go (Gin) | `?price__gte=100` |
|
|
521
260
|
|
|
522
|
-
|
|
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
|
-
```
|
|
261
|
+
Custom presets supported via `definePreset()`.
|
|
532
262
|
|
|
533
263
|
---
|
|
534
264
|
|
|
535
|
-
##
|
|
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
|
-
```
|
|
265
|
+
## Migration Timeline
|
|
556
266
|
|
|
557
267
|
```
|
|
558
|
-
Week 1
|
|
559
|
-
|
|
560
|
-
Build UI with local driver — everything works
|
|
268
|
+
Week 1 Install Fauxbase, define entities/services/seeds.
|
|
269
|
+
Build UI with local driver. Everything works.
|
|
561
270
|
|
|
562
|
-
Week 2-4
|
|
563
|
-
No blocking on backend
|
|
271
|
+
Week 2-4 Build features at full speed.
|
|
272
|
+
No blocking on backend. No mock data spaghetti.
|
|
564
273
|
|
|
565
|
-
Week 5
|
|
566
|
-
→
|
|
567
|
-
→
|
|
274
|
+
Week 5 "Products API is ready"
|
|
275
|
+
→ Switch products to HTTP driver (hybrid mode)
|
|
276
|
+
→ Zero component changes
|
|
568
277
|
|
|
569
|
-
Week 6
|
|
570
|
-
→
|
|
571
|
-
→ done
|
|
278
|
+
Week 6 "All APIs ready"
|
|
279
|
+
→ Set VITE_API_URL → done
|
|
572
280
|
```
|
|
573
281
|
|
|
574
282
|
---
|
|
575
283
|
|
|
576
|
-
##
|
|
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
|
-
---
|
|
284
|
+
## Who This Is For
|
|
601
285
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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 |
|
|
286
|
+
- Frontend teams waiting for backend APIs
|
|
287
|
+
- Solo devs building full-stack apps
|
|
288
|
+
- Prototypers using AI coding tools
|
|
289
|
+
- Teams building UI before backend is ready
|
|
616
290
|
|
|
617
291
|
---
|
|
618
292
|
|
|
619
293
|
## Roadmap
|
|
620
294
|
|
|
621
295
|
- [x] **v0.1** — Core: Entity, Service, QueryEngine, LocalDriver, Seeds
|
|
622
|
-
- [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation
|
|
296
|
+
- [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`) + Auth simulation
|
|
623
297
|
- [ ] **v0.3** — HTTP Driver + Backend Presets + DevTools
|
|
624
298
|
- [ ] **v0.4** — IndexedDB, CLI (`npx fauxbase init`), Vue/Svelte adapters
|
|
625
299
|
|