@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/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) return HINT_MAP[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
- for (const [key, value] of Object.entries(options.filter)) {
244
- items = items.filter((item) => String(item[key]) === value);
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 c = {
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 `${c.cyan}${m}${c.reset}`;
708
+ return `${c2.cyan}${m}${c2.reset}`;
595
709
  case "POST":
596
- return `${c.green}${m}${c.reset}`;
710
+ return `${c2.green}${m}${c2.reset}`;
597
711
  case "PUT":
598
- return `${c.yellow}${m}${c.reset}`;
712
+ return `${c2.yellow}${m}${c2.reset}`;
599
713
  case "PATCH":
600
- return `${c.yellow}${m}${c.reset}`;
714
+ return `${c2.yellow}${m}${c2.reset}`;
601
715
  case "DELETE":
602
- return `${c.red}${m}${c.reset}`;
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 `${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}`;
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 = `${c.gray}${ms}ms${c.reset}`;
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 base = `/${pluralize2(entityName)}`;
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(base, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(base, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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(`${base}/:id`, (req, reply) => {
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 entityRows = Object.keys(config.entities).map((name) => {
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 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}`;
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 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}
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
- ${c.bold}FFA dev server${c.reset} ${c.gray}v0.6.0${c.reset}
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
- ${delayInfo} ${c.gray}\u2192${c.reset} Swagger UI ${c.cyan}http://localhost:${port}/docs${c.reset}
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.8.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
- return this.hint("image");
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.8.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":"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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(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":";AAeO,SAAS,aAAa,QAA8B;AACzD,SAAO;AAAA,IACL,QAAQ,OAAO,UAAU,EAAE,MAAM,IAAK;AAAA,IACtC,UAAU,OAAO,YAAY,CAAC;AAAA,EAChC;AACF;;;AClBO,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,IACf,MAAM,SAAS;AAAA,EACjB;AACF;;;ACdO,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,MAAM,OAAgB,QAAuB;AAC3C,SAAK,KAAK,OAAO;AACjB,QAAI,UAAU,OAAY,MAAK,IAAI,MAAM,aAAc;AACvD,QAAI,WAAW,OAAW,MAAK,IAAI,MAAM,cAAc;AACvD,WAAO;AAAA,EACT;AAAA,EACA,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;;;ACrIO,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;AAE1E,IAAM,SAAS,CAAC,WAAuD;AAC5E,QAAM,eAAe,OAAO,YAAY,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;AAC9F,SAAO,IAAI,aAAa,UAAU,EAAE,aAAa,CAAC;AACpD;AAEO,IAAM,QAAQ,CAAC,MAAoB,UAA2C;AACnF,SAAO,IAAI,aAAa,SAAS,EAAE,WAAW,KAAK,MAAM,GAAG,YAAY,MAAM,CAAC;AACjF;;;ACUO,IAAM,gBAAgB;","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.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
  }