@koltakov/ffa-core 0.8.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +377 -299
- package/dist/cli.js +260 -38
- package/dist/index.d.ts +15 -4
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -63,6 +63,40 @@ async function loadConfig() {
|
|
|
63
63
|
// src/memory-db.ts
|
|
64
64
|
import { faker } from "@faker-js/faker";
|
|
65
65
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
66
|
+
var OP_SUFFIXES = ["_gte", "_lte", "_gt", "_lt", "_in", "_contains", "_ne"];
|
|
67
|
+
function parseFilter(filter) {
|
|
68
|
+
return Object.entries(filter).map(([key, value]) => {
|
|
69
|
+
for (const op of OP_SUFFIXES) {
|
|
70
|
+
if (key.endsWith(op)) {
|
|
71
|
+
return { field: key.slice(0, -op.length), op, value };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { field: key, op: null, value };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function applyFilter(item, field, op, value) {
|
|
78
|
+
const raw = item[field];
|
|
79
|
+
if (op === null) return String(raw) === value;
|
|
80
|
+
const num = Number(value);
|
|
81
|
+
switch (op) {
|
|
82
|
+
case "_gte":
|
|
83
|
+
return Number(raw) >= num;
|
|
84
|
+
case "_lte":
|
|
85
|
+
return Number(raw) <= num;
|
|
86
|
+
case "_gt":
|
|
87
|
+
return Number(raw) > num;
|
|
88
|
+
case "_lt":
|
|
89
|
+
return Number(raw) < num;
|
|
90
|
+
case "_ne":
|
|
91
|
+
return String(raw) !== value;
|
|
92
|
+
case "_in":
|
|
93
|
+
return value.split(",").map((v) => v.trim()).includes(String(raw));
|
|
94
|
+
case "_contains":
|
|
95
|
+
return String(raw).toLowerCase().includes(value.toLowerCase());
|
|
96
|
+
default:
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
66
100
|
var HINT_MAP = {
|
|
67
101
|
// internet
|
|
68
102
|
email: () => faker.internet.email(),
|
|
@@ -71,7 +105,7 @@ var HINT_MAP = {
|
|
|
71
105
|
ip: () => faker.internet.ip(),
|
|
72
106
|
username: () => faker.internet.username(),
|
|
73
107
|
// media
|
|
74
|
-
image: () => faker.image.url(),
|
|
108
|
+
image: () => faker.image.url({ width: 1280, height: 720 }),
|
|
75
109
|
avatar: () => faker.image.avatar(),
|
|
76
110
|
// person
|
|
77
111
|
firstName: () => faker.person.firstName(),
|
|
@@ -147,7 +181,15 @@ var FIELD_NAME_MAP = {
|
|
|
147
181
|
amount: HINT_MAP.price
|
|
148
182
|
};
|
|
149
183
|
function generateFakeValue(def, fieldName, db) {
|
|
150
|
-
if (def.rules.fakeHint)
|
|
184
|
+
if (def.rules.fakeHint) {
|
|
185
|
+
if (def.rules.fakeHint === "image") {
|
|
186
|
+
return faker.image.url({
|
|
187
|
+
width: def.rules.imageWidth ?? 1280,
|
|
188
|
+
height: def.rules.imageHeight ?? 720
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return HINT_MAP[def.rules.fakeHint]();
|
|
192
|
+
}
|
|
151
193
|
if (def.type === "belongsTo") {
|
|
152
194
|
const target = def.rules.entity;
|
|
153
195
|
if (target && db[target]?.length) {
|
|
@@ -167,6 +209,21 @@ function generateFakeValue(def, fieldName, db) {
|
|
|
167
209
|
if (def.rules.enumValues?.length) return faker.helpers.arrayElement(def.rules.enumValues);
|
|
168
210
|
return faker.lorem.word();
|
|
169
211
|
}
|
|
212
|
+
if (def.type === "object") {
|
|
213
|
+
if (!def.rules.objectFields) return {};
|
|
214
|
+
return Object.fromEntries(
|
|
215
|
+
Object.entries(def.rules.objectFields).map(([k, fieldDef]) => [
|
|
216
|
+
k,
|
|
217
|
+
generateFakeValue(fieldDef, k, db)
|
|
218
|
+
])
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (def.type === "array") {
|
|
222
|
+
if (!def.rules.arrayItem) return [];
|
|
223
|
+
const [min, max] = def.rules.arrayCount ?? [1, 3];
|
|
224
|
+
const count = faker.number.int({ min, max });
|
|
225
|
+
return Array.from({ length: count }).map(() => generateFakeValue(def.rules.arrayItem, "item", db));
|
|
226
|
+
}
|
|
170
227
|
const normalized = fieldName.toLowerCase().replace(/[-\s]/g, "_");
|
|
171
228
|
if (FIELD_NAME_MAP[normalized]) return FIELD_NAME_MAP[normalized]();
|
|
172
229
|
for (const key of Object.keys(FIELD_NAME_MAP)) {
|
|
@@ -215,17 +272,21 @@ function createMemoryDB(entities, persistPath) {
|
|
|
215
272
|
...names.filter((n) => Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany"))
|
|
216
273
|
];
|
|
217
274
|
}
|
|
275
|
+
function seedEntity(name) {
|
|
276
|
+
const { fields, count = 10, seed } = entities[name];
|
|
277
|
+
const seedItems = (seed ?? []).map((record) => ({
|
|
278
|
+
id: faker.string.uuid(),
|
|
279
|
+
...record
|
|
280
|
+
}));
|
|
281
|
+
const generateCount = Math.max(0, count - seedItems.length);
|
|
282
|
+
const generated = Array.from({ length: generateCount }).map(() => generateRecord(fields, db));
|
|
283
|
+
db[name] = [...seedItems, ...generated];
|
|
284
|
+
}
|
|
218
285
|
function seedAll() {
|
|
219
|
-
for (const name of sortedEntityNames())
|
|
220
|
-
const { fields, count = 10 } = entities[name];
|
|
221
|
-
db[name] = Array.from({ length: count }).map(() => generateRecord(fields, db));
|
|
222
|
-
}
|
|
286
|
+
for (const name of sortedEntityNames()) seedEntity(name);
|
|
223
287
|
}
|
|
224
288
|
for (const name of sortedEntityNames()) {
|
|
225
|
-
if (!db[name])
|
|
226
|
-
const { fields, count = 10 } = entities[name];
|
|
227
|
-
db[name] = Array.from({ length: count }).map(() => generateRecord(fields, db));
|
|
228
|
-
}
|
|
289
|
+
if (!db[name]) seedEntity(name);
|
|
229
290
|
}
|
|
230
291
|
function persist() {
|
|
231
292
|
if (persistPath) writeFileSync(persistPath, JSON.stringify(db, null, 2), "utf-8");
|
|
@@ -240,9 +301,10 @@ function createMemoryDB(entities, persistPath) {
|
|
|
240
301
|
);
|
|
241
302
|
}
|
|
242
303
|
if (options.filter) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
304
|
+
const conditions = parseFilter(options.filter);
|
|
305
|
+
items = items.filter(
|
|
306
|
+
(item) => conditions.every(({ field, op, value }) => applyFilter(item, field, op, value))
|
|
307
|
+
);
|
|
246
308
|
}
|
|
247
309
|
if (options.sort) {
|
|
248
310
|
const sortKey = options.sort;
|
|
@@ -306,6 +368,9 @@ function createMemoryDB(entities, persistPath) {
|
|
|
306
368
|
},
|
|
307
369
|
count(entity) {
|
|
308
370
|
return db[entity]?.length ?? 0;
|
|
371
|
+
},
|
|
372
|
+
snapshot() {
|
|
373
|
+
return Object.fromEntries(Object.entries(db).map(([k, v]) => [k, v.map((item) => ({ ...item }))]));
|
|
309
374
|
}
|
|
310
375
|
};
|
|
311
376
|
}
|
|
@@ -354,6 +419,22 @@ function fieldToZod(def) {
|
|
|
354
419
|
schema = z.array(z.string());
|
|
355
420
|
break;
|
|
356
421
|
}
|
|
422
|
+
case "object": {
|
|
423
|
+
if (!def.rules.objectFields) {
|
|
424
|
+
schema = z.object({});
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
const shape = {};
|
|
428
|
+
for (const [k, fieldDef] of Object.entries(def.rules.objectFields)) {
|
|
429
|
+
shape[k] = fieldToZod(fieldDef);
|
|
430
|
+
}
|
|
431
|
+
schema = z.object(shape);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "array": {
|
|
435
|
+
schema = def.rules.arrayItem ? z.array(fieldToZod(def.rules.arrayItem)) : z.array(z.unknown());
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
357
438
|
default: {
|
|
358
439
|
schema = z.unknown();
|
|
359
440
|
}
|
|
@@ -550,6 +631,39 @@ function generateOpenApiSpec(config) {
|
|
|
550
631
|
};
|
|
551
632
|
}
|
|
552
633
|
|
|
634
|
+
// src/validate-config.ts
|
|
635
|
+
var c = {
|
|
636
|
+
yellow: "\x1B[33m",
|
|
637
|
+
reset: "\x1B[0m",
|
|
638
|
+
bold: "\x1B[1m"
|
|
639
|
+
};
|
|
640
|
+
function validateConfig(entities) {
|
|
641
|
+
const names = new Set(Object.keys(entities));
|
|
642
|
+
const warnings = [];
|
|
643
|
+
for (const [entityName, def] of Object.entries(entities)) {
|
|
644
|
+
for (const [fieldName, field] of Object.entries(def.fields)) {
|
|
645
|
+
if ((field.type === "belongsTo" || field.type === "hasMany") && field.rules.entity) {
|
|
646
|
+
if (!names.has(field.rules.entity)) {
|
|
647
|
+
warnings.push(
|
|
648
|
+
`${entityName}.${fieldName} (${field.type}) references '${field.rules.entity}' which is not defined`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (field.type === "enum" && (!field.rules.enumValues || field.rules.enumValues.length === 0)) {
|
|
653
|
+
warnings.push(`${entityName}.${fieldName} is enum but has no values`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if ((def.count ?? 10) < (def.seed?.length ?? 0)) {
|
|
657
|
+
warnings.push(
|
|
658
|
+
`${entityName}: seed has ${def.seed.length} records but count is ${def.count} \u2014 seed will be truncated`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
for (const w of warnings) {
|
|
663
|
+
console.warn(` ${c.yellow}${c.bold}\u26A0${c.reset} ${w}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
553
667
|
// src/server.ts
|
|
554
668
|
function pluralize2(word) {
|
|
555
669
|
const lower = word.toLowerCase();
|
|
@@ -576,7 +690,7 @@ async function findPort(start) {
|
|
|
576
690
|
}
|
|
577
691
|
return port;
|
|
578
692
|
}
|
|
579
|
-
var
|
|
693
|
+
var c2 = {
|
|
580
694
|
reset: "\x1B[0m",
|
|
581
695
|
bold: "\x1B[1m",
|
|
582
696
|
dim: "\x1B[2m",
|
|
@@ -591,29 +705,30 @@ function colorMethod(method) {
|
|
|
591
705
|
const m = method.toUpperCase().padEnd(6);
|
|
592
706
|
switch (method.toUpperCase()) {
|
|
593
707
|
case "GET":
|
|
594
|
-
return `${
|
|
708
|
+
return `${c2.cyan}${m}${c2.reset}`;
|
|
595
709
|
case "POST":
|
|
596
|
-
return `${
|
|
710
|
+
return `${c2.green}${m}${c2.reset}`;
|
|
597
711
|
case "PUT":
|
|
598
|
-
return `${
|
|
712
|
+
return `${c2.yellow}${m}${c2.reset}`;
|
|
599
713
|
case "PATCH":
|
|
600
|
-
return `${
|
|
714
|
+
return `${c2.yellow}${m}${c2.reset}`;
|
|
601
715
|
case "DELETE":
|
|
602
|
-
return `${
|
|
716
|
+
return `${c2.red}${m}${c2.reset}`;
|
|
603
717
|
default:
|
|
604
718
|
return m;
|
|
605
719
|
}
|
|
606
720
|
}
|
|
607
721
|
function colorStatus(code) {
|
|
608
722
|
const s = String(code);
|
|
609
|
-
if (code < 300) return `${
|
|
610
|
-
if (code < 400) return `${
|
|
611
|
-
if (code < 500) return `${
|
|
612
|
-
return `${
|
|
723
|
+
if (code < 300) return `${c2.green}${s}${c2.reset}`;
|
|
724
|
+
if (code < 400) return `${c2.cyan}${s}${c2.reset}`;
|
|
725
|
+
if (code < 500) return `${c2.yellow}${s}${c2.reset}`;
|
|
726
|
+
return `${c2.red}${s}${c2.reset}`;
|
|
613
727
|
}
|
|
614
728
|
var currentApp = null;
|
|
615
729
|
async function startServer(portOverride) {
|
|
616
730
|
const config = await loadConfig();
|
|
731
|
+
validateConfig(config.entities);
|
|
617
732
|
const port = await findPort(portOverride ?? config.server.port);
|
|
618
733
|
const openApiSpec = generateOpenApiSpec(config);
|
|
619
734
|
const app = Fastify({ logger: false });
|
|
@@ -634,6 +749,16 @@ async function startServer(portOverride) {
|
|
|
634
749
|
await new Promise((r) => setTimeout(r, ms));
|
|
635
750
|
});
|
|
636
751
|
}
|
|
752
|
+
const errorRate = config.server.errorRate;
|
|
753
|
+
if (errorRate !== void 0 && errorRate > 0) {
|
|
754
|
+
const SYSTEM_ROUTES_ERR = /* @__PURE__ */ new Set(["/__reset", "/docs", "/openapi.json", "/__snapshot"]);
|
|
755
|
+
app.addHook("preHandler", async (req, reply) => {
|
|
756
|
+
if (SYSTEM_ROUTES_ERR.has(req.url) || req.url.startsWith("/docs")) return;
|
|
757
|
+
if (Math.random() < errorRate) {
|
|
758
|
+
reply.code(500).send({ error: "Simulated server error", hint: "errorRate is enabled in config" });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
637
762
|
app.addHook("onResponse", (req, reply, done) => {
|
|
638
763
|
if (req.url.startsWith("/docs/static") || req.url === "/favicon.ico") {
|
|
639
764
|
done();
|
|
@@ -642,7 +767,7 @@ async function startServer(portOverride) {
|
|
|
642
767
|
const ms = Date.now() - (req._ffaStart ?? Date.now());
|
|
643
768
|
const method = colorMethod(req.method);
|
|
644
769
|
const status = colorStatus(reply.statusCode);
|
|
645
|
-
const time = `${
|
|
770
|
+
const time = `${c2.gray}${ms}ms${c2.reset}`;
|
|
646
771
|
console.log(` ${method} ${req.url.padEnd(35)} ${status} ${time}`);
|
|
647
772
|
done();
|
|
648
773
|
});
|
|
@@ -652,10 +777,10 @@ async function startServer(portOverride) {
|
|
|
652
777
|
}
|
|
653
778
|
const db = createMemoryDB(config.entities, persistPath);
|
|
654
779
|
for (const entityName in config.entities) {
|
|
655
|
-
const
|
|
780
|
+
const base2 = `/${pluralize2(entityName)}`;
|
|
656
781
|
const zodSchema = entityToZod(config.entities[entityName].fields);
|
|
657
782
|
const entityFields = config.entities[entityName].fields;
|
|
658
|
-
app.get(
|
|
783
|
+
app.get(base2, (req, reply) => {
|
|
659
784
|
const query = req.query;
|
|
660
785
|
const page = query.page ? Number(query.page) : void 0;
|
|
661
786
|
const limit = query.limit ? Number(query.limit) : void 0;
|
|
@@ -678,7 +803,7 @@ async function startServer(portOverride) {
|
|
|
678
803
|
reply.header("X-Total-Count", String(result.meta.total));
|
|
679
804
|
return result;
|
|
680
805
|
});
|
|
681
|
-
app.get(`${
|
|
806
|
+
app.get(`${base2}/:id`, (req, reply) => {
|
|
682
807
|
const item = db.get(entityName, req.params.id);
|
|
683
808
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
684
809
|
const include = req.query.include;
|
|
@@ -695,13 +820,13 @@ async function startServer(portOverride) {
|
|
|
695
820
|
}
|
|
696
821
|
return result;
|
|
697
822
|
});
|
|
698
|
-
app.post(
|
|
823
|
+
app.post(base2, (req, reply) => {
|
|
699
824
|
const result = zodSchema.safeParse(req.body);
|
|
700
825
|
if (!result.success)
|
|
701
826
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
702
827
|
return reply.code(201).send(db.create(entityName, result.data));
|
|
703
828
|
});
|
|
704
|
-
app.put(`${
|
|
829
|
+
app.put(`${base2}/:id`, (req, reply) => {
|
|
705
830
|
const result = zodSchema.safeParse(req.body);
|
|
706
831
|
if (!result.success)
|
|
707
832
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
@@ -709,7 +834,7 @@ async function startServer(portOverride) {
|
|
|
709
834
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
710
835
|
return item;
|
|
711
836
|
});
|
|
712
|
-
app.patch(`${
|
|
837
|
+
app.patch(`${base2}/:id`, (req, reply) => {
|
|
713
838
|
const result = zodSchema.partial().safeParse(req.body);
|
|
714
839
|
if (!result.success)
|
|
715
840
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
@@ -717,7 +842,7 @@ async function startServer(portOverride) {
|
|
|
717
842
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
718
843
|
return item;
|
|
719
844
|
});
|
|
720
|
-
app.delete(`${
|
|
845
|
+
app.delete(`${base2}/:id`, (req, reply) => {
|
|
721
846
|
const ok = db.remove(entityName, req.params.id);
|
|
722
847
|
if (!ok) return reply.code(404).send({ error: "Not found" });
|
|
723
848
|
return { success: true };
|
|
@@ -727,23 +852,36 @@ async function startServer(portOverride) {
|
|
|
727
852
|
db.reset();
|
|
728
853
|
return { success: true, message: "Data reset to seed state" };
|
|
729
854
|
});
|
|
855
|
+
app.get("/__snapshot", () => db.snapshot());
|
|
730
856
|
app.get("/openapi.json", () => openApiSpec);
|
|
731
857
|
await app.listen({ port, host: "0.0.0.0" });
|
|
732
|
-
const
|
|
858
|
+
const VERSION = "0.16.0";
|
|
859
|
+
const base = `http://localhost:${port}`;
|
|
860
|
+
const entityNames = Object.keys(config.entities);
|
|
861
|
+
const colEntity = Math.max(6, ...entityNames.map((n) => n.length));
|
|
862
|
+
const colRoute = Math.max(5, ...entityNames.map((n) => `/${pluralize2(n)}`.length));
|
|
863
|
+
const header = ` ${"Entity".padEnd(colEntity)} ${"Count".padStart(5)} Route`;
|
|
864
|
+
const divider = ` ${"\u2500".repeat(colEntity)} ${"\u2500".repeat(5)} ${"\u2500".repeat(colRoute + 18)}`;
|
|
865
|
+
const entityRows = entityNames.map((name) => {
|
|
733
866
|
const count = db.count(name);
|
|
734
|
-
const
|
|
735
|
-
|
|
867
|
+
const route = `/${pluralize2(name)}`;
|
|
868
|
+
const methods = `${c2.cyan}GET${c2.reset} ${c2.green}POST${c2.reset}`;
|
|
869
|
+
return ` ${c2.bold}${name.padEnd(colEntity)}${c2.reset} ${c2.gray}${String(count).padStart(5)}${c2.reset} ${methods} ${c2.cyan}${base}${route}${c2.reset}`;
|
|
736
870
|
}).join("\n");
|
|
737
|
-
const
|
|
738
|
-
` :
|
|
871
|
+
const delayLine = delayConfig !== void 0 ? ` ${c2.gray}delay ${Array.isArray(delayConfig) ? `${delayConfig[0]}\u2013${delayConfig[1]}ms` : `${delayConfig}ms`}${c2.reset}
|
|
872
|
+
` : "";
|
|
873
|
+
const errorLine = errorRate !== void 0 && errorRate > 0 ? ` ${c2.yellow}errorRate ${(errorRate * 100).toFixed(0)}% chance of 500${c2.reset}
|
|
739
874
|
` : "";
|
|
740
875
|
console.log(
|
|
741
876
|
`
|
|
742
|
-
${
|
|
877
|
+
${c2.bold}${c2.cyan}FFA${c2.reset}${c2.bold} dev server${c2.reset} ${c2.gray}v${VERSION}${c2.reset}
|
|
743
878
|
|
|
879
|
+
${c2.gray}${header}
|
|
880
|
+
${divider}${c2.reset}
|
|
744
881
|
${entityRows}
|
|
745
882
|
|
|
746
|
-
${
|
|
883
|
+
${delayLine}${errorLine} ${c2.gray}docs ${c2.reset}${c2.cyan}${base}/docs${c2.reset}
|
|
884
|
+
${c2.gray}reset ${c2.reset}${c2.cyan}POST ${base}/__reset${c2.reset}
|
|
747
885
|
`
|
|
748
886
|
);
|
|
749
887
|
return { app, port };
|
|
@@ -804,6 +942,30 @@ export default defineConfig({
|
|
|
804
942
|
writeFileSync2(tsPath, template, "utf-8");
|
|
805
943
|
console.log(" ffa.config.ts created! Run: ffa dev");
|
|
806
944
|
});
|
|
945
|
+
program.command("snapshot").description("Export current DB state from running server to a JSON file").option("-o, --output <file>", "output file path", "ffa-snapshot.json").option("-p, --port <port>", "server port (reads from config by default)").action(async (opts) => {
|
|
946
|
+
let port = opts.port ? Number(opts.port) : void 0;
|
|
947
|
+
if (!port) {
|
|
948
|
+
try {
|
|
949
|
+
const cfg = await loadConfig();
|
|
950
|
+
port = cfg.server.port;
|
|
951
|
+
} catch {
|
|
952
|
+
port = 3e3;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
const res = await fetch(`http://localhost:${port}/__snapshot`);
|
|
957
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
958
|
+
const data = await res.json();
|
|
959
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
960
|
+
writeFileSync2(outPath, JSON.stringify(data, null, 2), "utf-8");
|
|
961
|
+
const counts = Object.entries(data).map(([k, v]) => `${k}: ${v.length}`).join(", ");
|
|
962
|
+
console.log(` Snapshot saved \u2192 ${outPath}`);
|
|
963
|
+
console.log(` ${counts}`);
|
|
964
|
+
} catch {
|
|
965
|
+
console.error(` Failed to snapshot. Is the server running on port ${port}?`);
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
807
969
|
program.command("reset").description("Delete persisted data file (ffa-data.json)").action(() => {
|
|
808
970
|
const dataPath = resolve(process.cwd(), "ffa-data.json");
|
|
809
971
|
if (!existsSync3(dataPath)) {
|
|
@@ -815,4 +977,64 @@ program.command("reset").description("Delete persisted data file (ffa-data.json)
|
|
|
815
977
|
" ffa-data.json deleted. Restart the server to regenerate seed data."
|
|
816
978
|
);
|
|
817
979
|
});
|
|
980
|
+
program.command("inspect").description("Show config structure without starting the server").action(async () => {
|
|
981
|
+
const c3 = {
|
|
982
|
+
reset: "\x1B[0m",
|
|
983
|
+
bold: "\x1B[1m",
|
|
984
|
+
dim: "\x1B[2m",
|
|
985
|
+
cyan: "\x1B[36m",
|
|
986
|
+
green: "\x1B[32m",
|
|
987
|
+
yellow: "\x1B[33m",
|
|
988
|
+
blue: "\x1B[34m",
|
|
989
|
+
gray: "\x1B[90m"
|
|
990
|
+
};
|
|
991
|
+
let config;
|
|
992
|
+
try {
|
|
993
|
+
config = await loadConfig();
|
|
994
|
+
} catch (err) {
|
|
995
|
+
console.error(` Error: ${err.message}`);
|
|
996
|
+
process.exit(1);
|
|
997
|
+
}
|
|
998
|
+
const entityNames = Object.keys(config.entities);
|
|
999
|
+
console.log(`
|
|
1000
|
+
${c3.bold}${c3.cyan}FFA${c3.reset}${c3.bold} inspect${c3.reset} ${c3.gray}port ${config.server.port}${c3.reset}
|
|
1001
|
+
`);
|
|
1002
|
+
for (const name of entityNames) {
|
|
1003
|
+
const def = config.entities[name];
|
|
1004
|
+
const count = def.count ?? 10;
|
|
1005
|
+
const seedCount = def.seed?.length ?? 0;
|
|
1006
|
+
const seedNote = seedCount > 0 ? ` ${c3.green}+${seedCount} seed${c3.reset}` : "";
|
|
1007
|
+
console.log(` ${c3.bold}${name}${c3.reset} ${c3.gray}${count} records${c3.reset}${seedNote}`);
|
|
1008
|
+
const fieldEntries = Object.entries(def.fields);
|
|
1009
|
+
const colName = Math.max(...fieldEntries.map(([k]) => k.length));
|
|
1010
|
+
for (const [fieldName, field] of fieldEntries) {
|
|
1011
|
+
const req = field.rules.required ? `${c3.yellow}*${c3.reset}` : " ";
|
|
1012
|
+
const ro = field.rules.readonly ? ` ${c3.gray}readonly${c3.reset}` : "";
|
|
1013
|
+
const hint = field.rules.fakeHint ? ` ${c3.dim}[${field.rules.fakeHint}]${c3.reset}` : "";
|
|
1014
|
+
const rel = field.rules.entity ? ` ${c3.blue}\u2192 ${field.rules.entity}${c3.reset}` : "";
|
|
1015
|
+
const enums = field.rules.enumValues?.length ? ` ${c3.gray}(${field.rules.enumValues.join(" | ")})${c3.reset}` : "";
|
|
1016
|
+
const nested = field.rules.objectFields ? ` ${c3.gray}{ ${Object.keys(field.rules.objectFields).join(", ")} }${c3.reset}` : "";
|
|
1017
|
+
const arrOf = field.rules.arrayItem ? ` ${c3.gray}of ${field.rules.arrayItem.type}${c3.reset}` : "";
|
|
1018
|
+
console.log(
|
|
1019
|
+
` ${req} ${fieldName.padEnd(colName)} ${c3.cyan}${field.type}${c3.reset}${hint}${rel}${enums}${nested}${arrOf}${ro}`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
if (def.meta && Object.keys(def.meta).length > 0) {
|
|
1023
|
+
const metaKeys = Object.keys(def.meta);
|
|
1024
|
+
console.log(` ${c3.gray}meta: ${metaKeys.join(", ")}${c3.reset}`);
|
|
1025
|
+
}
|
|
1026
|
+
console.log();
|
|
1027
|
+
}
|
|
1028
|
+
const { delay, persist, errorRate } = config.server;
|
|
1029
|
+
const info = [];
|
|
1030
|
+
if (delay !== void 0)
|
|
1031
|
+
info.push(`delay ${Array.isArray(delay) ? `${delay[0]}\u2013${delay[1]}ms` : `${delay}ms`}`);
|
|
1032
|
+
if (persist)
|
|
1033
|
+
info.push(`persist ${typeof persist === "string" ? persist : "ffa-data.json"}`);
|
|
1034
|
+
if (errorRate !== void 0 && errorRate > 0)
|
|
1035
|
+
info.push(`errorRate ${(errorRate * 100).toFixed(0)}%`);
|
|
1036
|
+
if (info.length > 0)
|
|
1037
|
+
console.log(` ${c3.gray}${info.join(" \xB7 ")}${c3.reset}
|
|
1038
|
+
`);
|
|
1039
|
+
});
|
|
818
1040
|
program.parse();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type FieldType = 'string' | 'number' | 'boolean' | 'uuid' | 'datetime' | 'enum' | 'belongsTo' | 'hasMany';
|
|
1
|
+
type FieldType = 'string' | 'number' | 'boolean' | 'uuid' | 'datetime' | 'enum' | 'belongsTo' | 'hasMany' | 'object' | 'array';
|
|
2
2
|
type StringFakeHint = 'email' | 'url' | 'domain' | 'ip' | 'username' | 'image' | 'avatar' | 'firstName' | 'lastName' | 'fullName' | 'phone' | 'city' | 'country' | 'address' | 'zip' | 'locale' | 'company' | 'jobTitle' | 'department' | 'currency' | 'word' | 'slug' | 'sentence' | 'paragraph' | 'bio' | 'color' | 'hexColor' | 'uuid';
|
|
3
3
|
type NumberFakeHint = 'price' | 'age' | 'rating' | 'percent' | 'lat' | 'lng' | 'year';
|
|
4
4
|
type FakeHint = StringFakeHint | NumberFakeHint;
|
|
@@ -19,6 +19,11 @@ interface FieldRules {
|
|
|
19
19
|
entity?: string;
|
|
20
20
|
enumValues?: string[];
|
|
21
21
|
fakeHint?: FakeHint;
|
|
22
|
+
imageWidth?: number;
|
|
23
|
+
imageHeight?: number;
|
|
24
|
+
objectFields?: Record<string, FieldDefinition>;
|
|
25
|
+
arrayItem?: FieldDefinition;
|
|
26
|
+
arrayCount?: [number, number];
|
|
22
27
|
}
|
|
23
28
|
interface FieldDefinition {
|
|
24
29
|
type: FieldType;
|
|
@@ -28,6 +33,7 @@ interface EntityDefinition {
|
|
|
28
33
|
fields: Record<string, FieldDefinition>;
|
|
29
34
|
count?: number;
|
|
30
35
|
meta?: Record<string, MetaValue>;
|
|
36
|
+
seed?: Record<string, unknown>[];
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
interface ServerConfig {
|
|
@@ -37,6 +43,7 @@ interface ServerConfig {
|
|
|
37
43
|
};
|
|
38
44
|
persist?: boolean | string;
|
|
39
45
|
delay?: number | [number, number];
|
|
46
|
+
errorRate?: number;
|
|
40
47
|
}
|
|
41
48
|
interface FfaConfig {
|
|
42
49
|
server: ServerConfig;
|
|
@@ -47,10 +54,12 @@ declare function defineConfig(config: FfaConfig): FfaConfig;
|
|
|
47
54
|
declare function entity(fields: Record<string, any>, options?: {
|
|
48
55
|
count?: number;
|
|
49
56
|
meta?: Record<string, MetaValue>;
|
|
57
|
+
seed?: Record<string, unknown>[];
|
|
50
58
|
}): {
|
|
51
59
|
fields: Record<string, any>;
|
|
52
60
|
count: number | undefined;
|
|
53
61
|
meta: Record<string, unknown> | undefined;
|
|
62
|
+
seed: Record<string, unknown>[] | undefined;
|
|
54
63
|
};
|
|
55
64
|
|
|
56
65
|
declare class FieldBuilder {
|
|
@@ -74,7 +83,7 @@ declare class StringFieldBuilder extends FieldBuilder {
|
|
|
74
83
|
domain(): this;
|
|
75
84
|
ip(): this;
|
|
76
85
|
username(): this;
|
|
77
|
-
image(): this;
|
|
86
|
+
image(width?: number, height?: number): this;
|
|
78
87
|
avatar(): this;
|
|
79
88
|
firstName(): this;
|
|
80
89
|
lastName(): this;
|
|
@@ -118,7 +127,9 @@ declare const datetime: () => FieldBuilder;
|
|
|
118
127
|
declare const enumField: (values: [string, ...string[]]) => FieldBuilder;
|
|
119
128
|
declare const belongsTo: (entity: string) => FieldBuilder;
|
|
120
129
|
declare const hasMany: (entity: string) => FieldBuilder;
|
|
130
|
+
declare const object: (fields: Record<string, FieldBuilder>) => FieldBuilder;
|
|
131
|
+
declare const array: (item: FieldBuilder, count?: [number, number]) => FieldBuilder;
|
|
121
132
|
|
|
122
|
-
declare const __ffa_version = "0.
|
|
133
|
+
declare const __ffa_version = "0.16.1";
|
|
123
134
|
|
|
124
|
-
export { type FakeHint, type FfaConfig, type MetaFn, type MetaValue, type NumberFakeHint, type StringFakeHint, __ffa_version, belongsTo, boolean, datetime, defineConfig, entity, enumField, hasMany, number, string, uuid };
|
|
135
|
+
export { type FakeHint, type FfaConfig, type MetaFn, type MetaValue, type NumberFakeHint, type StringFakeHint, __ffa_version, array, belongsTo, boolean, datetime, defineConfig, entity, enumField, hasMany, number, object, string, uuid };
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ function entity(fields, options) {
|
|
|
15
15
|
return {
|
|
16
16
|
fields: builtFields,
|
|
17
17
|
count: options?.count,
|
|
18
|
-
meta: options?.meta
|
|
18
|
+
meta: options?.meta,
|
|
19
|
+
seed: options?.seed
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -83,8 +84,11 @@ var StringFieldBuilder = class extends FieldBuilder {
|
|
|
83
84
|
return this.hint("username");
|
|
84
85
|
}
|
|
85
86
|
// Media
|
|
86
|
-
image() {
|
|
87
|
-
|
|
87
|
+
image(width, height) {
|
|
88
|
+
this.hint("image");
|
|
89
|
+
if (width !== void 0) this.def.rules.imageWidth = width;
|
|
90
|
+
if (height !== void 0) this.def.rules.imageHeight = height;
|
|
91
|
+
return this;
|
|
88
92
|
}
|
|
89
93
|
avatar() {
|
|
90
94
|
return this.hint("avatar");
|
|
@@ -200,11 +204,19 @@ var datetime = () => new FieldBuilder("datetime");
|
|
|
200
204
|
var enumField = (values) => new FieldBuilder("enum", { enumValues: values });
|
|
201
205
|
var belongsTo = (entity2) => new FieldBuilder("belongsTo", { entity: entity2 });
|
|
202
206
|
var hasMany = (entity2) => new FieldBuilder("hasMany", { entity: entity2 });
|
|
207
|
+
var object = (fields) => {
|
|
208
|
+
const objectFields = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, v.build()]));
|
|
209
|
+
return new FieldBuilder("object", { objectFields });
|
|
210
|
+
};
|
|
211
|
+
var array = (item, count) => {
|
|
212
|
+
return new FieldBuilder("array", { arrayItem: item.build(), arrayCount: count });
|
|
213
|
+
};
|
|
203
214
|
|
|
204
215
|
// src/index.ts
|
|
205
|
-
var __ffa_version = "0.
|
|
216
|
+
var __ffa_version = "0.16.1";
|
|
206
217
|
export {
|
|
207
218
|
__ffa_version,
|
|
219
|
+
array,
|
|
208
220
|
belongsTo,
|
|
209
221
|
boolean,
|
|
210
222
|
datetime,
|
|
@@ -213,6 +225,7 @@ export {
|
|
|
213
225
|
enumField,
|
|
214
226
|
hasMany,
|
|
215
227
|
number,
|
|
228
|
+
object,
|
|
216
229
|
string,
|
|
217
230
|
uuid
|
|
218
231
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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","import type { MetaValue } from '../field/types'\n\nexport function entity(\n fields: Record<string, any>,\n options?: { count?: number; meta?: Record<string, MetaValue> },\n) {\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 meta: options?.meta,\n }\n}\n","import type { FieldDefinition, FieldRules, FieldType, FakeHint, StringFakeHint, NumberFakeHint } from './types'\n\n// ─── Base builder ────────────────────────────────────────────────────────────\n\nexport class FieldBuilder {\n protected def: FieldDefinition\n\n constructor(type: FieldType, extraRules?: Partial<FieldRules>) {\n this.def = { type, rules: { ...extraRules } }\n }\n\n required(): this {\n this.def.rules.required = true\n return this\n }\n\n optional(): this {\n this.def.rules.required = false\n return this\n }\n\n min(value: number): this {\n this.def.rules.min = value\n return this\n }\n\n max(value: number): this {\n this.def.rules.max = value\n return this\n }\n\n default(value: any): this {\n this.def.rules.default = value\n return this\n }\n\n readonly(): this {\n this.def.rules.readonly = true\n return this\n }\n\n /** Explicit faker hint — overrides smart field-name detection */\n fake(hint: FakeHint): this {\n this.def.rules.fakeHint = hint\n return this\n }\n\n build(): FieldDefinition {\n return structuredClone(this.def)\n }\n}\n\n// ─── String builder ──────────────────────────────────────────────────────────\n\nexport class StringFieldBuilder extends FieldBuilder {\n constructor() {\n super('string')\n }\n\n private hint(h: StringFakeHint): this {\n this.def.rules.fakeHint = h\n return this\n }\n\n // Internet\n email(): this { return this.hint('email') }\n url(): this { return this.hint('url') }\n domain(): this { return this.hint('domain') }\n ip(): this { return this.hint('ip') }\n username(): this { return this.hint('username') }\n\n // Media\n image(): this { return this.hint('image') }\n avatar(): this { return this.hint('avatar') }\n\n // Person\n firstName(): this { return this.hint('firstName') }\n lastName(): this { return this.hint('lastName') }\n fullName(): this { return this.hint('fullName') }\n\n // Contact\n phone(): this { return this.hint('phone') }\n\n // Location\n city(): this { return this.hint('city') }\n country(): this { return this.hint('country') }\n address(): this { return this.hint('address') }\n zip(): this { return this.hint('zip') }\n locale(): this { return this.hint('locale') }\n\n // Business\n company(): this { return this.hint('company') }\n jobTitle(): this { return this.hint('jobTitle') }\n department(): this { return this.hint('department') }\n currency(): this { return this.hint('currency') }\n\n // Text\n word(): this { return this.hint('word') }\n slug(): this { return this.hint('slug') }\n sentence(): this { return this.hint('sentence') }\n paragraph(): this { return this.hint('paragraph') }\n bio(): this { return this.hint('bio') }\n\n // Visual\n color(): this { return this.hint('color') }\n hexColor(): this { return this.hint('hexColor') }\n\n // Id\n uuid(): this { return this.hint('uuid') }\n}\n\n// ─── Number builder ──────────────────────────────────────────────────────────\n\nexport class NumberFieldBuilder extends FieldBuilder {\n constructor() {\n super('number')\n }\n\n private hint(h: NumberFakeHint): this {\n this.def.rules.fakeHint = h\n return this\n }\n\n price(): this { return this.hint('price') }\n age(): this { return this.hint('age') }\n rating(): this { return this.hint('rating') }\n percent(): this { return this.hint('percent') }\n lat(): this { return this.hint('lat') }\n lng(): this { return this.hint('lng') }\n year(): this { return this.hint('year') }\n}\n","import { FieldBuilder, StringFieldBuilder, NumberFieldBuilder } from './FieldBuilder'\n\nexport const string = (): StringFieldBuilder => new StringFieldBuilder()\nexport const number = (): NumberFieldBuilder => new NumberFieldBuilder()\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'\nexport type { FakeHint, StringFakeHint, NumberFakeHint, MetaFn, MetaValue } from './field/types'\n\nexport const __ffa_version = '0.8.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;;;ACjBO,SAAS,OACd,QACA,SACA;AACA,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,IAChB,MAAM,SAAS;AAAA,EACjB;AACF;;;ACbO,IAAM,eAAN,MAAmB;AAAA,EACd;AAAA,EAEV,YAAY,MAAiB,YAAkC;AAC7D,SAAK,MAAM,EAAE,MAAM,OAAO,EAAE,GAAG,WAAW,EAAE;AAAA,EAC9C;AAAA,EAEA,WAAiB;AACf,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,WAAiB;AACf,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAAqB;AACvB,SAAK,IAAI,MAAM,MAAM;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAAqB;AACvB,SAAK,IAAI,MAAM,MAAM;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,OAAkB;AACxB,SAAK,IAAI,MAAM,UAAU;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,WAAiB;AACf,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,KAAK,MAAsB;AACzB,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,QAAyB;AACvB,WAAO,gBAAgB,KAAK,GAAG;AAAA,EACjC;AACF;AAIO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,cAAc;AACZ,UAAM,QAAQ;AAAA,EAChB;AAAA,EAEQ,KAAK,GAAyB;AACpC,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,QAAmB;AAAE,WAAO,KAAK,KAAK,OAAO;AAAA,EAAE;AAAA,EAC/C,MAAmB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA,EAC7C,SAAmB;AAAE,WAAO,KAAK,KAAK,QAAQ;AAAA,EAAE;AAAA,EAChD,KAAmB;AAAE,WAAO,KAAK,KAAK,IAAI;AAAA,EAAE;AAAA,EAC5C,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA;AAAA,EAGlD,QAAmB;AAAE,WAAO,KAAK,KAAK,OAAO;AAAA,EAAE;AAAA,EAC/C,SAAmB;AAAE,WAAO,KAAK,KAAK,QAAQ;AAAA,EAAE;AAAA;AAAA,EAGhD,YAAmB;AAAE,WAAO,KAAK,KAAK,WAAW;AAAA,EAAE;AAAA,EACnD,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA,EAClD,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA;AAAA,EAGlD,QAAmB;AAAE,WAAO,KAAK,KAAK,OAAO;AAAA,EAAE;AAAA;AAAA,EAG/C,OAAmB;AAAE,WAAO,KAAK,KAAK,MAAM;AAAA,EAAE;AAAA,EAC9C,UAAmB;AAAE,WAAO,KAAK,KAAK,SAAS;AAAA,EAAE;AAAA,EACjD,UAAmB;AAAE,WAAO,KAAK,KAAK,SAAS;AAAA,EAAE;AAAA,EACjD,MAAmB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA,EAC7C,SAAmB;AAAE,WAAO,KAAK,KAAK,QAAQ;AAAA,EAAE;AAAA;AAAA,EAGhD,UAAmB;AAAE,WAAO,KAAK,KAAK,SAAS;AAAA,EAAE;AAAA,EACjD,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA,EAClD,aAAmB;AAAE,WAAO,KAAK,KAAK,YAAY;AAAA,EAAE;AAAA,EACpD,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA;AAAA,EAGlD,OAAmB;AAAE,WAAO,KAAK,KAAK,MAAM;AAAA,EAAE;AAAA,EAC9C,OAAmB;AAAE,WAAO,KAAK,KAAK,MAAM;AAAA,EAAE;AAAA,EAC9C,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA,EAClD,YAAmB;AAAE,WAAO,KAAK,KAAK,WAAW;AAAA,EAAE;AAAA,EACnD,MAAmB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA;AAAA,EAG7C,QAAmB;AAAE,WAAO,KAAK,KAAK,OAAO;AAAA,EAAE;AAAA,EAC/C,WAAmB;AAAE,WAAO,KAAK,KAAK,UAAU;AAAA,EAAE;AAAA;AAAA,EAGlD,OAAmB;AAAE,WAAO,KAAK,KAAK,MAAM;AAAA,EAAE;AAChD;AAIO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,cAAc;AACZ,UAAM,QAAQ;AAAA,EAChB;AAAA,EAEQ,KAAK,GAAyB;AACpC,SAAK,IAAI,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,QAAgB;AAAE,WAAO,KAAK,KAAK,OAAO;AAAA,EAAE;AAAA,EAC5C,MAAgB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA,EAC1C,SAAgB;AAAE,WAAO,KAAK,KAAK,QAAQ;AAAA,EAAE;AAAA,EAC7C,UAAgB;AAAE,WAAO,KAAK,KAAK,SAAS;AAAA,EAAE;AAAA,EAC9C,MAAgB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA,EAC1C,MAAgB;AAAE,WAAO,KAAK,KAAK,KAAK;AAAA,EAAE;AAAA,EAC1C,OAAgB;AAAE,WAAO,KAAK,KAAK,MAAM;AAAA,EAAE;AAC7C;;;AChIO,IAAM,SAAS,MAA0B,IAAI,mBAAmB;AAChE,IAAM,SAAS,MAA0B,IAAI,mBAAmB;AAChE,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;;;ACE1E,IAAM,gBAAgB;","names":["entity"]}
|
|
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 errorRate?: 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","import type { MetaValue } from '../field/types'\n\nexport function entity(\n fields: Record<string, any>,\n options?: { count?: number; meta?: Record<string, MetaValue>; seed?: Record<string, unknown>[] },\n) {\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 meta: options?.meta,\n seed: options?.seed,\n }\n}\n","import type { FieldDefinition, FieldRules, FieldType, FakeHint, StringFakeHint, NumberFakeHint } from './types'\n\n// ─── Base builder ────────────────────────────────────────────────────────────\n\nexport class FieldBuilder {\n protected def: FieldDefinition\n\n constructor(type: FieldType, extraRules?: Partial<FieldRules>) {\n this.def = { type, rules: { ...extraRules } }\n }\n\n required(): this {\n this.def.rules.required = true\n return this\n }\n\n optional(): this {\n this.def.rules.required = false\n return this\n }\n\n min(value: number): this {\n this.def.rules.min = value\n return this\n }\n\n max(value: number): this {\n this.def.rules.max = value\n return this\n }\n\n default(value: any): this {\n this.def.rules.default = value\n return this\n }\n\n readonly(): this {\n this.def.rules.readonly = true\n return this\n }\n\n /** Explicit faker hint — overrides smart field-name detection */\n fake(hint: FakeHint): this {\n this.def.rules.fakeHint = hint\n return this\n }\n\n build(): FieldDefinition {\n return structuredClone(this.def)\n }\n}\n\n// ─── String builder ──────────────────────────────────────────────────────────\n\nexport class StringFieldBuilder extends FieldBuilder {\n constructor() {\n super('string')\n }\n\n private hint(h: StringFakeHint): this {\n this.def.rules.fakeHint = h\n return this\n }\n\n // Internet\n email(): this { return this.hint('email') }\n url(): this { return this.hint('url') }\n domain(): this { return this.hint('domain') }\n ip(): this { return this.hint('ip') }\n username(): this { return this.hint('username') }\n\n // Media\n image(width?: number, height?: number): this {\n this.hint('image')\n if (width !== undefined) this.def.rules.imageWidth = width\n if (height !== undefined) this.def.rules.imageHeight = height\n return this\n }\n avatar(): this { return this.hint('avatar') }\n\n // Person\n firstName(): this { return this.hint('firstName') }\n lastName(): this { return this.hint('lastName') }\n fullName(): this { return this.hint('fullName') }\n\n // Contact\n phone(): this { return this.hint('phone') }\n\n // Location\n city(): this { return this.hint('city') }\n country(): this { return this.hint('country') }\n address(): this { return this.hint('address') }\n zip(): this { return this.hint('zip') }\n locale(): this { return this.hint('locale') }\n\n // Business\n company(): this { return this.hint('company') }\n jobTitle(): this { return this.hint('jobTitle') }\n department(): this { return this.hint('department') }\n currency(): this { return this.hint('currency') }\n\n // Text\n word(): this { return this.hint('word') }\n slug(): this { return this.hint('slug') }\n sentence(): this { return this.hint('sentence') }\n paragraph(): this { return this.hint('paragraph') }\n bio(): this { return this.hint('bio') }\n\n // Visual\n color(): this { return this.hint('color') }\n hexColor(): this { return this.hint('hexColor') }\n\n // Id\n uuid(): this { return this.hint('uuid') }\n}\n\n// ─── Number builder ──────────────────────────────────────────────────────────\n\nexport class NumberFieldBuilder extends FieldBuilder {\n constructor() {\n super('number')\n }\n\n private hint(h: NumberFakeHint): this {\n this.def.rules.fakeHint = h\n return this\n }\n\n price(): this { return this.hint('price') }\n age(): this { return this.hint('age') }\n rating(): this { return this.hint('rating') }\n percent(): this { return this.hint('percent') }\n lat(): this { return this.hint('lat') }\n lng(): this { return this.hint('lng') }\n year(): this { return this.hint('year') }\n}\n","import { FieldBuilder, StringFieldBuilder, NumberFieldBuilder } from './FieldBuilder'\n\nexport const string = (): StringFieldBuilder => new StringFieldBuilder()\nexport const number = (): NumberFieldBuilder => new NumberFieldBuilder()\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\nexport const object = (fields: Record<string, FieldBuilder>): FieldBuilder => {\n const objectFields = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, v.build()]))\n return new FieldBuilder('object', { objectFields })\n}\n\nexport const array = (item: FieldBuilder, count?: [number, number]): FieldBuilder => {\n return new FieldBuilder('array', { arrayItem: item.build(), arrayCount: count })\n}\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 {\n string,\n number,\n boolean,\n uuid,\n datetime,\n enumField,\n belongsTo,\n hasMany,\n object,\n array,\n} from './field/factories'\nexport type {\n FakeHint,\n StringFakeHint,\n NumberFakeHint,\n MetaFn,\n MetaValue,\n} from './field/types'\n\nexport const __ffa_version = '0.16.1'\n"],"mappings":"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","names":["entity"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koltakov/ffa-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Instant mock REST API for frontend development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"build": "tsup",
|
|
23
23
|
"dev": "tsx src/cli.ts",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
25
27
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
26
28
|
},
|
|
27
29
|
"keywords": [
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
"@types/node": "^25.3.3",
|
|
65
67
|
"tsup": "^8.0.0",
|
|
66
68
|
"tsx": "^4.21.0",
|
|
67
|
-
"typescript": "^5.9.3"
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"vitest": "^4.0.18"
|
|
68
71
|
}
|
|
69
72
|
}
|