@koltakov/ffa-core 0.8.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 +358 -300
- package/dist/cli.js +250 -36
- package/dist/index.d.ts +12 -3
- package/dist/index.js +12 -2
- 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(),
|
|
@@ -167,6 +201,21 @@ function generateFakeValue(def, fieldName, db) {
|
|
|
167
201
|
if (def.rules.enumValues?.length) return faker.helpers.arrayElement(def.rules.enumValues);
|
|
168
202
|
return faker.lorem.word();
|
|
169
203
|
}
|
|
204
|
+
if (def.type === "object") {
|
|
205
|
+
if (!def.rules.objectFields) return {};
|
|
206
|
+
return Object.fromEntries(
|
|
207
|
+
Object.entries(def.rules.objectFields).map(([k, fieldDef]) => [
|
|
208
|
+
k,
|
|
209
|
+
generateFakeValue(fieldDef, k, db)
|
|
210
|
+
])
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (def.type === "array") {
|
|
214
|
+
if (!def.rules.arrayItem) return [];
|
|
215
|
+
const [min, max] = def.rules.arrayCount ?? [1, 3];
|
|
216
|
+
const count = faker.number.int({ min, max });
|
|
217
|
+
return Array.from({ length: count }).map(() => generateFakeValue(def.rules.arrayItem, "item", db));
|
|
218
|
+
}
|
|
170
219
|
const normalized = fieldName.toLowerCase().replace(/[-\s]/g, "_");
|
|
171
220
|
if (FIELD_NAME_MAP[normalized]) return FIELD_NAME_MAP[normalized]();
|
|
172
221
|
for (const key of Object.keys(FIELD_NAME_MAP)) {
|
|
@@ -215,17 +264,21 @@ function createMemoryDB(entities, persistPath) {
|
|
|
215
264
|
...names.filter((n) => Object.values(entities[n].fields).some((f) => f.type === "belongsTo" || f.type === "hasMany"))
|
|
216
265
|
];
|
|
217
266
|
}
|
|
267
|
+
function seedEntity(name) {
|
|
268
|
+
const { fields, count = 10, seed } = entities[name];
|
|
269
|
+
const seedItems = (seed ?? []).map((record) => ({
|
|
270
|
+
id: faker.string.uuid(),
|
|
271
|
+
...record
|
|
272
|
+
}));
|
|
273
|
+
const generateCount = Math.max(0, count - seedItems.length);
|
|
274
|
+
const generated = Array.from({ length: generateCount }).map(() => generateRecord(fields, db));
|
|
275
|
+
db[name] = [...seedItems, ...generated];
|
|
276
|
+
}
|
|
218
277
|
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
|
-
}
|
|
278
|
+
for (const name of sortedEntityNames()) seedEntity(name);
|
|
223
279
|
}
|
|
224
280
|
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
|
-
}
|
|
281
|
+
if (!db[name]) seedEntity(name);
|
|
229
282
|
}
|
|
230
283
|
function persist() {
|
|
231
284
|
if (persistPath) writeFileSync(persistPath, JSON.stringify(db, null, 2), "utf-8");
|
|
@@ -240,9 +293,10 @@ function createMemoryDB(entities, persistPath) {
|
|
|
240
293
|
);
|
|
241
294
|
}
|
|
242
295
|
if (options.filter) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
const conditions = parseFilter(options.filter);
|
|
297
|
+
items = items.filter(
|
|
298
|
+
(item) => conditions.every(({ field, op, value }) => applyFilter(item, field, op, value))
|
|
299
|
+
);
|
|
246
300
|
}
|
|
247
301
|
if (options.sort) {
|
|
248
302
|
const sortKey = options.sort;
|
|
@@ -306,6 +360,9 @@ function createMemoryDB(entities, persistPath) {
|
|
|
306
360
|
},
|
|
307
361
|
count(entity) {
|
|
308
362
|
return db[entity]?.length ?? 0;
|
|
363
|
+
},
|
|
364
|
+
snapshot() {
|
|
365
|
+
return Object.fromEntries(Object.entries(db).map(([k, v]) => [k, v.map((item) => ({ ...item }))]));
|
|
309
366
|
}
|
|
310
367
|
};
|
|
311
368
|
}
|
|
@@ -354,6 +411,22 @@ function fieldToZod(def) {
|
|
|
354
411
|
schema = z.array(z.string());
|
|
355
412
|
break;
|
|
356
413
|
}
|
|
414
|
+
case "object": {
|
|
415
|
+
if (!def.rules.objectFields) {
|
|
416
|
+
schema = z.object({});
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
const shape = {};
|
|
420
|
+
for (const [k, fieldDef] of Object.entries(def.rules.objectFields)) {
|
|
421
|
+
shape[k] = fieldToZod(fieldDef);
|
|
422
|
+
}
|
|
423
|
+
schema = z.object(shape);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case "array": {
|
|
427
|
+
schema = def.rules.arrayItem ? z.array(fieldToZod(def.rules.arrayItem)) : z.array(z.unknown());
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
357
430
|
default: {
|
|
358
431
|
schema = z.unknown();
|
|
359
432
|
}
|
|
@@ -550,6 +623,39 @@ function generateOpenApiSpec(config) {
|
|
|
550
623
|
};
|
|
551
624
|
}
|
|
552
625
|
|
|
626
|
+
// src/validate-config.ts
|
|
627
|
+
var c = {
|
|
628
|
+
yellow: "\x1B[33m",
|
|
629
|
+
reset: "\x1B[0m",
|
|
630
|
+
bold: "\x1B[1m"
|
|
631
|
+
};
|
|
632
|
+
function validateConfig(entities) {
|
|
633
|
+
const names = new Set(Object.keys(entities));
|
|
634
|
+
const warnings = [];
|
|
635
|
+
for (const [entityName, def] of Object.entries(entities)) {
|
|
636
|
+
for (const [fieldName, field] of Object.entries(def.fields)) {
|
|
637
|
+
if ((field.type === "belongsTo" || field.type === "hasMany") && field.rules.entity) {
|
|
638
|
+
if (!names.has(field.rules.entity)) {
|
|
639
|
+
warnings.push(
|
|
640
|
+
`${entityName}.${fieldName} (${field.type}) references '${field.rules.entity}' which is not defined`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (field.type === "enum" && (!field.rules.enumValues || field.rules.enumValues.length === 0)) {
|
|
645
|
+
warnings.push(`${entityName}.${fieldName} is enum but has no values`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if ((def.count ?? 10) < (def.seed?.length ?? 0)) {
|
|
649
|
+
warnings.push(
|
|
650
|
+
`${entityName}: seed has ${def.seed.length} records but count is ${def.count} \u2014 seed will be truncated`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
for (const w of warnings) {
|
|
655
|
+
console.warn(` ${c.yellow}${c.bold}\u26A0${c.reset} ${w}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
553
659
|
// src/server.ts
|
|
554
660
|
function pluralize2(word) {
|
|
555
661
|
const lower = word.toLowerCase();
|
|
@@ -576,7 +682,7 @@ async function findPort(start) {
|
|
|
576
682
|
}
|
|
577
683
|
return port;
|
|
578
684
|
}
|
|
579
|
-
var
|
|
685
|
+
var c2 = {
|
|
580
686
|
reset: "\x1B[0m",
|
|
581
687
|
bold: "\x1B[1m",
|
|
582
688
|
dim: "\x1B[2m",
|
|
@@ -591,29 +697,30 @@ function colorMethod(method) {
|
|
|
591
697
|
const m = method.toUpperCase().padEnd(6);
|
|
592
698
|
switch (method.toUpperCase()) {
|
|
593
699
|
case "GET":
|
|
594
|
-
return `${
|
|
700
|
+
return `${c2.cyan}${m}${c2.reset}`;
|
|
595
701
|
case "POST":
|
|
596
|
-
return `${
|
|
702
|
+
return `${c2.green}${m}${c2.reset}`;
|
|
597
703
|
case "PUT":
|
|
598
|
-
return `${
|
|
704
|
+
return `${c2.yellow}${m}${c2.reset}`;
|
|
599
705
|
case "PATCH":
|
|
600
|
-
return `${
|
|
706
|
+
return `${c2.yellow}${m}${c2.reset}`;
|
|
601
707
|
case "DELETE":
|
|
602
|
-
return `${
|
|
708
|
+
return `${c2.red}${m}${c2.reset}`;
|
|
603
709
|
default:
|
|
604
710
|
return m;
|
|
605
711
|
}
|
|
606
712
|
}
|
|
607
713
|
function colorStatus(code) {
|
|
608
714
|
const s = String(code);
|
|
609
|
-
if (code < 300) return `${
|
|
610
|
-
if (code < 400) return `${
|
|
611
|
-
if (code < 500) return `${
|
|
612
|
-
return `${
|
|
715
|
+
if (code < 300) return `${c2.green}${s}${c2.reset}`;
|
|
716
|
+
if (code < 400) return `${c2.cyan}${s}${c2.reset}`;
|
|
717
|
+
if (code < 500) return `${c2.yellow}${s}${c2.reset}`;
|
|
718
|
+
return `${c2.red}${s}${c2.reset}`;
|
|
613
719
|
}
|
|
614
720
|
var currentApp = null;
|
|
615
721
|
async function startServer(portOverride) {
|
|
616
722
|
const config = await loadConfig();
|
|
723
|
+
validateConfig(config.entities);
|
|
617
724
|
const port = await findPort(portOverride ?? config.server.port);
|
|
618
725
|
const openApiSpec = generateOpenApiSpec(config);
|
|
619
726
|
const app = Fastify({ logger: false });
|
|
@@ -634,6 +741,16 @@ async function startServer(portOverride) {
|
|
|
634
741
|
await new Promise((r) => setTimeout(r, ms));
|
|
635
742
|
});
|
|
636
743
|
}
|
|
744
|
+
const errorRate = config.server.errorRate;
|
|
745
|
+
if (errorRate !== void 0 && errorRate > 0) {
|
|
746
|
+
const SYSTEM_ROUTES_ERR = /* @__PURE__ */ new Set(["/__reset", "/docs", "/openapi.json", "/__snapshot"]);
|
|
747
|
+
app.addHook("preHandler", async (req, reply) => {
|
|
748
|
+
if (SYSTEM_ROUTES_ERR.has(req.url) || req.url.startsWith("/docs")) return;
|
|
749
|
+
if (Math.random() < errorRate) {
|
|
750
|
+
reply.code(500).send({ error: "Simulated server error", hint: "errorRate is enabled in config" });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
637
754
|
app.addHook("onResponse", (req, reply, done) => {
|
|
638
755
|
if (req.url.startsWith("/docs/static") || req.url === "/favicon.ico") {
|
|
639
756
|
done();
|
|
@@ -642,7 +759,7 @@ async function startServer(portOverride) {
|
|
|
642
759
|
const ms = Date.now() - (req._ffaStart ?? Date.now());
|
|
643
760
|
const method = colorMethod(req.method);
|
|
644
761
|
const status = colorStatus(reply.statusCode);
|
|
645
|
-
const time = `${
|
|
762
|
+
const time = `${c2.gray}${ms}ms${c2.reset}`;
|
|
646
763
|
console.log(` ${method} ${req.url.padEnd(35)} ${status} ${time}`);
|
|
647
764
|
done();
|
|
648
765
|
});
|
|
@@ -652,10 +769,10 @@ async function startServer(portOverride) {
|
|
|
652
769
|
}
|
|
653
770
|
const db = createMemoryDB(config.entities, persistPath);
|
|
654
771
|
for (const entityName in config.entities) {
|
|
655
|
-
const
|
|
772
|
+
const base2 = `/${pluralize2(entityName)}`;
|
|
656
773
|
const zodSchema = entityToZod(config.entities[entityName].fields);
|
|
657
774
|
const entityFields = config.entities[entityName].fields;
|
|
658
|
-
app.get(
|
|
775
|
+
app.get(base2, (req, reply) => {
|
|
659
776
|
const query = req.query;
|
|
660
777
|
const page = query.page ? Number(query.page) : void 0;
|
|
661
778
|
const limit = query.limit ? Number(query.limit) : void 0;
|
|
@@ -678,7 +795,7 @@ async function startServer(portOverride) {
|
|
|
678
795
|
reply.header("X-Total-Count", String(result.meta.total));
|
|
679
796
|
return result;
|
|
680
797
|
});
|
|
681
|
-
app.get(`${
|
|
798
|
+
app.get(`${base2}/:id`, (req, reply) => {
|
|
682
799
|
const item = db.get(entityName, req.params.id);
|
|
683
800
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
684
801
|
const include = req.query.include;
|
|
@@ -695,13 +812,13 @@ async function startServer(portOverride) {
|
|
|
695
812
|
}
|
|
696
813
|
return result;
|
|
697
814
|
});
|
|
698
|
-
app.post(
|
|
815
|
+
app.post(base2, (req, reply) => {
|
|
699
816
|
const result = zodSchema.safeParse(req.body);
|
|
700
817
|
if (!result.success)
|
|
701
818
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
702
819
|
return reply.code(201).send(db.create(entityName, result.data));
|
|
703
820
|
});
|
|
704
|
-
app.put(`${
|
|
821
|
+
app.put(`${base2}/:id`, (req, reply) => {
|
|
705
822
|
const result = zodSchema.safeParse(req.body);
|
|
706
823
|
if (!result.success)
|
|
707
824
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
@@ -709,7 +826,7 @@ async function startServer(portOverride) {
|
|
|
709
826
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
710
827
|
return item;
|
|
711
828
|
});
|
|
712
|
-
app.patch(`${
|
|
829
|
+
app.patch(`${base2}/:id`, (req, reply) => {
|
|
713
830
|
const result = zodSchema.partial().safeParse(req.body);
|
|
714
831
|
if (!result.success)
|
|
715
832
|
return reply.code(422).send({ error: "Validation failed", issues: result.error.issues });
|
|
@@ -717,7 +834,7 @@ async function startServer(portOverride) {
|
|
|
717
834
|
if (!item) return reply.code(404).send({ error: "Not found" });
|
|
718
835
|
return item;
|
|
719
836
|
});
|
|
720
|
-
app.delete(`${
|
|
837
|
+
app.delete(`${base2}/:id`, (req, reply) => {
|
|
721
838
|
const ok = db.remove(entityName, req.params.id);
|
|
722
839
|
if (!ok) return reply.code(404).send({ error: "Not found" });
|
|
723
840
|
return { success: true };
|
|
@@ -727,23 +844,36 @@ async function startServer(portOverride) {
|
|
|
727
844
|
db.reset();
|
|
728
845
|
return { success: true, message: "Data reset to seed state" };
|
|
729
846
|
});
|
|
847
|
+
app.get("/__snapshot", () => db.snapshot());
|
|
730
848
|
app.get("/openapi.json", () => openApiSpec);
|
|
731
849
|
await app.listen({ port, host: "0.0.0.0" });
|
|
732
|
-
const
|
|
850
|
+
const VERSION = "0.16.0";
|
|
851
|
+
const base = `http://localhost:${port}`;
|
|
852
|
+
const entityNames = Object.keys(config.entities);
|
|
853
|
+
const colEntity = Math.max(6, ...entityNames.map((n) => n.length));
|
|
854
|
+
const colRoute = Math.max(5, ...entityNames.map((n) => `/${pluralize2(n)}`.length));
|
|
855
|
+
const header = ` ${"Entity".padEnd(colEntity)} ${"Count".padStart(5)} Route`;
|
|
856
|
+
const divider = ` ${"\u2500".repeat(colEntity)} ${"\u2500".repeat(5)} ${"\u2500".repeat(colRoute + 18)}`;
|
|
857
|
+
const entityRows = entityNames.map((name) => {
|
|
733
858
|
const count = db.count(name);
|
|
734
|
-
const
|
|
735
|
-
|
|
859
|
+
const route = `/${pluralize2(name)}`;
|
|
860
|
+
const methods = `${c2.cyan}GET${c2.reset} ${c2.green}POST${c2.reset}`;
|
|
861
|
+
return ` ${c2.bold}${name.padEnd(colEntity)}${c2.reset} ${c2.gray}${String(count).padStart(5)}${c2.reset} ${methods} ${c2.cyan}${base}${route}${c2.reset}`;
|
|
736
862
|
}).join("\n");
|
|
737
|
-
const
|
|
738
|
-
` :
|
|
863
|
+
const delayLine = delayConfig !== void 0 ? ` ${c2.gray}delay ${Array.isArray(delayConfig) ? `${delayConfig[0]}\u2013${delayConfig[1]}ms` : `${delayConfig}ms`}${c2.reset}
|
|
864
|
+
` : "";
|
|
865
|
+
const errorLine = errorRate !== void 0 && errorRate > 0 ? ` ${c2.yellow}errorRate ${(errorRate * 100).toFixed(0)}% chance of 500${c2.reset}
|
|
739
866
|
` : "";
|
|
740
867
|
console.log(
|
|
741
868
|
`
|
|
742
|
-
${
|
|
869
|
+
${c2.bold}${c2.cyan}FFA${c2.reset}${c2.bold} dev server${c2.reset} ${c2.gray}v${VERSION}${c2.reset}
|
|
743
870
|
|
|
871
|
+
${c2.gray}${header}
|
|
872
|
+
${divider}${c2.reset}
|
|
744
873
|
${entityRows}
|
|
745
874
|
|
|
746
|
-
${
|
|
875
|
+
${delayLine}${errorLine} ${c2.gray}docs ${c2.reset}${c2.cyan}${base}/docs${c2.reset}
|
|
876
|
+
${c2.gray}reset ${c2.reset}${c2.cyan}POST ${base}/__reset${c2.reset}
|
|
747
877
|
`
|
|
748
878
|
);
|
|
749
879
|
return { app, port };
|
|
@@ -804,6 +934,30 @@ export default defineConfig({
|
|
|
804
934
|
writeFileSync2(tsPath, template, "utf-8");
|
|
805
935
|
console.log(" ffa.config.ts created! Run: ffa dev");
|
|
806
936
|
});
|
|
937
|
+
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) => {
|
|
938
|
+
let port = opts.port ? Number(opts.port) : void 0;
|
|
939
|
+
if (!port) {
|
|
940
|
+
try {
|
|
941
|
+
const cfg = await loadConfig();
|
|
942
|
+
port = cfg.server.port;
|
|
943
|
+
} catch {
|
|
944
|
+
port = 3e3;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
const res = await fetch(`http://localhost:${port}/__snapshot`);
|
|
949
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
950
|
+
const data = await res.json();
|
|
951
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
952
|
+
writeFileSync2(outPath, JSON.stringify(data, null, 2), "utf-8");
|
|
953
|
+
const counts = Object.entries(data).map(([k, v]) => `${k}: ${v.length}`).join(", ");
|
|
954
|
+
console.log(` Snapshot saved \u2192 ${outPath}`);
|
|
955
|
+
console.log(` ${counts}`);
|
|
956
|
+
} catch {
|
|
957
|
+
console.error(` Failed to snapshot. Is the server running on port ${port}?`);
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
807
961
|
program.command("reset").description("Delete persisted data file (ffa-data.json)").action(() => {
|
|
808
962
|
const dataPath = resolve(process.cwd(), "ffa-data.json");
|
|
809
963
|
if (!existsSync3(dataPath)) {
|
|
@@ -815,4 +969,64 @@ program.command("reset").description("Delete persisted data file (ffa-data.json)
|
|
|
815
969
|
" ffa-data.json deleted. Restart the server to regenerate seed data."
|
|
816
970
|
);
|
|
817
971
|
});
|
|
972
|
+
program.command("inspect").description("Show config structure without starting the server").action(async () => {
|
|
973
|
+
const c3 = {
|
|
974
|
+
reset: "\x1B[0m",
|
|
975
|
+
bold: "\x1B[1m",
|
|
976
|
+
dim: "\x1B[2m",
|
|
977
|
+
cyan: "\x1B[36m",
|
|
978
|
+
green: "\x1B[32m",
|
|
979
|
+
yellow: "\x1B[33m",
|
|
980
|
+
blue: "\x1B[34m",
|
|
981
|
+
gray: "\x1B[90m"
|
|
982
|
+
};
|
|
983
|
+
let config;
|
|
984
|
+
try {
|
|
985
|
+
config = await loadConfig();
|
|
986
|
+
} catch (err) {
|
|
987
|
+
console.error(` Error: ${err.message}`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
const entityNames = Object.keys(config.entities);
|
|
991
|
+
console.log(`
|
|
992
|
+
${c3.bold}${c3.cyan}FFA${c3.reset}${c3.bold} inspect${c3.reset} ${c3.gray}port ${config.server.port}${c3.reset}
|
|
993
|
+
`);
|
|
994
|
+
for (const name of entityNames) {
|
|
995
|
+
const def = config.entities[name];
|
|
996
|
+
const count = def.count ?? 10;
|
|
997
|
+
const seedCount = def.seed?.length ?? 0;
|
|
998
|
+
const seedNote = seedCount > 0 ? ` ${c3.green}+${seedCount} seed${c3.reset}` : "";
|
|
999
|
+
console.log(` ${c3.bold}${name}${c3.reset} ${c3.gray}${count} records${c3.reset}${seedNote}`);
|
|
1000
|
+
const fieldEntries = Object.entries(def.fields);
|
|
1001
|
+
const colName = Math.max(...fieldEntries.map(([k]) => k.length));
|
|
1002
|
+
for (const [fieldName, field] of fieldEntries) {
|
|
1003
|
+
const req = field.rules.required ? `${c3.yellow}*${c3.reset}` : " ";
|
|
1004
|
+
const ro = field.rules.readonly ? ` ${c3.gray}readonly${c3.reset}` : "";
|
|
1005
|
+
const hint = field.rules.fakeHint ? ` ${c3.dim}[${field.rules.fakeHint}]${c3.reset}` : "";
|
|
1006
|
+
const rel = field.rules.entity ? ` ${c3.blue}\u2192 ${field.rules.entity}${c3.reset}` : "";
|
|
1007
|
+
const enums = field.rules.enumValues?.length ? ` ${c3.gray}(${field.rules.enumValues.join(" | ")})${c3.reset}` : "";
|
|
1008
|
+
const nested = field.rules.objectFields ? ` ${c3.gray}{ ${Object.keys(field.rules.objectFields).join(", ")} }${c3.reset}` : "";
|
|
1009
|
+
const arrOf = field.rules.arrayItem ? ` ${c3.gray}of ${field.rules.arrayItem.type}${c3.reset}` : "";
|
|
1010
|
+
console.log(
|
|
1011
|
+
` ${req} ${fieldName.padEnd(colName)} ${c3.cyan}${field.type}${c3.reset}${hint}${rel}${enums}${nested}${arrOf}${ro}`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (def.meta && Object.keys(def.meta).length > 0) {
|
|
1015
|
+
const metaKeys = Object.keys(def.meta);
|
|
1016
|
+
console.log(` ${c3.gray}meta: ${metaKeys.join(", ")}${c3.reset}`);
|
|
1017
|
+
}
|
|
1018
|
+
console.log();
|
|
1019
|
+
}
|
|
1020
|
+
const { delay, persist, errorRate } = config.server;
|
|
1021
|
+
const info = [];
|
|
1022
|
+
if (delay !== void 0)
|
|
1023
|
+
info.push(`delay ${Array.isArray(delay) ? `${delay[0]}\u2013${delay[1]}ms` : `${delay}ms`}`);
|
|
1024
|
+
if (persist)
|
|
1025
|
+
info.push(`persist ${typeof persist === "string" ? persist : "ffa-data.json"}`);
|
|
1026
|
+
if (errorRate !== void 0 && errorRate > 0)
|
|
1027
|
+
info.push(`errorRate ${(errorRate * 100).toFixed(0)}%`);
|
|
1028
|
+
if (info.length > 0)
|
|
1029
|
+
console.log(` ${c3.gray}${info.join(" \xB7 ")}${c3.reset}
|
|
1030
|
+
`);
|
|
1031
|
+
});
|
|
818
1032
|
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,9 @@ interface FieldRules {
|
|
|
19
19
|
entity?: string;
|
|
20
20
|
enumValues?: string[];
|
|
21
21
|
fakeHint?: FakeHint;
|
|
22
|
+
objectFields?: Record<string, FieldDefinition>;
|
|
23
|
+
arrayItem?: FieldDefinition;
|
|
24
|
+
arrayCount?: [number, number];
|
|
22
25
|
}
|
|
23
26
|
interface FieldDefinition {
|
|
24
27
|
type: FieldType;
|
|
@@ -28,6 +31,7 @@ interface EntityDefinition {
|
|
|
28
31
|
fields: Record<string, FieldDefinition>;
|
|
29
32
|
count?: number;
|
|
30
33
|
meta?: Record<string, MetaValue>;
|
|
34
|
+
seed?: Record<string, unknown>[];
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
interface ServerConfig {
|
|
@@ -37,6 +41,7 @@ interface ServerConfig {
|
|
|
37
41
|
};
|
|
38
42
|
persist?: boolean | string;
|
|
39
43
|
delay?: number | [number, number];
|
|
44
|
+
errorRate?: number;
|
|
40
45
|
}
|
|
41
46
|
interface FfaConfig {
|
|
42
47
|
server: ServerConfig;
|
|
@@ -47,10 +52,12 @@ declare function defineConfig(config: FfaConfig): FfaConfig;
|
|
|
47
52
|
declare function entity(fields: Record<string, any>, options?: {
|
|
48
53
|
count?: number;
|
|
49
54
|
meta?: Record<string, MetaValue>;
|
|
55
|
+
seed?: Record<string, unknown>[];
|
|
50
56
|
}): {
|
|
51
57
|
fields: Record<string, any>;
|
|
52
58
|
count: number | undefined;
|
|
53
59
|
meta: Record<string, unknown> | undefined;
|
|
60
|
+
seed: Record<string, unknown>[] | undefined;
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
declare class FieldBuilder {
|
|
@@ -118,7 +125,9 @@ declare const datetime: () => FieldBuilder;
|
|
|
118
125
|
declare const enumField: (values: [string, ...string[]]) => FieldBuilder;
|
|
119
126
|
declare const belongsTo: (entity: string) => FieldBuilder;
|
|
120
127
|
declare const hasMany: (entity: string) => FieldBuilder;
|
|
128
|
+
declare const object: (fields: Record<string, FieldBuilder>) => FieldBuilder;
|
|
129
|
+
declare const array: (item: FieldBuilder, count?: [number, number]) => FieldBuilder;
|
|
121
130
|
|
|
122
|
-
declare const __ffa_version = "0.
|
|
131
|
+
declare const __ffa_version = "0.16.0";
|
|
123
132
|
|
|
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 };
|
|
133
|
+
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
|
|
|
@@ -200,11 +201,19 @@ var datetime = () => new FieldBuilder("datetime");
|
|
|
200
201
|
var enumField = (values) => new FieldBuilder("enum", { enumValues: values });
|
|
201
202
|
var belongsTo = (entity2) => new FieldBuilder("belongsTo", { entity: entity2 });
|
|
202
203
|
var hasMany = (entity2) => new FieldBuilder("hasMany", { entity: entity2 });
|
|
204
|
+
var object = (fields) => {
|
|
205
|
+
const objectFields = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, v.build()]));
|
|
206
|
+
return new FieldBuilder("object", { objectFields });
|
|
207
|
+
};
|
|
208
|
+
var array = (item, count) => {
|
|
209
|
+
return new FieldBuilder("array", { arrayItem: item.build(), arrayCount: count });
|
|
210
|
+
};
|
|
203
211
|
|
|
204
212
|
// src/index.ts
|
|
205
|
-
var __ffa_version = "0.
|
|
213
|
+
var __ffa_version = "0.16.0";
|
|
206
214
|
export {
|
|
207
215
|
__ffa_version,
|
|
216
|
+
array,
|
|
208
217
|
belongsTo,
|
|
209
218
|
boolean,
|
|
210
219
|
datetime,
|
|
@@ -213,6 +222,7 @@ export {
|
|
|
213
222
|
enumField,
|
|
214
223
|
hasMany,
|
|
215
224
|
number,
|
|
225
|
+
object,
|
|
216
226
|
string,
|
|
217
227
|
uuid
|
|
218
228
|
};
|
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":"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","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(): 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\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 { string, number, boolean, uuid, datetime, enumField, belongsTo, hasMany, object, array } from './field/factories'\nexport type { FakeHint, StringFakeHint, NumberFakeHint, MetaFn, MetaValue } from './field/types'\n\nexport const __ffa_version = '0.16.0'\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.0",
|
|
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
|
}
|