@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/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
- for (const [key, value] of Object.entries(options.filter)) {
244
- items = items.filter((item) => String(item[key]) === value);
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 c = {
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 `${c.cyan}${m}${c.reset}`;
700
+ return `${c2.cyan}${m}${c2.reset}`;
595
701
  case "POST":
596
- return `${c.green}${m}${c.reset}`;
702
+ return `${c2.green}${m}${c2.reset}`;
597
703
  case "PUT":
598
- return `${c.yellow}${m}${c.reset}`;
704
+ return `${c2.yellow}${m}${c2.reset}`;
599
705
  case "PATCH":
600
- return `${c.yellow}${m}${c.reset}`;
706
+ return `${c2.yellow}${m}${c2.reset}`;
601
707
  case "DELETE":
602
- return `${c.red}${m}${c.reset}`;
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 `${c.green}${s}${c.reset}`;
610
- if (code < 400) return `${c.cyan}${s}${c.reset}`;
611
- if (code < 500) return `${c.yellow}${s}${c.reset}`;
612
- return `${c.red}${s}${c.reset}`;
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 = `${c.gray}${ms}ms${c.reset}`;
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 base = `/${pluralize2(entityName)}`;
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(base, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(base, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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 entityRows = Object.keys(config.entities).map((name) => {
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 url = `http://localhost:${port}/${pluralize2(name)}`;
735
- return ` ${c.gray}\u2192${c.reset} ${name.padEnd(16)} ${c.gray}${String(count).padStart(3)} records${c.reset} ${c.cyan}${url}${c.reset}`;
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 delayInfo = delayConfig !== void 0 ? Array.isArray(delayConfig) ? ` ${c.gray}delay: ${delayConfig[0]}\u2013${delayConfig[1]}ms${c.reset}
738
- ` : ` ${c.gray}delay: ${delayConfig}ms${c.reset}
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
- ${c.bold}FFA dev server${c.reset} ${c.gray}v0.6.0${c.reset}
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
- ${delayInfo} ${c.gray}\u2192${c.reset} Swagger UI ${c.cyan}http://localhost:${port}/docs${c.reset}
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.8.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.8.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.8.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
  }