@koltakov/ffa-core 0.6.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 ADDED
@@ -0,0 +1,281 @@
1
+ # ffa-core
2
+
3
+ **F**rontend **F**irst **A**PI — instant mock REST API for frontend development.
4
+
5
+ Stop waiting for the backend. Describe your data, run one command, get a working API.
6
+
7
+ ---
8
+
9
+ ## Why ffa?
10
+
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
+ - **Zero boilerplate** — one config file, one command
14
+ - **Auto CRUD** — all 6 routes per entity out of the box
15
+ - **Smart fake data** — field names drive generation (`email`, `price`, `avatar`, ...)
16
+ - **Zod validation** — request bodies validated against your schema
17
+ - **Swagger UI** — auto-generated docs at `/docs`
18
+ - **Delay simulation** — test loading states with artificial latency
19
+ - **TypeScript or JSON** — pick your config format
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # 1. Install
27
+ npm install @koltakov/ffa-core
28
+
29
+ # 2. Scaffold a config
30
+ npx ffa init
31
+
32
+ # 3. Start
33
+ npx ffa dev
34
+ ```
35
+
36
+ Or skip `init` and write `ffa.config.ts` manually (see below).
37
+
38
+ ---
39
+
40
+ ## Config: TypeScript
41
+
42
+ ```ts
43
+ import { defineConfig, entity, string, number, boolean, enumField, belongsTo } from '@koltakov/ffa-core'
44
+
45
+ export default defineConfig({
46
+ server: {
47
+ port: 3333,
48
+ delay: [200, 600], // artificial latency (ms or [min, max])
49
+ persist: true, // save data to ffa-data.json between restarts
50
+ },
51
+
52
+ entities: {
53
+ User: entity({
54
+ name: string().required(),
55
+ email: string().required(),
56
+ role: enumField(['admin', 'editor', 'viewer']).required(),
57
+ avatar: string().optional(),
58
+ }, { count: 20 }),
59
+
60
+ Post: entity({
61
+ title: string().required(),
62
+ body: string().required(),
63
+ status: enumField(['draft', 'published', 'archived']).required(),
64
+ price: number().min(0).max(9999).required(),
65
+ authorId: belongsTo('User'),
66
+ }),
67
+ },
68
+ })
69
+ ```
70
+
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
+ ---
94
+
95
+ ## CLI
96
+
97
+ ```bash
98
+ ffa dev # start server
99
+ ffa dev --port 4000 # override port
100
+ ffa dev --watch # restart on ffa.config.ts changes
101
+ ffa dev --open # open Swagger UI in browser on start
102
+ ffa dev -p 4000 -w -o # all flags combined
103
+
104
+ ffa init # create ffa.config.ts in current directory
105
+ ffa reset # delete ffa-data.json (clears persisted data)
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Generated Endpoints
111
+
112
+ For every entity `Foo` ffa creates six routes:
113
+
114
+ | Method | Path | Description | Status |
115
+ |-----------|--------------|--------------------------------------|--------------|
116
+ | `GET` | `/foos` | List all records | 200 |
117
+ | `GET` | `/foos/:id` | Get single record | 200, 404 |
118
+ | `POST` | `/foos` | Create a record | 201, 422 |
119
+ | `PUT` | `/foos/:id` | Full replace (all fields required) | 200, 404, 422|
120
+ | `PATCH` | `/foos/:id` | Partial update (all fields optional) | 200, 404, 422|
121
+ | `DELETE` | `/foos/:id` | Delete a record | 200, 404 |
122
+
123
+ > Names are auto-pluralized: `Product` → `/products`, `Category` → `/categories`.
124
+
125
+ ---
126
+
127
+ ## Query Params on GET /list
128
+
129
+ | Param | Example | Description |
130
+ |---------------|------------------------------|-------------------------------------|
131
+ | `page` | `?page=2` | Page number (default: 1) |
132
+ | `limit` | `?limit=10` | Records per page |
133
+ | `sort` | `?sort=price` | Sort by field |
134
+ | `order` | `?order=desc` | `asc` (default) or `desc` |
135
+ | `search` | `?search=apple` | Full-text search across string fields|
136
+ | `{field}` | `?status=published` | Exact field filter |
137
+
138
+ All params are combinable:
139
+ ```
140
+ GET /posts?search=react&status=published&sort=price&order=asc&page=1&limit=20
141
+ ```
142
+
143
+ **Response envelope:**
144
+ ```json
145
+ {
146
+ "data": [ { "id": "...", "title": "...", ... } ],
147
+ "meta": { "total": 47, "page": 1, "limit": 20, "pages": 3 }
148
+ }
149
+ ```
150
+
151
+ Also sets `X-Total-Count: 47` header.
152
+
153
+ ---
154
+
155
+ ## Relations
156
+
157
+ ```ts
158
+ import { belongsTo, hasMany } from '@koltakov/ffa-core'
159
+
160
+ entities: {
161
+ User: entity({ name: string().required() }),
162
+
163
+ Post: entity({
164
+ title: string().required(),
165
+ authorId: belongsTo('User'), // stores a random User id
166
+ tagIds: hasMany('Tag'), // stores 1–3 random Tag ids
167
+ }),
168
+ }
169
+ ```
170
+
171
+ **Inline join** on GET by id:
172
+ ```
173
+ GET /posts/abc?include=authorId,tagIds
174
+ ```
175
+ Returns the related objects inline instead of just ids.
176
+
177
+ ---
178
+
179
+ ## Field Types
180
+
181
+ | Factory | Faker output | Notes |
182
+ |-----------------------|----------------------------------|--------------------------------|
183
+ | `string()` | Smart by field name or word | See smart faker below |
184
+ | `number()` | Integer 0–1000 | Respects `.min()` / `.max()` |
185
+ | `boolean()` | `true` / `false` | |
186
+ | `uuid()` | UUID v4 | |
187
+ | `datetime()` | ISO 8601 string | |
188
+ | `enumField([...])` | Random value from array | Also validates on write |
189
+ | `belongsTo('Entity')` | Random id from that entity | |
190
+ | `hasMany('Entity')` | Array of 1–3 ids from that entity| |
191
+
192
+ ### Smart Faker by Field Name
193
+
194
+ Field names drive faker output automatically:
195
+
196
+ | Field name pattern | Generated value |
197
+ |-----------------------------|-------------------------------|
198
+ | `email`, `mail` | `faker.internet.email()` |
199
+ | `name`, `firstName` | `faker.person.firstName()` |
200
+ | `lastName`, `surname` | `faker.person.lastName()` |
201
+ | `phone`, `tel`, `mobile` | `faker.phone.number()` |
202
+ | `city`, `country`, `address`| Location values |
203
+ | `url`, `website`, `link` | `faker.internet.url()` |
204
+ | `avatar`, `photo`, `image` | `faker.image.avatar()` |
205
+ | `company` | `faker.company.name()` |
206
+ | `title`, `heading` | Short sentence |
207
+ | `description`, `bio`, `text`| Paragraph |
208
+ | `price`, `cost`, `amount` | `faker.commerce.price()` |
209
+ | `color` | `faker.color.human()` |
210
+
211
+ ---
212
+
213
+ ## Field Rules
214
+
215
+ | Rule | Applies to | Effect |
216
+ |---------------|--------------------|-------------------------------------------------|
217
+ | `.required()` | all | Field required in POST/PUT requests |
218
+ | `.optional()` | all | Field can be omitted |
219
+ | `.min(n)` | `string`, `number` | Min length / min value |
220
+ | `.max(n)` | `string`, `number` | Max length / max value |
221
+ | `.readonly()` | all | Excluded from create/update validation |
222
+
223
+ ---
224
+
225
+ ## Server Config
226
+
227
+ ```ts
228
+ server: {
229
+ port: 3333, // default: 3000
230
+ delay: 400, // fixed delay in ms
231
+ delay: [200, 800], // random delay between min and max ms
232
+ persist: true, // save to ./ffa-data.json
233
+ persist: './data.json', // custom file path
234
+ }
235
+ ```
236
+
237
+ If `delay` is set, system routes (`/__reset`, `/docs`) are not delayed.
238
+
239
+ If the configured port is busy, ffa automatically tries the next one.
240
+
241
+ ---
242
+
243
+ ## System Endpoints
244
+
245
+ | Method | Path | Description |
246
+ |--------|----------------|------------------------------------|
247
+ | `POST` | `/__reset` | Regenerate all seed data in memory |
248
+ | `GET` | `/docs` | Swagger UI |
249
+ | `GET` | `/openapi.json`| Raw OpenAPI 3.0 spec |
250
+
251
+ ```ts
252
+ // Reset data from frontend code (e.g. in test setup)
253
+ await fetch('http://localhost:3333/__reset', { method: 'POST' })
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Terminal Output
259
+
260
+ Every request is logged with method, URL, status code and response time:
261
+
262
+ ```
263
+ FFA dev server v0.6.0
264
+
265
+ → Product 20 records http://localhost:3333/products
266
+ → User 10 records http://localhost:3333/users
267
+
268
+ delay: 200–600ms
269
+ → Swagger UI http://localhost:3333/docs
270
+
271
+ GET /products 200 312ms
272
+ POST /products 201 287ms
273
+ GET /products/abc-123 404 201ms
274
+ PATCH /products/def-456 200 344ms
275
+ ```
276
+
277
+ ---
278
+
279
+ ## License
280
+
281
+ ISC
package/dist/cli.js ADDED
@@ -0,0 +1,770 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2, unlinkSync } from "fs";
6
+ import { resolve } from "path";
7
+ import { exec } from "child_process";
8
+
9
+ // src/server.ts
10
+ import Fastify from "fastify";
11
+ import cors from "@fastify/cors";
12
+ import swagger from "@fastify/swagger";
13
+ import swaggerUi from "@fastify/swagger-ui";
14
+ import path2 from "path";
15
+ import { createServer } from "net";
16
+
17
+ // src/config.ts
18
+ import { existsSync, readFileSync } from "fs";
19
+ import path from "path";
20
+ import { pathToFileURL } from "url";
21
+ function parseJsonConfig(raw) {
22
+ const entities = {};
23
+ for (const [entityName, entityRaw] of Object.entries(raw.entities ?? {})) {
24
+ const entityDef = entityRaw;
25
+ const fields = {};
26
+ for (const [fieldName, fieldRaw] of Object.entries(entityDef.fields ?? {})) {
27
+ if (Array.isArray(fieldRaw)) {
28
+ fields[fieldName] = { type: "enum", rules: { enumValues: fieldRaw } };
29
+ } else if (typeof fieldRaw === "string") {
30
+ const required = fieldRaw.endsWith("!");
31
+ const typeName = required ? fieldRaw.slice(0, -1) : fieldRaw;
32
+ fields[fieldName] = { type: typeName, rules: required ? { required: true } : {} };
33
+ } else if (typeof fieldRaw === "object" && fieldRaw !== null) {
34
+ const { type, required, min, max, readonly: ro, entity, enumValues } = fieldRaw;
35
+ fields[fieldName] = {
36
+ type,
37
+ rules: { required, min, max, readonly: ro, entity, enumValues }
38
+ };
39
+ }
40
+ }
41
+ entities[entityName] = { fields, count: entityDef.count };
42
+ }
43
+ return {
44
+ server: raw.server ?? { port: 3e3 },
45
+ entities
46
+ };
47
+ }
48
+ async function loadConfig() {
49
+ const tsPath = path.resolve(process.cwd(), "ffa.config.ts");
50
+ const jsonPath = path.resolve(process.cwd(), "ffa.config.json");
51
+ if (existsSync(tsPath)) {
52
+ const configUrl = pathToFileURL(tsPath).href;
53
+ const config = await import(`${configUrl}?t=${Date.now()}`);
54
+ return config.default;
55
+ }
56
+ if (existsSync(jsonPath)) {
57
+ const raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
58
+ return parseJsonConfig(raw);
59
+ }
60
+ throw new Error("No config found. Create ffa.config.ts or ffa.config.json, or run: ffa init");
61
+ }
62
+
63
+ // src/memory-db.ts
64
+ import { faker } from "@faker-js/faker";
65
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
66
+ var FIELD_NAME_MAP = {
67
+ email: () => faker.internet.email(),
68
+ mail: () => faker.internet.email(),
69
+ name: () => faker.person.firstName(),
70
+ firstname: () => faker.person.firstName(),
71
+ first_name: () => faker.person.firstName(),
72
+ lastname: () => faker.person.lastName(),
73
+ last_name: () => faker.person.lastName(),
74
+ surname: () => faker.person.lastName(),
75
+ fullname: () => faker.person.fullName(),
76
+ username: () => faker.person.fullName(),
77
+ phone: () => faker.phone.number(),
78
+ tel: () => faker.phone.number(),
79
+ mobile: () => faker.phone.number(),
80
+ city: () => faker.location.city(),
81
+ country: () => faker.location.country(),
82
+ address: () => faker.location.streetAddress(),
83
+ zip: () => faker.location.zipCode(),
84
+ postal: () => faker.location.zipCode(),
85
+ url: () => faker.internet.url(),
86
+ website: () => faker.internet.url(),
87
+ link: () => faker.internet.url(),
88
+ avatar: () => faker.image.avatar(),
89
+ photo: () => faker.image.avatar(),
90
+ image: () => faker.image.avatar(),
91
+ company: () => faker.company.name(),
92
+ title: () => faker.lorem.sentence(3),
93
+ heading: () => faker.lorem.sentence(3),
94
+ description: () => faker.lorem.paragraph(),
95
+ bio: () => faker.lorem.paragraph(),
96
+ text: () => faker.lorem.paragraph(),
97
+ color: () => faker.color.human(),
98
+ price: () => faker.commerce.price(),
99
+ cost: () => faker.commerce.price(),
100
+ amount: () => faker.commerce.price()
101
+ };
102
+ function generateFakeValue(def, fieldName, db) {
103
+ if (def.type === "enum") {
104
+ if (def.rules.enumValues?.length) return faker.helpers.arrayElement(def.rules.enumValues);
105
+ return faker.lorem.word();
106
+ }
107
+ if (def.type === "belongsTo") {
108
+ const target = def.rules.entity;
109
+ if (target && db[target]?.length) {
110
+ const items = db[target];
111
+ return items[Math.floor(Math.random() * items.length)].id;
112
+ }
113
+ return faker.string.uuid();
114
+ }
115
+ if (def.type === "hasMany") {
116
+ const target = def.rules.entity;
117
+ if (target && db[target]?.length) {
118
+ const items = db[target];
119
+ const count = faker.number.int({ min: 1, max: Math.min(3, items.length) });
120
+ return [...items].sort(() => Math.random() - 0.5).slice(0, count).map((i) => i.id);
121
+ }
122
+ return [];
123
+ }
124
+ const normalized = fieldName.toLowerCase().replace(/[-\s]/g, "_");
125
+ const mapFn = FIELD_NAME_MAP[normalized];
126
+ if (mapFn) return mapFn();
127
+ for (const key of Object.keys(FIELD_NAME_MAP)) {
128
+ if (normalized.includes(key)) return FIELD_NAME_MAP[key]();
129
+ }
130
+ const { type, rules } = def;
131
+ switch (type) {
132
+ case "string":
133
+ return faker.lorem.word();
134
+ case "number": {
135
+ const min = rules.min ?? 0;
136
+ const max = rules.max ?? 1e3;
137
+ return faker.number.int({ min, max });
138
+ }
139
+ case "boolean":
140
+ return faker.datatype.boolean();
141
+ case "uuid":
142
+ return faker.string.uuid();
143
+ case "datetime":
144
+ return faker.date.recent().toISOString();
145
+ default:
146
+ return faker.lorem.word();
147
+ }
148
+ }
149
+ function generateRecord(fields, db) {
150
+ return {
151
+ id: faker.string.uuid(),
152
+ ...Object.fromEntries(
153
+ Object.entries(fields).map(([key, def]) => [key, generateFakeValue(def, key, db)])
154
+ )
155
+ };
156
+ }
157
+ function createMemoryDB(entities, persistPath) {
158
+ const db = {};
159
+ if (persistPath && existsSync2(persistPath)) {
160
+ try {
161
+ const raw = readFileSync2(persistPath, "utf-8");
162
+ const saved = JSON.parse(raw);
163
+ for (const name in entities) {
164
+ db[name] = saved[name] ?? [];
165
+ }
166
+ } catch {
167
+ }
168
+ }
169
+ function seedAll() {
170
+ const names = Object.keys(entities);
171
+ const sorted = [
172
+ ...names.filter((n) => !Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany")),
173
+ ...names.filter((n) => Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany"))
174
+ ];
175
+ for (const name of sorted) {
176
+ const { fields, count = 10 } = entities[name];
177
+ db[name] = Array.from({ length: count }).map(() => generateRecord(fields, db));
178
+ }
179
+ }
180
+ const needsSeed = Object.keys(entities).filter((n) => !db[n]);
181
+ if (needsSeed.length > 0) {
182
+ const names = Object.keys(entities);
183
+ const sorted = [
184
+ ...names.filter((n) => !Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany")),
185
+ ...names.filter((n) => Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany"))
186
+ ];
187
+ for (const name of sorted) {
188
+ if (!db[name]) {
189
+ const { fields, count = 10 } = entities[name];
190
+ db[name] = Array.from({ length: count }).map(() => generateRecord(fields, db));
191
+ }
192
+ }
193
+ }
194
+ function persist() {
195
+ if (persistPath) {
196
+ writeFileSync(persistPath, JSON.stringify(db, null, 2), "utf-8");
197
+ }
198
+ }
199
+ return {
200
+ list(entity, options = {}) {
201
+ let items = [...db[entity] ?? []];
202
+ if (options.search) {
203
+ const q = options.search.toLowerCase();
204
+ items = items.filter(
205
+ (item) => Object.values(item).some((v) => typeof v === "string" && v.toLowerCase().includes(q))
206
+ );
207
+ }
208
+ if (options.filter) {
209
+ for (const [key, value] of Object.entries(options.filter)) {
210
+ items = items.filter((item) => String(item[key]) === value);
211
+ }
212
+ }
213
+ if (options.sort) {
214
+ const sortKey = options.sort;
215
+ const order = options.order ?? "asc";
216
+ items.sort((a, b) => {
217
+ const aVal = a[sortKey];
218
+ const bVal = b[sortKey];
219
+ if (aVal == null) return 1;
220
+ if (bVal == null) return -1;
221
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
222
+ return order === "desc" ? -cmp : cmp;
223
+ });
224
+ }
225
+ const total = items.length;
226
+ const page = options.page ?? 1;
227
+ const limit = options.limit ?? total;
228
+ const pages = limit > 0 ? Math.ceil(total / limit) : 1;
229
+ const offset = (page - 1) * limit;
230
+ const data = options.limit ? items.slice(offset, offset + limit) : items;
231
+ return { data, meta: { total, page, limit, pages } };
232
+ },
233
+ get(entity, id) {
234
+ return db[entity]?.find((item) => item.id === id);
235
+ },
236
+ create(entity, data) {
237
+ const item = { id: faker.string.uuid(), ...data };
238
+ db[entity].push(item);
239
+ persist();
240
+ return item;
241
+ },
242
+ update(entity, id, data) {
243
+ const idx = db[entity].findIndex((i) => i.id === id);
244
+ if (idx === -1) return null;
245
+ db[entity][idx] = { ...db[entity][idx], ...data };
246
+ persist();
247
+ return db[entity][idx];
248
+ },
249
+ remove(entity, id) {
250
+ const idx = db[entity].findIndex((i) => i.id === id);
251
+ if (idx === -1) return false;
252
+ db[entity].splice(idx, 1);
253
+ persist();
254
+ return true;
255
+ },
256
+ reset() {
257
+ for (const name in entities) db[name] = [];
258
+ seedAll();
259
+ persist();
260
+ },
261
+ count(entity) {
262
+ return db[entity]?.length ?? 0;
263
+ }
264
+ };
265
+ }
266
+
267
+ // src/field/toZodSchema.ts
268
+ import { z } from "zod";
269
+ function fieldToZod(def) {
270
+ let schema;
271
+ switch (def.type) {
272
+ case "string": {
273
+ let s = z.string();
274
+ if (def.rules.min !== void 0) s = s.min(def.rules.min);
275
+ if (def.rules.max !== void 0) s = s.max(def.rules.max);
276
+ schema = s;
277
+ break;
278
+ }
279
+ case "number": {
280
+ let n = z.number();
281
+ if (def.rules.min !== void 0) n = n.gte(def.rules.min);
282
+ if (def.rules.max !== void 0) n = n.lte(def.rules.max);
283
+ schema = n;
284
+ break;
285
+ }
286
+ case "boolean": {
287
+ schema = z.boolean();
288
+ break;
289
+ }
290
+ case "uuid": {
291
+ schema = z.string().uuid();
292
+ break;
293
+ }
294
+ case "datetime": {
295
+ schema = z.string().datetime();
296
+ break;
297
+ }
298
+ case "enum": {
299
+ const values = def.rules.enumValues ?? [];
300
+ schema = values.length >= 1 ? z.enum(values) : z.string();
301
+ break;
302
+ }
303
+ case "belongsTo": {
304
+ schema = z.string();
305
+ break;
306
+ }
307
+ case "hasMany": {
308
+ schema = z.array(z.string());
309
+ break;
310
+ }
311
+ default: {
312
+ schema = z.unknown();
313
+ }
314
+ }
315
+ if (def.rules.required === false) {
316
+ schema = schema.optional();
317
+ }
318
+ return schema;
319
+ }
320
+ function entityToZod(fields) {
321
+ const shape = {};
322
+ for (const [key, def] of Object.entries(fields)) {
323
+ if (def.rules.readonly) continue;
324
+ shape[key] = fieldToZod(def);
325
+ }
326
+ return z.object(shape);
327
+ }
328
+
329
+ // src/openapi.ts
330
+ function fieldToJsonSchema(def) {
331
+ switch (def.type) {
332
+ case "string": {
333
+ const prop = { type: "string" };
334
+ if (def.rules.min !== void 0) prop.minLength = def.rules.min;
335
+ if (def.rules.max !== void 0) prop.maxLength = def.rules.max;
336
+ return prop;
337
+ }
338
+ case "number": {
339
+ const prop = { type: "number" };
340
+ if (def.rules.min !== void 0) prop.minimum = def.rules.min;
341
+ if (def.rules.max !== void 0) prop.maximum = def.rules.max;
342
+ return prop;
343
+ }
344
+ case "boolean":
345
+ return { type: "boolean" };
346
+ case "uuid":
347
+ return { type: "string", format: "uuid" };
348
+ case "datetime":
349
+ return { type: "string", format: "date-time" };
350
+ case "enum":
351
+ return { type: "string", enum: def.rules.enumValues ?? [] };
352
+ case "belongsTo":
353
+ return { type: "string", format: "uuid" };
354
+ case "hasMany":
355
+ return { type: "array", items: { type: "string", format: "uuid" } };
356
+ default:
357
+ return { type: "string" };
358
+ }
359
+ }
360
+ function fieldsToJsonSchema(fields, includeReadonly = true) {
361
+ const properties = {
362
+ id: { type: "string", format: "uuid" }
363
+ };
364
+ const required = ["id"];
365
+ for (const [key, def] of Object.entries(fields)) {
366
+ if (!includeReadonly && def.rules.readonly) continue;
367
+ properties[key] = fieldToJsonSchema(def);
368
+ if (def.rules.required) required.push(key);
369
+ }
370
+ return { type: "object", properties, required };
371
+ }
372
+ function fieldsToInputSchema(fields) {
373
+ const properties = {};
374
+ const required = [];
375
+ for (const [key, def] of Object.entries(fields)) {
376
+ if (def.rules.readonly) continue;
377
+ properties[key] = fieldToJsonSchema(def);
378
+ if (def.rules.required) required.push(key);
379
+ }
380
+ return { type: "object", properties, required: required.length ? required : void 0 };
381
+ }
382
+ function pluralize(word) {
383
+ const lower = word.toLowerCase();
384
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) {
385
+ return lower.slice(0, -1) + "ies";
386
+ }
387
+ if (/(s|x|z|ch|sh)$/i.test(lower)) {
388
+ return lower + "es";
389
+ }
390
+ return lower + "s";
391
+ }
392
+ function generateOpenApiSpec(config) {
393
+ const schemas = {};
394
+ const paths = {};
395
+ for (const [entityName, entityDef] of Object.entries(config.entities)) {
396
+ const pluralName = pluralize(entityName);
397
+ const basePath = `/${pluralName}`;
398
+ schemas[entityName] = fieldsToJsonSchema(entityDef.fields);
399
+ schemas[`${entityName}Input`] = fieldsToInputSchema(entityDef.fields);
400
+ const entityRef = { $ref: `#/components/schemas/${entityName}` };
401
+ const inputRef = { $ref: `#/components/schemas/${entityName}Input` };
402
+ paths[basePath] = {
403
+ get: {
404
+ summary: `List all ${pluralName}`,
405
+ tags: [entityName],
406
+ responses: {
407
+ "200": {
408
+ description: "Success",
409
+ content: {
410
+ "application/json": {
411
+ schema: { type: "array", items: entityRef }
412
+ }
413
+ }
414
+ }
415
+ }
416
+ },
417
+ post: {
418
+ summary: `Create a ${entityName}`,
419
+ tags: [entityName],
420
+ requestBody: {
421
+ required: true,
422
+ content: {
423
+ "application/json": { schema: inputRef }
424
+ }
425
+ },
426
+ responses: {
427
+ "201": {
428
+ description: "Created",
429
+ content: {
430
+ "application/json": { schema: entityRef }
431
+ }
432
+ },
433
+ "422": { description: "Validation error" }
434
+ }
435
+ }
436
+ };
437
+ paths[`${basePath}/{id}`] = {
438
+ get: {
439
+ summary: `Get ${entityName} by ID`,
440
+ tags: [entityName],
441
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
442
+ responses: {
443
+ "200": {
444
+ description: "Success",
445
+ content: { "application/json": { schema: entityRef } }
446
+ },
447
+ "404": { description: "Not found" }
448
+ }
449
+ },
450
+ put: {
451
+ summary: `Replace ${entityName}`,
452
+ tags: [entityName],
453
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
454
+ requestBody: {
455
+ required: true,
456
+ content: { "application/json": { schema: inputRef } }
457
+ },
458
+ responses: {
459
+ "200": {
460
+ description: "Updated",
461
+ content: { "application/json": { schema: entityRef } }
462
+ },
463
+ "404": { description: "Not found" },
464
+ "422": { description: "Validation error" }
465
+ }
466
+ },
467
+ patch: {
468
+ summary: `Partially update ${entityName}`,
469
+ tags: [entityName],
470
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
471
+ requestBody: {
472
+ required: true,
473
+ content: { "application/json": { schema: inputRef } }
474
+ },
475
+ responses: {
476
+ "200": {
477
+ description: "Updated",
478
+ content: { "application/json": { schema: entityRef } }
479
+ },
480
+ "404": { description: "Not found" },
481
+ "422": { description: "Validation error" }
482
+ }
483
+ },
484
+ delete: {
485
+ summary: `Delete ${entityName}`,
486
+ tags: [entityName],
487
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
488
+ responses: {
489
+ "200": { description: "Deleted" },
490
+ "404": { description: "Not found" }
491
+ }
492
+ }
493
+ };
494
+ }
495
+ return {
496
+ openapi: "3.0.0",
497
+ info: {
498
+ title: "FFA Mock API",
499
+ version: "1.0.0",
500
+ description: "Auto-generated mock REST API by ffa-core"
501
+ },
502
+ paths,
503
+ components: { schemas }
504
+ };
505
+ }
506
+
507
+ // src/server.ts
508
+ function pluralize2(word) {
509
+ const lower = word.toLowerCase();
510
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
511
+ if (/(s|x|z|ch|sh)$/i.test(lower)) return lower + "es";
512
+ return lower + "s";
513
+ }
514
+ function isPortFree(port) {
515
+ return new Promise((resolve2) => {
516
+ const srv = createServer();
517
+ srv.once("error", () => resolve2(false));
518
+ srv.once("listening", () => {
519
+ srv.close();
520
+ resolve2(true);
521
+ });
522
+ srv.listen(port, "0.0.0.0");
523
+ });
524
+ }
525
+ async function findPort(start) {
526
+ let port = start;
527
+ while (!await isPortFree(port)) {
528
+ console.log(` Port ${port} is busy, trying ${port + 1}...`);
529
+ port++;
530
+ }
531
+ return port;
532
+ }
533
+ var c = {
534
+ reset: "\x1B[0m",
535
+ bold: "\x1B[1m",
536
+ dim: "\x1B[2m",
537
+ cyan: "\x1B[36m",
538
+ green: "\x1B[32m",
539
+ yellow: "\x1B[33m",
540
+ red: "\x1B[31m",
541
+ blue: "\x1B[34m",
542
+ gray: "\x1B[90m"
543
+ };
544
+ function colorMethod(method) {
545
+ const m = method.toUpperCase().padEnd(6);
546
+ switch (method.toUpperCase()) {
547
+ case "GET":
548
+ return `${c.cyan}${m}${c.reset}`;
549
+ case "POST":
550
+ return `${c.green}${m}${c.reset}`;
551
+ case "PUT":
552
+ return `${c.yellow}${m}${c.reset}`;
553
+ case "PATCH":
554
+ return `${c.yellow}${m}${c.reset}`;
555
+ case "DELETE":
556
+ return `${c.red}${m}${c.reset}`;
557
+ default:
558
+ return m;
559
+ }
560
+ }
561
+ function colorStatus(code) {
562
+ const s = String(code);
563
+ if (code < 300) return `${c.green}${s}${c.reset}`;
564
+ if (code < 400) return `${c.cyan}${s}${c.reset}`;
565
+ if (code < 500) return `${c.yellow}${s}${c.reset}`;
566
+ return `${c.red}${s}${c.reset}`;
567
+ }
568
+ var currentApp = null;
569
+ async function startServer(portOverride) {
570
+ const config = await loadConfig();
571
+ const port = await findPort(portOverride ?? config.server.port);
572
+ const openApiSpec = generateOpenApiSpec(config);
573
+ const app = Fastify({ logger: false });
574
+ currentApp = app;
575
+ await app.register(cors, { origin: "*" });
576
+ await app.register(swagger, { mode: "static", specification: { document: openApiSpec } });
577
+ await app.register(swaggerUi, { routePrefix: "/docs" });
578
+ app.addHook("onRequest", (req, _reply, done) => {
579
+ req._ffaStart = Date.now();
580
+ done();
581
+ });
582
+ const delayConfig = config.server.delay;
583
+ if (delayConfig !== void 0) {
584
+ const SYSTEM_ROUTES = /* @__PURE__ */ new Set(["/__reset", "/docs", "/openapi.json"]);
585
+ app.addHook("preHandler", async (req) => {
586
+ if (SYSTEM_ROUTES.has(req.url) || req.url.startsWith("/docs")) return;
587
+ const ms = Array.isArray(delayConfig) ? Math.floor(Math.random() * (delayConfig[1] - delayConfig[0] + 1)) + delayConfig[0] : delayConfig;
588
+ await new Promise((r) => setTimeout(r, ms));
589
+ });
590
+ }
591
+ app.addHook("onResponse", (req, reply, done) => {
592
+ if (req.url.startsWith("/docs/static") || req.url === "/favicon.ico") {
593
+ done();
594
+ return;
595
+ }
596
+ const ms = Date.now() - (req._ffaStart ?? Date.now());
597
+ const method = colorMethod(req.method);
598
+ const status = colorStatus(reply.statusCode);
599
+ const time = `${c.gray}${ms}ms${c.reset}`;
600
+ console.log(` ${method} ${req.url.padEnd(35)} ${status} ${time}`);
601
+ done();
602
+ });
603
+ let persistPath;
604
+ if (config.server.persist) {
605
+ persistPath = typeof config.server.persist === "string" ? path2.resolve(process.cwd(), config.server.persist) : path2.resolve(process.cwd(), "ffa-data.json");
606
+ }
607
+ const db = createMemoryDB(config.entities, persistPath);
608
+ for (const entityName in config.entities) {
609
+ const base = `/${pluralize2(entityName)}`;
610
+ const zodSchema = entityToZod(config.entities[entityName].fields);
611
+ const entityFields = config.entities[entityName].fields;
612
+ app.get(base, (req, reply) => {
613
+ const query = req.query;
614
+ const page = query.page ? Number(query.page) : void 0;
615
+ const limit = query.limit ? Number(query.limit) : void 0;
616
+ const sort = query.sort;
617
+ const order = query.order === "desc" ? "desc" : "asc";
618
+ const search = query.search;
619
+ const reserved = /* @__PURE__ */ new Set(["page", "limit", "sort", "order", "include", "search"]);
620
+ const filter = {};
621
+ for (const [k, v] of Object.entries(query)) {
622
+ if (!reserved.has(k)) filter[k] = v;
623
+ }
624
+ const result = db.list(entityName, {
625
+ page,
626
+ limit,
627
+ sort,
628
+ order,
629
+ search,
630
+ filter: Object.keys(filter).length ? filter : void 0
631
+ });
632
+ reply.header("X-Total-Count", String(result.meta.total));
633
+ return result;
634
+ });
635
+ app.get(`${base}/:id`, (req, reply) => {
636
+ const item = db.get(entityName, req.params.id);
637
+ if (!item) return reply.code(404).send({ error: "Not found" });
638
+ const include = req.query.include;
639
+ if (!include) return item;
640
+ const result = { ...item };
641
+ for (const fieldName of include.split(",").map((s) => s.trim())) {
642
+ const fieldDef = entityFields[fieldName];
643
+ if (!fieldDef) continue;
644
+ if (fieldDef.type === "belongsTo" && fieldDef.rules.entity) {
645
+ result[fieldName] = db.get(fieldDef.rules.entity, item[fieldName]) ?? null;
646
+ } else if (fieldDef.type === "hasMany" && fieldDef.rules.entity) {
647
+ result[fieldName] = (item[fieldName] ?? []).map((id) => db.get(fieldDef.rules.entity, id)).filter(Boolean);
648
+ }
649
+ }
650
+ return result;
651
+ });
652
+ app.post(base, (req, reply) => {
653
+ const result = zodSchema.safeParse(req.body);
654
+ if (!result.success)
655
+ return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
656
+ return reply.code(201).send(db.create(entityName, result.data));
657
+ });
658
+ app.put(`${base}/:id`, (req, reply) => {
659
+ const result = zodSchema.safeParse(req.body);
660
+ if (!result.success)
661
+ return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
662
+ const item = db.update(entityName, req.params.id, result.data);
663
+ if (!item) return reply.code(404).send({ error: "Not found" });
664
+ return item;
665
+ });
666
+ app.patch(`${base}/:id`, (req, reply) => {
667
+ const result = zodSchema.partial().safeParse(req.body);
668
+ if (!result.success)
669
+ return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
670
+ const item = db.update(entityName, req.params.id, result.data);
671
+ if (!item) return reply.code(404).send({ error: "Not found" });
672
+ return item;
673
+ });
674
+ app.delete(`${base}/:id`, (req, reply) => {
675
+ const ok = db.remove(entityName, req.params.id);
676
+ if (!ok) return reply.code(404).send({ error: "Not found" });
677
+ return { success: true };
678
+ });
679
+ }
680
+ app.post("/__reset", () => {
681
+ db.reset();
682
+ return { success: true, message: "Data reset to seed state" };
683
+ });
684
+ app.get("/openapi.json", () => openApiSpec);
685
+ await app.listen({ port, host: "0.0.0.0" });
686
+ const entityRows = Object.keys(config.entities).map((name) => {
687
+ const count = db.count(name);
688
+ const url = `http://localhost:${port}/${pluralize2(name)}`;
689
+ return ` ${c.gray}\u2192${c.reset} ${name.padEnd(16)} ${c.gray}${String(count).padStart(3)} records${c.reset} ${c.cyan}${url}${c.reset}`;
690
+ }).join("\n");
691
+ const delayInfo = delayConfig !== void 0 ? Array.isArray(delayConfig) ? ` ${c.gray}delay: ${delayConfig[0]}\u2013${delayConfig[1]}ms${c.reset}
692
+ ` : ` ${c.gray}delay: ${delayConfig}ms${c.reset}
693
+ ` : "";
694
+ console.log(
695
+ `
696
+ ${c.bold}FFA dev server${c.reset} ${c.gray}v0.6.0${c.reset}
697
+
698
+ ${entityRows}
699
+
700
+ ${delayInfo} ${c.gray}\u2192${c.reset} Swagger UI ${c.cyan}http://localhost:${port}/docs${c.reset}
701
+ `
702
+ );
703
+ return { app, port };
704
+ }
705
+ async function restartServer(portOverride) {
706
+ if (currentApp) {
707
+ await currentApp.close();
708
+ currentApp = null;
709
+ }
710
+ return startServer(portOverride);
711
+ }
712
+
713
+ // src/cli.ts
714
+ var program = new Command();
715
+ program.command("dev").description("Start FFA dev server").option("-p, --port <port>", "override server port").option("-w, --watch", "watch ffa.config.ts for changes and restart").option("-o, --open", "open Swagger UI in browser on start").action(async (opts) => {
716
+ const portOverride = opts.port ? Number(opts.port) : void 0;
717
+ const { port } = await startServer(portOverride);
718
+ if (opts.open) {
719
+ const url = `http://localhost:${port}/docs`;
720
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
721
+ exec(cmd);
722
+ }
723
+ if (opts.watch) {
724
+ const chokidar = await import("chokidar");
725
+ const configPath = process.cwd() + "/ffa.config.ts";
726
+ const watcher = chokidar.watch(configPath, { ignoreInitial: true });
727
+ watcher.on("change", async () => {
728
+ console.log("\n [watch] ffa.config.ts changed \u2014 restarting...");
729
+ try {
730
+ await restartServer(portOverride);
731
+ } catch (err) {
732
+ console.error(" [watch] Restart failed:", err);
733
+ }
734
+ });
735
+ console.log(" [watch] Watching ffa.config.ts...\n");
736
+ }
737
+ });
738
+ program.command("init").description("Create ffa.config.ts in the current directory").action(() => {
739
+ const tsPath = resolve(process.cwd(), "ffa.config.ts");
740
+ if (existsSync3(tsPath)) {
741
+ console.log(" ffa.config.ts already exists!");
742
+ process.exit(1);
743
+ }
744
+ const template = `import { defineConfig, entity, string, number, boolean, enumField } from 'ffa-core'
745
+
746
+ export default defineConfig({
747
+ server: { port: 3333 },
748
+ entities: {
749
+ Post: entity({
750
+ title: string().required(),
751
+ body: string().required(),
752
+ status: enumField(['draft', 'published', 'archived']).required(),
753
+ published: boolean().required(),
754
+ }),
755
+ },
756
+ })
757
+ `;
758
+ writeFileSync2(tsPath, template, "utf-8");
759
+ console.log(" ffa.config.ts created! Run: ffa dev");
760
+ });
761
+ program.command("reset").description("Delete persisted data file (ffa-data.json)").action(() => {
762
+ const dataPath = resolve(process.cwd(), "ffa-data.json");
763
+ if (!existsSync3(dataPath)) {
764
+ console.log(" No ffa-data.json found. Nothing to reset.");
765
+ return;
766
+ }
767
+ unlinkSync(dataPath);
768
+ console.log(" ffa-data.json deleted. Restart the server to regenerate seed data.");
769
+ });
770
+ program.parse();
@@ -0,0 +1,64 @@
1
+ type FieldType = 'string' | 'number' | 'boolean' | 'uuid' | 'datetime' | 'enum' | 'belongsTo' | 'hasMany';
2
+ interface FieldRules {
3
+ required?: boolean;
4
+ min?: number;
5
+ max?: number;
6
+ default?: any;
7
+ readonly?: boolean;
8
+ entity?: string;
9
+ enumValues?: string[];
10
+ }
11
+ interface FieldDefinition {
12
+ type: FieldType;
13
+ rules: FieldRules;
14
+ }
15
+ interface EntityDefinition {
16
+ fields: Record<string, FieldDefinition>;
17
+ count?: number;
18
+ }
19
+
20
+ interface ServerConfig {
21
+ port: number;
22
+ cors?: boolean | {
23
+ origin: string | string[];
24
+ };
25
+ persist?: boolean | string;
26
+ delay?: number | [number, number];
27
+ }
28
+ interface FfaConfig {
29
+ server: ServerConfig;
30
+ entities: Record<string, EntityDefinition>;
31
+ }
32
+ declare function defineConfig(config: FfaConfig): FfaConfig;
33
+
34
+ declare function entity(fields: Record<string, any>, options?: {
35
+ count?: number;
36
+ }): {
37
+ fields: Record<string, any>;
38
+ count: number | undefined;
39
+ };
40
+
41
+ declare class FieldBuilder {
42
+ private def;
43
+ constructor(type: FieldType, extraRules?: Partial<FieldRules>);
44
+ required(): this;
45
+ optional(): this;
46
+ min(value: number): this;
47
+ max(value: number): this;
48
+ default(value: any): this;
49
+ readonly(): this;
50
+ build(): FieldDefinition;
51
+ }
52
+
53
+ declare const string: () => FieldBuilder;
54
+ declare const number: () => FieldBuilder;
55
+ declare const boolean: () => FieldBuilder;
56
+ declare const uuid: () => FieldBuilder;
57
+ declare const datetime: () => FieldBuilder;
58
+ declare const enumField: (values: [string, ...string[]]) => FieldBuilder;
59
+ declare const belongsTo: (entity: string) => FieldBuilder;
60
+ declare const hasMany: (entity: string) => FieldBuilder;
61
+
62
+ declare const __ffa_version = "0.6.0";
63
+
64
+ export { type FfaConfig, __ffa_version, belongsTo, boolean, datetime, defineConfig, entity, enumField, hasMany, number, string, uuid };
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ // src/config/defineConfig.ts
2
+ function defineConfig(config) {
3
+ return {
4
+ server: config.server ?? { port: 3e3 },
5
+ entities: config.entities ?? {}
6
+ };
7
+ }
8
+
9
+ // src/entity/entity.ts
10
+ function entity(fields, options) {
11
+ const builtFields = {};
12
+ for (const [key, value] of Object.entries(fields)) {
13
+ builtFields[key] = value.build();
14
+ }
15
+ return {
16
+ fields: builtFields,
17
+ count: options?.count
18
+ };
19
+ }
20
+
21
+ // src/field/FieldBuilder.ts
22
+ var FieldBuilder = class {
23
+ def;
24
+ constructor(type, extraRules) {
25
+ this.def = {
26
+ type,
27
+ rules: { ...extraRules }
28
+ };
29
+ }
30
+ required() {
31
+ this.def.rules.required = true;
32
+ return this;
33
+ }
34
+ optional() {
35
+ this.def.rules.required = false;
36
+ return this;
37
+ }
38
+ min(value) {
39
+ this.def.rules.min = value;
40
+ return this;
41
+ }
42
+ max(value) {
43
+ this.def.rules.max = value;
44
+ return this;
45
+ }
46
+ default(value) {
47
+ this.def.rules.default = value;
48
+ return this;
49
+ }
50
+ readonly() {
51
+ this.def.rules.readonly = true;
52
+ return this;
53
+ }
54
+ build() {
55
+ return structuredClone(this.def);
56
+ }
57
+ };
58
+
59
+ // src/field/factories.ts
60
+ var string = () => new FieldBuilder("string");
61
+ var number = () => new FieldBuilder("number");
62
+ var boolean = () => new FieldBuilder("boolean");
63
+ var uuid = () => new FieldBuilder("uuid");
64
+ var datetime = () => new FieldBuilder("datetime");
65
+ var enumField = (values) => new FieldBuilder("enum", { enumValues: values });
66
+ var belongsTo = (entity2) => new FieldBuilder("belongsTo", { entity: entity2 });
67
+ var hasMany = (entity2) => new FieldBuilder("hasMany", { entity: entity2 });
68
+
69
+ // src/index.ts
70
+ var __ffa_version = "0.6.0";
71
+ export {
72
+ __ffa_version,
73
+ belongsTo,
74
+ boolean,
75
+ datetime,
76
+ defineConfig,
77
+ entity,
78
+ enumField,
79
+ hasMany,
80
+ number,
81
+ string,
82
+ uuid
83
+ };
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/defineConfig.ts","../src/entity/entity.ts","../src/field/FieldBuilder.ts","../src/field/factories.ts","../src/index.ts"],"sourcesContent":["import type { EntityDefinition } from '../field/types'\n\nexport interface ServerConfig {\n port: number\n cors?: boolean | { origin: string | string[] }\n persist?: boolean | string\n delay?: number | [number, number]\n}\n\nexport interface FfaConfig {\n server: ServerConfig\n entities: Record<string, EntityDefinition>\n}\n\nexport function defineConfig(config: FfaConfig): FfaConfig {\n return {\n server: config.server ?? { port: 3000 },\n entities: config.entities ?? {},\n }\n}\n","// src/entity/entity.ts\nexport function entity(fields: Record<string, any>, options?: { count?: number }) {\n const builtFields: Record<string, any> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n builtFields[key] = value.build()\n }\n\n return {\n fields: builtFields,\n count: options?.count,\n }\n}\n","import { FieldDefinition, FieldRules, FieldType } from './types'\n\nexport class FieldBuilder {\n private def: FieldDefinition\n\n constructor(type: FieldType, extraRules?: Partial<FieldRules>) {\n this.def = {\n type,\n rules: { ...extraRules },\n }\n }\n\n required() {\n this.def.rules.required = true\n return this\n }\n\n optional() {\n this.def.rules.required = false\n return this\n }\n\n min(value: number) {\n this.def.rules.min = value\n return this\n }\n\n max(value: number) {\n this.def.rules.max = value\n return this\n }\n\n default(value: any) {\n this.def.rules.default = value\n return this\n }\n\n readonly() {\n this.def.rules.readonly = true\n return this\n }\n\n build(): FieldDefinition {\n return structuredClone(this.def)\n }\n}\n","import { FieldBuilder } from './FieldBuilder'\n\nexport const string = () => new FieldBuilder('string')\nexport const number = () => new FieldBuilder('number')\nexport const boolean = () => new FieldBuilder('boolean')\nexport const uuid = () => new FieldBuilder('uuid')\nexport const datetime = () => new FieldBuilder('datetime')\nexport const enumField = (values: [string, ...string[]]) => new FieldBuilder('enum', { enumValues: values })\nexport const belongsTo = (entity: string) => new FieldBuilder('belongsTo', { entity })\nexport const hasMany = (entity: string) => new FieldBuilder('hasMany', { entity })\n","// config\nexport { defineConfig } from './config/defineConfig'\nexport type { FfaConfig } from './config/defineConfig'\n\n// entity\nexport { entity } from './entity/entity'\n\n// field DSL\nexport { string, number, boolean, uuid, datetime, enumField, belongsTo, hasMany } from './field/factories'\n\nexport const __ffa_version = '0.6.0'\n"],"mappings":";AAcO,SAAS,aAAa,QAA8B;AACzD,SAAO;AAAA,IACL,QAAQ,OAAO,UAAU,EAAE,MAAM,IAAK;AAAA,IACtC,UAAU,OAAO,YAAY,CAAC;AAAA,EAChC;AACF;;;AClBO,SAAS,OAAO,QAA6B,SAA8B;AAChF,QAAM,cAAmC,CAAC;AAE1C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,gBAAY,GAAG,IAAI,MAAM,MAAM;AAAA,EACjC;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO,SAAS;AAAA,EAClB;AACF;;;ACVO,IAAM,eAAN,MAAmB;AAAA,EAChB;AAAA,EAER,YAAY,MAAiB,YAAkC;AAC7D,SAAK,MAAM;AAAA,MACT;AAAA,MACA,OAAO,EAAE,GAAG,WAAW;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,WAAW;AACT,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,WAAW;AACT,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAAe;AACjB,SAAK,IAAI,MAAM,MAAM;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAAe;AACjB,SAAK,IAAI,MAAM,MAAM;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,OAAY;AAClB,SAAK,IAAI,MAAM,UAAU;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,WAAW;AACT,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,QAAyB;AACvB,WAAO,gBAAgB,KAAK,GAAG;AAAA,EACjC;AACF;;;AC3CO,IAAM,SAAS,MAAM,IAAI,aAAa,QAAQ;AAC9C,IAAM,SAAS,MAAM,IAAI,aAAa,QAAQ;AAC9C,IAAM,UAAU,MAAM,IAAI,aAAa,SAAS;AAChD,IAAM,OAAO,MAAM,IAAI,aAAa,MAAM;AAC1C,IAAM,WAAW,MAAM,IAAI,aAAa,UAAU;AAClD,IAAM,YAAY,CAAC,WAAkC,IAAI,aAAa,QAAQ,EAAE,YAAY,OAAO,CAAC;AACpG,IAAM,YAAY,CAACA,YAAmB,IAAI,aAAa,aAAa,EAAE,QAAAA,QAAO,CAAC;AAC9E,IAAM,UAAU,CAACA,YAAmB,IAAI,aAAa,WAAW,EAAE,QAAAA,QAAO,CAAC;;;ACC1E,IAAM,gBAAgB;","names":["entity"]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@koltakov/ffa-core",
3
+ "version": "0.6.0",
4
+ "description": "Instant mock REST API for frontend development",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "ffa": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsx src/cli.ts",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run typecheck && npm run build"
26
+ },
27
+ "keywords": [
28
+ "mock",
29
+ "api",
30
+ "fake",
31
+ "rest",
32
+ "faker",
33
+ "frontend",
34
+ "development",
35
+ "cli",
36
+ "scaffold",
37
+ "prototype"
38
+ ],
39
+ "author": "IgorKoltakov",
40
+ "license": "ISC",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/IgorKoltakov/ffa-core.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/IgorKoltakov/ffa-core/issues"
47
+ },
48
+ "homepage": "https://github.com/IgorKoltakov/ffa-core#readme",
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "dependencies": {
53
+ "@faker-js/faker": "^10.3.0",
54
+ "@fastify/cors": "^11.2.0",
55
+ "@fastify/swagger": "^9.7.0",
56
+ "@fastify/swagger-ui": "^5.2.5",
57
+ "chokidar": "^5.0.0",
58
+ "commander": "^14.0.3",
59
+ "fastify": "^5.7.4",
60
+ "zod": "^4.3.6"
61
+ },
62
+ "devDependencies": {
63
+ "@types/chokidar": "^1.7.5",
64
+ "@types/node": "^25.3.3",
65
+ "tsup": "^8.0.0",
66
+ "tsx": "^4.21.0",
67
+ "typescript": "^5.9.3"
68
+ }
69
+ }