@koltakov/ffa-core 0.6.0 → 0.16.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 +402 -158
- package/dist/cli.js +361 -99
- package/dist/index.d.ts +75 -6
- package/dist/index.js +153 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# ffa-core
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Frontend First API** — instant mock REST API for frontend development.
|
|
4
4
|
|
|
5
|
-
Stop waiting for the backend. Describe your data, run one command, get a working API.
|
|
5
|
+
Stop waiting for the backend. Describe your data in one config file, run one command, get a fully working REST API.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @koltakov/ffa-core init
|
|
9
|
+
npx @koltakov/ffa-core dev
|
|
10
|
+
```
|
|
6
11
|
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
## Why ffa?
|
|
10
15
|
|
|
11
|
-
When building a frontend you need a real API to work against — but the backend isn't ready yet. `ffa` generates a fully functional REST API from a simple config file. No JSON files to maintain, no manual routes, no extra services.
|
|
12
|
-
|
|
13
16
|
- **Zero boilerplate** — one config file, one command
|
|
14
|
-
- **Auto CRUD** —
|
|
17
|
+
- **Auto CRUD** — 6 routes per entity, out of the box
|
|
15
18
|
- **Smart fake data** — field names drive generation (`email`, `price`, `avatar`, ...)
|
|
16
|
-
- **
|
|
19
|
+
- **Typed DSL** — `string().email().required()` with full TypeScript autocomplete
|
|
20
|
+
- **Zod validation** — POST/PUT/PATCH bodies validated against your schema
|
|
21
|
+
- **Relations** — `belongsTo` / `hasMany` with inline join via `?include=`
|
|
17
22
|
- **Swagger UI** — auto-generated docs at `/docs`
|
|
18
23
|
- **Delay simulation** — test loading states with artificial latency
|
|
19
24
|
- **TypeScript or JSON** — pick your config format
|
|
@@ -23,87 +28,94 @@ When building a frontend you need a real API to work against — but the backend
|
|
|
23
28
|
## Quick Start
|
|
24
29
|
|
|
25
30
|
```bash
|
|
26
|
-
# 1. Install
|
|
27
31
|
npm install @koltakov/ffa-core
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
npx ffa
|
|
31
|
-
|
|
32
|
-
# 3. Start
|
|
33
|
-
npx ffa dev
|
|
33
|
+
npx ffa init # scaffold ffa.config.ts
|
|
34
|
+
npx ffa dev # start server
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
Or skip `init` and write `ffa.config.ts` manually (see below).
|
|
37
|
-
|
|
38
37
|
---
|
|
39
38
|
|
|
40
|
-
## Config
|
|
39
|
+
## Config
|
|
41
40
|
|
|
42
41
|
```ts
|
|
43
|
-
|
|
42
|
+
// ffa.config.ts
|
|
43
|
+
import {
|
|
44
|
+
defineConfig, entity,
|
|
45
|
+
string, number, boolean, enumField,
|
|
46
|
+
belongsTo, hasMany, object, array,
|
|
47
|
+
} from '@koltakov/ffa-core'
|
|
44
48
|
|
|
45
49
|
export default defineConfig({
|
|
46
50
|
server: {
|
|
47
51
|
port: 3333,
|
|
48
|
-
delay: [200, 600], // artificial latency
|
|
52
|
+
delay: [200, 600], // artificial latency in ms (number or [min, max])
|
|
49
53
|
persist: true, // save data to ffa-data.json between restarts
|
|
54
|
+
errorRate: 0.05, // 5% of requests return 500 (for error state testing)
|
|
50
55
|
},
|
|
51
56
|
|
|
52
57
|
entities: {
|
|
53
58
|
User: entity({
|
|
54
|
-
name:
|
|
55
|
-
email:
|
|
56
|
-
role:
|
|
57
|
-
avatar: string().optional(),
|
|
58
|
-
|
|
59
|
+
name: string().fullName().required(),
|
|
60
|
+
email: string().email().required(),
|
|
61
|
+
role: enumField(['admin', 'editor', 'viewer']).required(),
|
|
62
|
+
avatar: string().avatar().optional(),
|
|
63
|
+
address: object({
|
|
64
|
+
city: string().city(),
|
|
65
|
+
country: string().country(),
|
|
66
|
+
zip: string().zip(),
|
|
67
|
+
}),
|
|
68
|
+
phones: array(string().phone()),
|
|
69
|
+
}, {
|
|
70
|
+
count: 20,
|
|
71
|
+
// Guaranteed records — appear first, rest are generated up to count
|
|
72
|
+
seed: [
|
|
73
|
+
{ name: 'Admin User', email: 'admin@example.com', role: 'admin' },
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
59
76
|
|
|
60
77
|
Post: entity({
|
|
61
|
-
title:
|
|
62
|
-
body:
|
|
63
|
-
status:
|
|
64
|
-
price:
|
|
78
|
+
title: string().sentence().required(),
|
|
79
|
+
body: string().paragraph().required(),
|
|
80
|
+
status: enumField(['draft', 'published', 'archived']).required(),
|
|
81
|
+
price: number().price().required(),
|
|
82
|
+
rating: number().rating().optional(),
|
|
65
83
|
authorId: belongsTo('User'),
|
|
84
|
+
tagIds: hasMany('Tag'),
|
|
85
|
+
}, {
|
|
86
|
+
count: 50,
|
|
87
|
+
meta: {
|
|
88
|
+
currency: 'USD', // static value
|
|
89
|
+
total: (items) => items.length, // computed from filtered items
|
|
90
|
+
avgPrice: (items) => +(
|
|
91
|
+
items.reduce((s, i) => s + (i.price as number), 0) / items.length
|
|
92
|
+
).toFixed(2),
|
|
93
|
+
},
|
|
66
94
|
}),
|
|
95
|
+
|
|
96
|
+
Tag: entity({
|
|
97
|
+
name: string().word().required(),
|
|
98
|
+
color: string().hexColor().optional(),
|
|
99
|
+
}, { count: 15 }),
|
|
67
100
|
},
|
|
68
101
|
})
|
|
69
102
|
```
|
|
70
103
|
|
|
71
|
-
## Config: JSON (no TypeScript needed)
|
|
72
|
-
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"server": { "port": 3333, "delay": 300 },
|
|
76
|
-
"entities": {
|
|
77
|
-
"Product": {
|
|
78
|
-
"count": 20,
|
|
79
|
-
"fields": {
|
|
80
|
-
"title": "string!",
|
|
81
|
-
"price": "number",
|
|
82
|
-
"status": ["draft", "published", "archived"],
|
|
83
|
-
"inStock": "boolean",
|
|
84
|
-
"createdAt": "datetime"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Save as `ffa.config.json`. Type suffix `!` means required (`"string!"` → required string). An array is shorthand for `enumField`.
|
|
92
|
-
|
|
93
104
|
---
|
|
94
105
|
|
|
95
106
|
## CLI
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
ffa dev
|
|
100
|
-
ffa dev
|
|
101
|
-
ffa dev
|
|
102
|
-
ffa dev -
|
|
103
|
-
|
|
104
|
-
ffa
|
|
105
|
-
ffa
|
|
106
|
-
|
|
108
|
+
| Command | Description |
|
|
109
|
+
|---------|-------------|
|
|
110
|
+
| `ffa dev` | Start the dev server |
|
|
111
|
+
| `ffa dev -p 4000` | Override port |
|
|
112
|
+
| `ffa dev -w` | Watch mode — restart on config changes |
|
|
113
|
+
| `ffa dev -o` | Open Swagger UI in browser on start |
|
|
114
|
+
| `ffa init` | Scaffold `ffa.config.ts` in current directory |
|
|
115
|
+
| `ffa inspect` | Show config structure without starting the server |
|
|
116
|
+
| `ffa snapshot` | Export current in-memory DB to JSON (server must be running) |
|
|
117
|
+
| `ffa snapshot -o dump.json` | Custom output path |
|
|
118
|
+
| `ffa reset` | Delete `ffa-data.json` (clears persisted data) |
|
|
107
119
|
|
|
108
120
|
---
|
|
109
121
|
|
|
@@ -111,167 +123,399 @@ ffa reset # delete ffa-data.json (clears persisted data)
|
|
|
111
123
|
|
|
112
124
|
For every entity `Foo` ffa creates six routes:
|
|
113
125
|
|
|
114
|
-
| Method
|
|
115
|
-
|
|
116
|
-
| `GET`
|
|
117
|
-
| `GET`
|
|
118
|
-
| `POST`
|
|
119
|
-
| `PUT`
|
|
120
|
-
| `PATCH`
|
|
121
|
-
| `DELETE`
|
|
126
|
+
| Method | Path | Description | Status |
|
|
127
|
+
|--------|------|-------------|--------|
|
|
128
|
+
| `GET` | `/foos` | List all records | 200 |
|
|
129
|
+
| `GET` | `/foos/:id` | Get single record | 200, 404 |
|
|
130
|
+
| `POST` | `/foos` | Create a record | 201, 422 |
|
|
131
|
+
| `PUT` | `/foos/:id` | Full replace | 200, 404, 422 |
|
|
132
|
+
| `PATCH` | `/foos/:id` | Partial update | 200, 404, 422 |
|
|
133
|
+
| `DELETE` | `/foos/:id` | Delete a record | 200, 404 |
|
|
122
134
|
|
|
123
135
|
> Names are auto-pluralized: `Product` → `/products`, `Category` → `/categories`.
|
|
124
136
|
|
|
137
|
+
### System endpoints
|
|
138
|
+
|
|
139
|
+
| Method | Path | Description |
|
|
140
|
+
|--------|------|-------------|
|
|
141
|
+
| `POST` | `/__reset` | Regenerate all data |
|
|
142
|
+
| `GET` | `/__snapshot` | Dump current DB to JSON |
|
|
143
|
+
| `GET` | `/docs` | Swagger UI |
|
|
144
|
+
| `GET` | `/openapi.json` | OpenAPI 3.0 spec |
|
|
145
|
+
|
|
125
146
|
---
|
|
126
147
|
|
|
127
|
-
## Query Params
|
|
148
|
+
## Query Params (GET /list)
|
|
149
|
+
|
|
150
|
+
### Pagination, sorting, search
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
?page=2&limit=10
|
|
154
|
+
?sort=price&order=desc
|
|
155
|
+
?search=apple full-text search across all string fields
|
|
156
|
+
?status=published exact match filter
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Filter operators
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
```
|
|
162
|
+
?price_gte=100 price >= 100
|
|
163
|
+
?price_lte=500 price <= 500
|
|
164
|
+
?price_gt=0 price > 0
|
|
165
|
+
?price_lt=1000 price < 1000
|
|
166
|
+
?price_ne=0 price != 0
|
|
167
|
+
?status_in=draft,published status in ['draft', 'published']
|
|
168
|
+
?title_contains=hello title contains 'hello' (case-insensitive)
|
|
169
|
+
```
|
|
137
170
|
|
|
138
171
|
All params are combinable:
|
|
139
172
|
```
|
|
140
|
-
GET /posts?search=react&
|
|
173
|
+
GET /posts?search=react&status_in=draft,published&price_gte=10&sort=price&order=asc&page=1&limit=20
|
|
141
174
|
```
|
|
142
175
|
|
|
143
|
-
|
|
176
|
+
### Response envelope
|
|
177
|
+
|
|
144
178
|
```json
|
|
145
179
|
{
|
|
146
|
-
"data": [
|
|
147
|
-
"meta": {
|
|
180
|
+
"data": [{ "id": "...", "title": "...", "price": 49.99 }],
|
|
181
|
+
"meta": {
|
|
182
|
+
"pagination": { "total": 47, "page": 1, "limit": 10, "pages": 5 },
|
|
183
|
+
"currency": "USD",
|
|
184
|
+
"avgPrice": 124.50
|
|
185
|
+
}
|
|
148
186
|
}
|
|
149
187
|
```
|
|
150
188
|
|
|
151
|
-
|
|
189
|
+
Header: `X-Total-Count: 47`
|
|
152
190
|
|
|
153
191
|
---
|
|
154
192
|
|
|
155
|
-
##
|
|
193
|
+
## Field Types
|
|
194
|
+
|
|
195
|
+
### Primitives
|
|
156
196
|
|
|
157
197
|
```ts
|
|
158
|
-
|
|
198
|
+
string() // chainable hints — see below
|
|
199
|
+
number() // chainable hints — see below
|
|
200
|
+
boolean()
|
|
201
|
+
uuid()
|
|
202
|
+
datetime()
|
|
203
|
+
```
|
|
159
204
|
|
|
160
|
-
|
|
161
|
-
User: entity({ name: string().required() }),
|
|
205
|
+
### Enum
|
|
162
206
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
authorId: belongsTo('User'), // stores a random User id
|
|
166
|
-
tagIds: hasMany('Tag'), // stores 1–3 random Tag ids
|
|
167
|
-
}),
|
|
168
|
-
}
|
|
207
|
+
```ts
|
|
208
|
+
enumField(['draft', 'published', 'archived'])
|
|
169
209
|
```
|
|
170
210
|
|
|
171
|
-
|
|
211
|
+
### Relations
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
belongsTo('User') // stores a random User id
|
|
215
|
+
hasMany('Tag') // stores an array of 1–3 Tag ids
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Inline join** on `GET /:id`:
|
|
172
219
|
```
|
|
173
220
|
GET /posts/abc?include=authorId,tagIds
|
|
174
221
|
```
|
|
175
|
-
|
|
222
|
+
|
|
223
|
+
### Nested structures (v0.12.0)
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// Nested object — generates each field recursively
|
|
227
|
+
address: object({
|
|
228
|
+
city: string().city(),
|
|
229
|
+
country: string().country(),
|
|
230
|
+
zip: string().zip(),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Array of typed items
|
|
234
|
+
tags: array(string().word())
|
|
235
|
+
phones: array(string().phone(), [1, 4]) // [min, max] items
|
|
236
|
+
scores: array(number().rating())
|
|
237
|
+
```
|
|
176
238
|
|
|
177
239
|
---
|
|
178
240
|
|
|
179
|
-
##
|
|
241
|
+
## String Fake Hints
|
|
180
242
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
243
|
+
Chain a method on `string()` to control what gets generated:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
// Internet
|
|
247
|
+
string().email() // "user@example.com"
|
|
248
|
+
string().url() // "https://example.com"
|
|
249
|
+
string().domain() // "example.com"
|
|
250
|
+
string().ip() // "192.168.1.1"
|
|
251
|
+
string().username() // "john_doe"
|
|
252
|
+
|
|
253
|
+
// Media
|
|
254
|
+
string().image() // "https://loremflickr.com/640/480"
|
|
255
|
+
string().avatar() // avatar URL
|
|
256
|
+
|
|
257
|
+
// Person
|
|
258
|
+
string().firstName() // "Alice"
|
|
259
|
+
string().lastName() // "Johnson"
|
|
260
|
+
string().fullName() // "Alice Johnson"
|
|
261
|
+
string().phone() // "+1-555-234-5678"
|
|
262
|
+
|
|
263
|
+
// Location
|
|
264
|
+
string().city() // "Berlin"
|
|
265
|
+
string().country() // "Germany"
|
|
266
|
+
string().address() // "12 Oak Street"
|
|
267
|
+
string().zip() // "10115"
|
|
268
|
+
string().locale() // "DE"
|
|
269
|
+
|
|
270
|
+
// Business
|
|
271
|
+
string().company() // "Acme Corp"
|
|
272
|
+
string().jobTitle() // "Senior Engineer"
|
|
273
|
+
string().department() // "Electronics"
|
|
274
|
+
string().currency() // "EUR"
|
|
275
|
+
|
|
276
|
+
// Text
|
|
277
|
+
string().word() // "matrix"
|
|
278
|
+
string().slug() // "hello-world"
|
|
279
|
+
string().sentence() // "The quick brown fox."
|
|
280
|
+
string().paragraph() // "Lorem ipsum dolor..."
|
|
281
|
+
string().bio() // "Lorem ipsum dolor..."
|
|
282
|
+
|
|
283
|
+
// Visual
|
|
284
|
+
string().color() // "azure"
|
|
285
|
+
string().hexColor() // "#A3F5C2"
|
|
286
|
+
|
|
287
|
+
// Id
|
|
288
|
+
string().uuid() // UUID v4
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Number Fake Hints
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
number().price() // 19.99
|
|
295
|
+
number().age() // 34
|
|
296
|
+
number().rating() // 4
|
|
297
|
+
number().percent() // 72
|
|
298
|
+
number().lat() // 51.5074
|
|
299
|
+
number().lng() // -0.1278
|
|
300
|
+
number().year() // 2021
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Smart Field-Name Detection
|
|
304
|
+
|
|
305
|
+
When no hint is set, ffa infers the right value from the field name automatically:
|
|
306
|
+
|
|
307
|
+
| Field name pattern | Generated value |
|
|
308
|
+
|--------------------|----------------|
|
|
309
|
+
| `email`, `mail` | Email address |
|
|
310
|
+
| `name`, `firstName` | First name |
|
|
311
|
+
| `lastName`, `surname` | Last name |
|
|
312
|
+
| `phone`, `tel`, `mobile` | Phone number |
|
|
313
|
+
| `city`, `country`, `address` | Location values |
|
|
314
|
+
| `url`, `website`, `link` | URL |
|
|
315
|
+
| `avatar`, `photo` | Avatar URL |
|
|
316
|
+
| `image` | Image URL |
|
|
317
|
+
| `company` | Company name |
|
|
318
|
+
| `title`, `heading` | Short sentence |
|
|
319
|
+
| `description`, `bio`, `text` | Paragraph |
|
|
320
|
+
| `price`, `cost`, `amount` | Price |
|
|
321
|
+
| `color` | Color name |
|
|
210
322
|
|
|
211
323
|
---
|
|
212
324
|
|
|
213
325
|
## Field Rules
|
|
214
326
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
|
218
|
-
|
|
219
|
-
| `.
|
|
220
|
-
| `.
|
|
221
|
-
| `.
|
|
327
|
+
All builders share these chainable rules:
|
|
328
|
+
|
|
329
|
+
| Method | Effect |
|
|
330
|
+
|--------|--------|
|
|
331
|
+
| `.required()` | Field required in POST/PUT |
|
|
332
|
+
| `.optional()` | Field can be omitted (default) |
|
|
333
|
+
| `.min(n)` | Min string length / min number value |
|
|
334
|
+
| `.max(n)` | Max string length / max number value |
|
|
335
|
+
| `.readonly()` | Excluded from create/update validation |
|
|
336
|
+
| `.default(val)` | Default value |
|
|
337
|
+
| `.fake(hint)` | Explicit faker hint |
|
|
222
338
|
|
|
223
339
|
---
|
|
224
340
|
|
|
225
|
-
##
|
|
341
|
+
## Seed Data (v0.9.0)
|
|
342
|
+
|
|
343
|
+
Guarantee specific records always exist in your entity:
|
|
226
344
|
|
|
227
345
|
```ts
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
346
|
+
User: entity({
|
|
347
|
+
name: string().fullName().required(),
|
|
348
|
+
role: enumField(['admin', 'editor', 'viewer']).required(),
|
|
349
|
+
}, {
|
|
350
|
+
count: 20,
|
|
351
|
+
seed: [
|
|
352
|
+
{ id: 'admin-1', name: 'Admin User', role: 'admin' },
|
|
353
|
+
{ name: 'Guest User', role: 'viewer' }, // id auto-generated if omitted
|
|
354
|
+
],
|
|
355
|
+
})
|
|
235
356
|
```
|
|
236
357
|
|
|
237
|
-
|
|
358
|
+
Seed records appear first. The remaining `count - seed.length` records are generated.
|
|
238
359
|
|
|
239
|
-
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Custom Meta (v0.8.0)
|
|
363
|
+
|
|
364
|
+
Attach extra fields to the list response `meta`. Values can be static or computed from the current filtered dataset (before pagination):
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
Product: entity({ price: number().price(), status: enumField(['active', 'sale']) }, {
|
|
368
|
+
count: 50,
|
|
369
|
+
meta: {
|
|
370
|
+
currency: 'USD', // static
|
|
371
|
+
apiVersion: 2, // static
|
|
372
|
+
total: (items) => items.length, // computed
|
|
373
|
+
avgPrice: (items) => +(items.reduce((s, i) => s + (i.price as number), 0) / items.length).toFixed(2),
|
|
374
|
+
byStatus: (items) => Object.fromEntries(
|
|
375
|
+
['active', 'sale'].map(s => [s, items.filter(i => i.status === s).length])
|
|
376
|
+
),
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Meta functions receive items **after** search/filter but **before** pagination — so aggregates always reflect the current query.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Seed Data Export (v0.15.0)
|
|
386
|
+
|
|
387
|
+
Export the current in-memory DB while the server is running — useful for generating fixture files:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
ffa snapshot # → ffa-snapshot.json
|
|
391
|
+
ffa snapshot -o fixtures.json
|
|
392
|
+
ffa snapshot -p 4000 # specify port if not in config
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Or call directly:
|
|
396
|
+
```
|
|
397
|
+
GET /__snapshot → { "User": [...], "Post": [...] }
|
|
398
|
+
```
|
|
240
399
|
|
|
241
400
|
---
|
|
242
401
|
|
|
243
|
-
##
|
|
402
|
+
## Inspect Config (v0.16.0)
|
|
403
|
+
|
|
404
|
+
Print your config structure without starting the server:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
ffa inspect
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
FFA inspect port 3333
|
|
412
|
+
|
|
413
|
+
User 20 records +1 seed
|
|
414
|
+
* name string [fullName]
|
|
415
|
+
* email string [email]
|
|
416
|
+
* role enum (admin | editor | viewer)
|
|
417
|
+
avatar string [avatar]
|
|
418
|
+
address object { city, country, zip }
|
|
419
|
+
phones array of string
|
|
420
|
+
|
|
421
|
+
Post 50 records
|
|
422
|
+
* title string [sentence]
|
|
423
|
+
* body string [paragraph]
|
|
424
|
+
* status enum (draft | published | archived)
|
|
425
|
+
* price number [price]
|
|
426
|
+
authorId belongsTo → User
|
|
427
|
+
tagIds hasMany → Tag
|
|
428
|
+
meta: currency, total, avgPrice
|
|
429
|
+
|
|
430
|
+
delay 200–600ms · persist ffa-data.json · errorRate 5%
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Config Validation (v0.11.0)
|
|
436
|
+
|
|
437
|
+
On startup, ffa checks your config and prints warnings:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
⚠ Post.authorId (belongsTo) references 'Author' which is not defined
|
|
441
|
+
⚠ Tag.type is enum but has no values
|
|
442
|
+
⚠ User: seed has 5 records but count is 3 — seed will be truncated
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
244
446
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
| `GET` | `/docs` | Swagger UI |
|
|
249
|
-
| `GET` | `/openapi.json`| Raw OpenAPI 3.0 spec |
|
|
447
|
+
## Error Simulation (v0.14.0)
|
|
448
|
+
|
|
449
|
+
Test your frontend error handling without mocking:
|
|
250
450
|
|
|
251
451
|
```ts
|
|
252
|
-
|
|
253
|
-
|
|
452
|
+
server: {
|
|
453
|
+
errorRate: 0.1 // 10% of requests return 500
|
|
454
|
+
}
|
|
254
455
|
```
|
|
255
456
|
|
|
457
|
+
System routes (`/__reset`, `/__snapshot`, `/docs`) are never affected.
|
|
458
|
+
|
|
256
459
|
---
|
|
257
460
|
|
|
258
|
-
##
|
|
461
|
+
## Persistence
|
|
259
462
|
|
|
260
|
-
|
|
463
|
+
```ts
|
|
464
|
+
server: {
|
|
465
|
+
persist: true, // save to ./ffa-data.json
|
|
466
|
+
persist: './data/db.json', // custom path
|
|
467
|
+
}
|
|
468
|
+
```
|
|
261
469
|
|
|
470
|
+
Data is saved on every write (POST/PUT/PATCH/DELETE) and restored on restart.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## JSON Config (no TypeScript)
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"server": { "port": 3333, "delay": 300 },
|
|
479
|
+
"entities": {
|
|
480
|
+
"Product": {
|
|
481
|
+
"count": 20,
|
|
482
|
+
"fields": {
|
|
483
|
+
"title": "string!",
|
|
484
|
+
"price": "number",
|
|
485
|
+
"status": ["draft", "published", "archived"],
|
|
486
|
+
"inStock": "boolean",
|
|
487
|
+
"createdAt": "datetime"
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
262
492
|
```
|
|
263
|
-
FFA dev server v0.6.0
|
|
264
493
|
|
|
265
|
-
|
|
266
|
-
|
|
494
|
+
Save as `ffa.config.json`. `"string!"` = required, `"string"` = optional. Arrays are shorthand for `enumField`.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Terminal Output
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
FFA dev server v0.16.0
|
|
502
|
+
|
|
503
|
+
Entity Count Route
|
|
504
|
+
──────────────────────────────────────────────────────────
|
|
505
|
+
User 20 GET POST http://localhost:3333/users
|
|
506
|
+
Post 50 GET POST http://localhost:3333/posts
|
|
507
|
+
Tag 15 GET POST http://localhost:3333/tags
|
|
508
|
+
|
|
509
|
+
delay 200–600ms
|
|
510
|
+
errorRate 5% chance of 500
|
|
267
511
|
|
|
268
|
-
|
|
269
|
-
|
|
512
|
+
docs http://localhost:3333/docs
|
|
513
|
+
reset POST http://localhost:3333/__reset
|
|
270
514
|
|
|
271
|
-
GET /
|
|
272
|
-
POST /
|
|
273
|
-
GET /
|
|
274
|
-
PATCH /
|
|
515
|
+
GET /users 200 14ms
|
|
516
|
+
POST /posts 201 312ms
|
|
517
|
+
GET /posts/abc-123 404 8ms
|
|
518
|
+
PATCH /posts/def-456 200 267ms
|
|
275
519
|
```
|
|
276
520
|
|
|
277
521
|
---
|