fakelab 0.0.18 โ†’ 0.0.20

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - ๐Ÿš€ Instant mock server
8
8
  - ๐Ÿ—‚๏ธ Mock from Typescript files
9
- - ๐Ÿ“ฆ Lightweight ~ 665 kB
9
+ - ๐Ÿ“ฆ Lightweight
10
10
  - ๐Ÿงช Perfect for local development, prototyping, and frontend testing
11
11
 
12
12
  ## Installation
@@ -88,9 +88,9 @@ export interface User {
88
88
 
89
89
  ## Fakelab Runtime
90
90
 
91
- `fakelab/runtime` enables `fakelab` module at runtime, allowing your frontend or Node environment to communicate with the running Fakelab mock server.
91
+ `fakelab/browser` enables `fakelab` module at runtime, allowing your frontend or Node environment to communicate with the running Fakelab mock server.
92
92
 
93
- ## `fakelab.url()`
93
+ ## `fakelab.url`
94
94
 
95
95
  The base URL of the running Fakelab server.
96
96
 
@@ -119,7 +119,7 @@ fakelab.fetch(name: string, count?: number): Promise<T>
119
119
  ### Basic example
120
120
 
121
121
  ```ts
122
- import { fakelab } from "fakelab/runtime";
122
+ import { fakelab } from "fakelab/browser";
123
123
 
124
124
  const users = await fakelab.fetch("User", 10);
125
125
 
@@ -128,7 +128,7 @@ console.log(users);
128
128
  // or
129
129
 
130
130
  // can be enabled as a global object
131
- import "fakelab/runtime";
131
+ import "fakelab/browser";
132
132
 
133
133
  const users = await fakelab.fetch("User", 10);
134
134
 
@@ -159,6 +159,52 @@ export type DatabaseOptions = {
159
159
  export default defineConfig({
160
160
  database: { enabled: true, dest: "db" },
161
161
  });
162
+
163
+ import { database } from "fakelab/browser";
164
+
165
+ const users = await database.get("User");
166
+
167
+ console.log(users);
168
+
169
+ // or insert fresh data to database
170
+ await database.post("User");
171
+ ```
172
+
173
+ ### Database Seeding
174
+
175
+ Fakelab supports database seeding to initialize mock data.
176
+
177
+ ```ts
178
+ type SeedOptions = {
179
+ count?: number;
180
+ strategy?: "reset" | "once" | "merge";
181
+ };
182
+ ```
183
+
184
+ ### Options
185
+
186
+ | Name | Type | Description |
187
+ | ---------- | ------------------------ | ----------------------------------------------------------------------------- |
188
+ | `count` | `number` | Number of records to generate |
189
+ | `strategy` | `reset`, `once`, `merge` | Defines how seeding interacts with existing database data. default is `reset` |
190
+
191
+ - `reset`: Removes all existing data and recreates it from scratch.
192
+ - `once`: Seeds data only if the database is empty.
193
+ - `merge`: Inserts new records and updates existing ones. The total number of items per table is limited to `1000` records.
194
+
195
+ ### Basic example
196
+
197
+ ```ts
198
+ export default defineConfig({
199
+ database: { enabled: true, dest: "db" },
200
+ });
201
+
202
+ import { database } from "fakelab/browser";
203
+
204
+ await database.seed("User", { count: 10, strategy: "once" });
205
+
206
+ // to flush the database
207
+ await database.flush("User");
162
208
  ```
163
209
 
164
210
  ## Network Simulation
@@ -231,7 +277,3 @@ npx fakelab serve --pathPrefix /v1 --locale fr
231
277
  ## Related
232
278
 
233
279
  Fakelab is powered by [Fakerjs](https://fakerjs.dev/) library.
234
-
235
- ## License
236
-
237
- [MIT](https://choosealicense.com/licenses/mit/)
package/lib/cli.js CHANGED
@@ -1,7 +1,9 @@
1
- import g from'path';import O from'fs-extra';import {Command}from'commander';import R from'express';import H from'cors';import K from'express-ejs-layouts';import X from'http';import'ejs';import {Project}from'ts-morph';import d from'winston';import b from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import U from'qs';import V from'figlet';import {bundleRequire}from'bundle-require';import se from'joycon';function S(s){switch(s){case "info":return b.blueBright(s.toUpperCase());case "error":return b.redBright(s.toUpperCase());case "warn":return b.yellowBright(s.toUpperCase());default:return s.toUpperCase()}}var j=d.format.printf(({level:s,message:e,timestamp:t})=>`${b.dim(`[${t}]`)} ${S(s)} ${e}`),m=d.createLogger({format:d.format.combine(d.format.timestamp(),d.format.splat(),j),transports:[new d.transports.Console]}),I=new Intl.ListFormat("en",{style:"long",type:"unit"}),c=class{static info(e,...t){return m.info(e,...t)}static warn(e,...t){return m.warn(e,...t)}static error(e,...t){return m.error(e,...t)}static debug(e,...t){if(!(typeof process>"u"||!process.env.DEBUG))return m.info(e,...t)}static list(e){return I.format(e)}static close(){m.removeAllListeners(),m.close();}};var w=class{constructor(e){this.faker=e;}JSDOC_FAKER_FIELD="faker";FAKER_TAG_REGEX=/^([a-zA-Z0-9._]+)(?:\((.*)\))?$/;boolMapping={true:true,false:false};string(e){return this.execute(e,this.faker.word.noun)}int(e){return this.execute(e,this.faker.number.int)}bool(e){return this.execute(e,this.faker.datatype.boolean)}bigInt(e){return this.execute(e,this.faker.number.bigInt)}litbool(e){return this.boolMapping[e]}async object(e,t){let r={};return await Promise.all(e.map(async o=>{let i=o.getTypeAtLocation(o.getValueDeclarationOrThrow());r[o.getName()]=await t(i,this,this.readJSDocTags(o));})),r}async union(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}async intersection(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}evalArgs(e){if(!(!e||!e.trim()))return Function(`"use strict"; return (${e});`)()}readJSDocTags(e){let t=e.getJsDocTags().filter(r=>r.getName()===this.JSDOC_FAKER_FIELD);return t.length===0?[]:t.map(r=>{let[o]=r.getText();if(!o)return;let i=o.text.trim().match(this.FAKER_TAG_REGEX);if(!i)return;let[,n,a]=i,p=this.evalArgs(a);return {path:n,args:p}})}execute(e,t){if(!e)return t();let r=e.path.split("."),o=this.faker;for(let i of r)o=o[i],o||(c.error("Invalid faker module path:",e.path),process.exit(1));typeof o!="function"&&(c.error("Unresolvable faker function.",e.path),process.exit(1));try{return e.args?o(e.args):o()}catch{return c.error("Passed invalid arguments to faker function."),o()}}};var u=g.dirname(fileURLToPath(import.meta.url)),F=process.cwd();var k=class{constructor(e,t){this.files=e;this.config=t;let o=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(e);this.__targets=o.flatMap(i=>{let n=i.getInterfaces(),a=i.getTypeAliases(),p=i.getExportDeclarations().flatMap(l=>l.getNamedExports().flatMap(f=>f.getLocalTargetDeclarations()));return [...n,...a,...p]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(e){return await e()}normalizePath(e){return e.split(g.sep).join(g.posix.sep)}generateInFileEntitiyMap(e){let t=[...new Set(e.map(i=>{let n=i.getName(),a=i.getSourceFile().getFilePath();return `${n}: import("${a}").${n}`}))],{expose:r}=this.config.browserOpts(),o=`
1
+ import y from'path';import T from'fs-extra';import {Command}from'commander';import P from'express';import Z from'cors';import ee from'express-ejs-layouts';import te from'http';import {Project}from'ts-morph';import m from'winston';import w from'picocolors';import {JSONFilePreset}from'lowdb/node';import {fileURLToPath}from'url';import H from'qs';import re from'figlet';import {bundleRequire}from'bundle-require';import X from'joycon';function _(o){switch(o){case "info":return w.blueBright(o.toUpperCase());case "error":return w.redBright(o.toUpperCase());case "warn":return w.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var $=m.format.printf(({level:o,message:e,timestamp:t})=>`${w.dim(`[${t}]`)} ${_(o)} ${e}`),f=m.createLogger({format:m.format.combine(m.format.timestamp(),m.format.splat(),$),transports:[new m.transports.Console]}),M=new Intl.ListFormat("en",{style:"long",type:"unit"}),p=class{static info(e,...t){return f.info(e,...t)}static warn(e,...t){return f.warn(e,...t)}static error(e,...t){return f.error(e,...t)}static debug(e,...t){if(!(typeof process>"u"||!process.env.DEBUG))return f.info(e,...t)}static list(e){return M.format(e)}static close(){f.removeAllListeners(),f.close();}};var v=class{constructor(e){this.faker=e;}JSDOC_FAKER_FIELD="faker";FAKER_TAG_REGEX=/^([a-zA-Z0-9._]+)(?:\((.*)\))?$/;boolMapping={true:true,false:false};string(e){return this.execute(e,this.faker.word.noun)}int(e){return this.execute(e,this.faker.number.int)}bool(e){return this.execute(e,this.faker.datatype.boolean)}bigInt(e){return this.execute(e,this.faker.number.bigInt)}litbool(e){return this.boolMapping[e]}async object(e,t){let r={};return await Promise.all(e.map(async s=>{let a=s.getTypeAtLocation(s.getValueDeclarationOrThrow());r[s.getName()]=await t(a,this,this.readJSDocTags(s));})),r}async union(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}async intersection(e){let t=await Promise.all(e);return t[Math.floor(Math.random()*t.length)]}evalArgs(e){if(!(!e||!e.trim()))return Function(`"use strict"; return (${e});`)()}readJSDocTags(e){let t=e.getJsDocTags().filter(r=>r.getName()===this.JSDOC_FAKER_FIELD);return t.length===0?[]:t.map(r=>{let[s]=r.getText();if(!s)return;let a=s.text.trim().match(this.FAKER_TAG_REGEX);if(!a)return;let[,i,n]=a,c=this.evalArgs(n);return {path:i,args:c}})}execute(e,t){if(!e)return t();let r=e.path.split("."),s=this.faker;for(let a of r)s=s[a],s||(p.error("Invalid faker module path: (%s)",e.path),process.exit(1));typeof s!="function"&&(p.error("Unresolvable faker function. (%s)",e.path),process.exit(1));try{return e.args?s(e.args):s()}catch{return p.error("Passed invalid arguments to faker function."),s()}}};var d=y.dirname(fileURLToPath(import.meta.url)),b=process.cwd();var x=class{constructor(e,t,r){this.files=e;this.config=t;this.database=r;let a=new Project({tsConfigFilePath:"tsconfig.json"}).addSourceFilesAtPaths(e);this.__targets=a.flatMap(i=>{let n=i.getInterfaces(),c=i.getTypeAliases(),l=i.getExportDeclarations().flatMap(u=>u.getNamedExports().flatMap(g=>g.getLocalTargetDeclarations()));return [...n,...c,...l]}),this.generateInFileEntitiyMap(this.__targets);}__targets;async run(e){return await e()}normalizePath(e){return e.split(y.sep).join(y.posix.sep)}generateInFileEntitiyMap(e){let t=[...new Set(e.map(a=>{let i=a.getName(),n=a.getSourceFile().getFilePath();return `${i}: import("${n}").${i}`}))],{expose:r}=this.config.browserOpts(),s=`
2
2
  interface Runtime$ {
3
3
  ${t.join(`
4
4
  `)}
5
- }`;r.mode==="global"&&(o=`
6
- declare global {${o}
7
- }`),O.appendFile(g.resolve(u,this.config.RUNTIME_DECL_FILENAME),o);}address(e,t){let r=this.normalizePath(F);return `${e.replace(r,"")}/${t}`}async entities(){let e=await Promise.all(this.__targets.map(async t=>{let r=t.getName().toLowerCase(),o=t.getType(),i=this.normalizePath(t.getSourceFile().getDirectoryPath()),n=t.getSourceFile().getBaseName(),a=this.address(i,n),p=this.config.getDatabaseDirectoryPath(),l=g.resolve(p,`${r}.json`),f=this.address(this.normalizePath(p),g.basename(l)),y=await JSONFilePreset(l,[]);return [r,{type:o,filepath:a,table:y,tablepath:f}]}));return new Map(e)}async initFakerLibrary(e){let{faker:t}=await import(`@faker-js/faker/locale/${e.locale}`);return t}};async function h(s,e,t=[],r=0){if(s.isString())return e.string(t[r]);if(s.isNumber())return e.int(t[r]);if(s.isBoolean())return e.bool(t[r]);if(s.isBigInt())return e.bigInt(t[r]);if(s.isBooleanLiteral())return e.litbool(s.getText());if(s.isLiteral())return s.getLiteralValue();if(!s.isUndefined()){if(s.isUnion()){let o=s.getUnionTypes();return await e.union(o.map((i,n)=>h(i,e,t,n)))}if(s.isIntersection()){let o=s.getIntersectionTypes();return await e.intersection(o.map((i,n)=>h(i,e,t,n)))}if(s.isArray()){let o=s.getArrayElementTypeOrThrow();return [await h(o,e,t,r)]}if(s.isObject()){let o=s.getProperties();return await e.object(o,(i,n,a)=>h(i,n,a,r))}return null}}function J({each:s}){return {resolve:async t=>await Promise.all(Array.from({length:t},s))}}async function A(s,e){let t=await s.files(e.source),r=new k(t,s),o=await r.entities(),i=await r.initFakerLibrary(s.fakerOpts(e.locale)),n=new w(i);async function a(p,l){let f=J({each:()=>h(p,n)}),y=await(l.count?f.resolve(parseInt(l.count)):h(p,n)),D=JSON.stringify(y,null,2);return {data:y,json:D}}return {entities:o,build:a}}var v=class{constructor(e,t,r){this.builder=e;this.config=t;this.pkg=r;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}index(){return (e,t)=>{t.render("index",{name:null,entities:this.builder.entities,version:this.pkg.version,enabled:this.config.databaseEnabled()});}}preview(e){return async(t,r)=>{let o=`${t.protocol}://${t.host}/`,i=t.params.name,n=await this.handleQueries(t),a=U.stringify(n,{addQueryPrefix:true}),p=this.builder.entities.get(i.toLowerCase());if(p){let{json:l}=await this.builder.build(p.type,n),f=p.filepath;r.render("preview",{name:i,filepath:f,address:o,search:a,json:l,prefix:e,entities:this.builder.entities,version:this.pkg.version,enabled:this.config.databaseEnabled()});}else r.redirect("/");}}database(){return (e,t)=>{this.config.databaseEnabled()?t.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):t.redirect("/");}}table(e){return async(t,r)=>{let o=`${t.protocol}://${t.host}/`,i=t.params.name,n=this.builder.entities.get(i.toLowerCase());if(!this.config.databaseEnabled())r.redirect("/");else if(n){await n.table.read();let p=n.table.data.length>0,l=JSON.stringify(n.table.data,null,2),f=n.filepath;r.render("table",{name:i,filepath:f,address:o,prefix:e,json:l,hasData:p,entities:this.builder.entities,version:this.pkg.version});}else r.redirect("/database");}}};var x=class{constructor(e,t){this.builder=e;this.network=t;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}async applyNetworkHandlers(e){if(this.network.offline()){let{status:t,message:r}=this.network.state("offline");return e.status(t).json({message:r}),true}if(await this.network.wait(),this.network.timeout())return true;if(this.network.error()){let{status:t,message:r}=this.network.state("error");return e.status(t).json({message:r}),true}return false}entity(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,o=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,o);t.status(200).json(n);}else t.status(400).json({message:"The entity is not exists"});}catch(r){t.status(500).send(r);}}}getTable(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,o=this.builder.entities.get(r.toLowerCase());o?(await o.table.read(),t.status(200).json(o.table.data)):t.status(400).json({message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}updateTable(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,o=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,o);await i.table.update(a=>a.push(n)),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}_update(){return async(e,t)=>{try{let r=e.params.name,o=await this.handleQueries(e),i=this.builder.entities.get(r.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,o);await i.table.update(a=>a.push(n)),t.status(301).redirect(`/database/${r.toLowerCase()}`);}else t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}_clear(){return async(e,t)=>{try{let r=e.params.name,o=this.builder.entities.get(r.toLowerCase());o?(await o.table.read(),o.table.data.length>0&&await o.table.update(i=>i.length=0),t.status(301).redirect(`/database/${r.toLowerCase()}`)):t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}};var G=O.readJSONSync(g.join(u,"../package.json")),C=class{constructor(e,t,r,o){this.router=e;this.config=t;this.opts=r;this.network=o;let{pathPrefix:i}=this.config.serverOpts(this.opts.pathPrefix,this.opts.port);this.prefix=i;}prefix;async register(){let e=await A(this.config,this.opts),t=new v(e,this.config,G),r=new x(e,this.network);this.router.get("/",t.index()),this.router.get("/entities/:name",t.preview(this.prefix)),this.router.get("/database",t.database()),this.router.get("/database/:name",t.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,r.entity()),this.router.get(`/${this.prefix}/database/:name`,r.getTable()),this.router.post(`/${this.prefix}/database/:name`,r.updateTable()),this.router.post("/__update/:name",r._update()),this.router.post("/__delete/:name",r._clear());}};var T=class s{constructor(e){this.config=e;this.options=this.config.networkOpts(),this.timeout=this.timeout.bind(this),this.error=this.error.bind(this),this.state=this.state.bind(this),this.middleware=this.middleware.bind(this),this.wait=this.wait.bind(this),this.offline=this.offline.bind(this);}options;static initHandlers(e){return new s(e)}timeout(){let e=this.chance(this.options?.timeoutRate);return e&&c.debug("Network timeout..."),e}error(){let e=this.chance(this.options?.errorRate);return e&&c.debug("Network error..."),e}state(e){switch(e){case "error":return {status:500,message:"Network error"};case "offline":return {status:503,message:"Network offline"};default:return {status:500,message:"Unknown error"}}}async wait(){let e=this.resolveDelay();e>0&&(c.debug("Network waiting (%d ms)...",e),await this.sleep(e));}offline(){return this.options?.offline??false}middleware(e,t,r){let o=this.options?.errorRate||0,i=this.options?.timeoutRate||0,n=this.options?.offline??false,a=`delay=${this.resolveDelay()},error=${o},timeout=${i},offline=${n}`;t.setHeader("X-Fakelab-Network",a),r();}resolveDelay(){let e=this.options?.delay;return typeof e=="number"?Math.round(e):Array.isArray(e)?this.random(e):0}chance(e=0){return Math.random()<e}random([e,t]){return Math.round(Math.random()*(t-e)+e)}sleep(e){return new Promise(t=>setTimeout(t,e))}};function W(s,e){try{process.loadEnvFile("./.env.local");}catch{}s.databaseEnabled()&&c.info(`database: ${s.getDatabaseDirectoryPath()}`),c.info(`server: http://localhost:${e}`),console.log(V.textSync("FAKELAB"));}function Z(s,e,t){let{port:r}=e.serverOpts(t.pathPrefix,t.port);s.listen(r,"localhost",()=>W(e,r)),s.on("close",()=>{c.close();});}function Y(s,e,t){e.setHeader("x-powered-by","fakelab"),t();}function ee(s,e){s.disable("x-powered-by"),s.use(R.json()),s.use(H({methods:"GET"})),s.use(R.static(u+"/public")),s.use(Y),s.use(e.middleware);}function te(s){s.set("views",g.join(u,"views")),s.set("view engine","ejs"),s.use(K),s.set("layout","layouts/main");}async function L(s,e){let t=R(),r=R.Router(),o=X.createServer(t),i=T.initHandlers(s);ee(t,i),te(t),await s.generateInFileRuntimeConfig(u,e),await new C(r,s,e,i).register(),t.use(r),Z(o,s,e);}async function N(){try{let e=await new se().resolve({files:["fakelab.config.ts"]});return e||(c.error("No fakelab config file is detected."),process.exit(1)),(await bundleRequire({filepath:e})).mod.default}catch{c.error("Could not load the config file."),process.exit(1);}}var P=new Command,E=O.readJSONSync(g.join(u,"../package.json"));P.name(E.name).description(E.description).version(E.version);P.command("serve").description("start server").option("-s, --source <char>","config source path").option("-x, --pathPrefix <char>","server url path prefix").option("-p, --port <number>","server port number",parseInt).option("-l, --locale <char>","faker custom locale").action(async s=>{let e=await N();L(e,s);});P.parse();
5
+ }`;r.mode==="global"&&(s=`
6
+ declare global {${s}
7
+ }`),T.appendFile(y.resolve(d,this.config.RUNTIME_DECL_FILENAME),s);}address(e,t){let r=this.normalizePath(b);return `${e.replace(r,"")}/${t}`}async entities(){let e=await Promise.all(this.__targets.map(async t=>{let r=t.getName().toLowerCase(),s=t.getType(),a=this.normalizePath(t.getSourceFile().getDirectoryPath()),i=t.getSourceFile().getBaseName(),n=this.address(a,i),c=this.database.directoryPath(),l=y.resolve(c,`${r}.json`),u=this.address(this.normalizePath(c),y.basename(l)),g=await JSONFilePreset(l,[]);return [r,{type:s,filepath:n,table:g,tablepath:u}]}));return new Map(e)}async initFakerLibrary(e){let{faker:t}=await import(`@faker-js/faker/locale/${e.locale}`);return t}};async function h(o,e,t=[],r=0){if(o.isString())return e.string(t[r]);if(o.isNumber())return e.int(t[r]);if(o.isBoolean())return e.bool(t[r]);if(o.isBigInt())return e.bigInt(t[r]);if(o.isBooleanLiteral())return e.litbool(o.getText());if(o.isLiteral())return o.getLiteralValue();if(!o.isUndefined()){if(o.isUnion()){let s=o.getUnionTypes();return await e.union(s.map((a,i)=>h(a,e,t,i)))}if(o.isIntersection()){let s=o.getIntersectionTypes();return await e.intersection(s.map((a,i)=>h(a,e,t,i)))}if(o.isArray()){let s=o.getArrayElementTypeOrThrow();return [await h(s,e,t,r)]}if(o.isObject()){let s=o.getProperties();return await e.object(s,(a,i,n)=>h(a,i,n,r))}return null}}function G({each:o}){return {resolve:async t=>await Promise.all(Array.from({length:t},o))}}async function N(o,e,t){let r=await o.files(e.source),s=new x(r,o,t),a=await s.entities(),i=await s.initFakerLibrary(o.fakerOpts(e.locale)),n=new v(i);async function c(l,u){let g=G({each:()=>h(l,n)}),j=await(u.count?g.resolve(parseInt(u.count)):h(l,n)),I=JSON.stringify(j,null,2);return {data:j,json:I}}return {entities:a,build:c}}var k=class{constructor(e,t,r){this.builder=e;this.database=t;this.pkg=r;}async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}index(){return (e,t)=>{t.render("index",{name:null,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}}preview(e){return async(t,r)=>{let s=`${t.protocol}://${t.host}/`,a=t.params.name,i=await this.handleQueries(t),n=H.stringify(i,{addQueryPrefix:true}),c=this.builder.entities.get(a.toLowerCase());if(c){let{json:l}=await this.builder.build(c.type,i),u=c.filepath;r.render("preview",{name:a,filepath:u,address:s,search:n,json:l,prefix:e,entities:this.builder.entities,version:this.pkg.version,enabled:this.database.enabled()});}else r.redirect("/");}}db(){return (e,t)=>{this.database.enabled()?t.render("database",{name:null,entities:this.builder.entities,version:this.pkg.version}):t.redirect("/");}}table(e){return async(t,r)=>{let s=`${t.protocol}://${t.host}/`,a=t.params.name,i=this.builder.entities.get(a.toLowerCase());if(!this.database.enabled())r.redirect("/");else if(i){await i.table.read();let c=i.table.data.length>0,l=JSON.stringify(i.table.data,null,2),u=i.filepath;r.render("table",{name:a,filepath:u,address:s,prefix:e,json:l,hasData:c,entities:this.builder.entities,version:this.pkg.version});}else r.redirect("/database");}}};var E=class{constructor(e,t,r){this.builder=e;this.network=t;this.database=r;}SEED_MERGE_THRESHOLD=1e3;async handleQueries(e){let t=e.query.count;return t?{count:t.toString()}:{}}async applyNetworkHandlers(e){if(this.network.offline()){let{status:t,message:r}=this.network.state("offline");return e.status(t).json({message:r}),true}if(await this.network.wait(),this.network.timeout())return true;if(this.network.error()){let{status:t,message:r}=this.network.state("error");return e.status(t).json({message:r}),true}return false}entity(){return async(e,t)=>{try{if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=await this.handleQueries(e),a=this.builder.entities.get(r.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,s);t.status(200).json(i);}else t.status(400).json({message:"The entity is not exists"});}catch(r){t.status(500).send(r);}}}getTable(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=this.builder.entities.get(r.toLowerCase());s?(await s.table.read(),t.status(200).json(s.table.data)):t.status(400).json({message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}updateTable(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});if(await this.applyNetworkHandlers(t))return;let r=e.params.name,s=await this.handleQueries(e),a=this.builder.entities.get(r.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,s);await a.table.update(n=>n.push(...Array.isArray(i)?i:[i])),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}insert(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let r=e.body?.count||1,s=e.body?.strategy||"reset",a=e.params.name,i=this.builder.entities.get(a.toLowerCase());if(i){let{data:n}=await this.builder.build(i.type,{count:r});if(await i.table.read(),s==="once"&&i.table.data.length>0)return t.status(200).json({message:`${a} entity was seeded once before.`});await i.table.update(c=>{s!=="merge"&&(c.length=0);let l=Array.isArray(n)?n:[n];c.length+l.length<this.SEED_MERGE_THRESHOLD&&c.push(...l);}),t.status(200).json({success:!0});}else t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){t.status(500).send(r);}}}flush(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let r=e.params.name,s=this.builder.entities.get(r.toLowerCase());s?(await s.table.update(a=>a.length=0),t.status(200).json({success:!0})):t.status(400).json({success:!1,message:"The table is not exists"});}catch(r){console.log({error:r}),t.status(500).send(r);}}}_update(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let r=e.params.name,s=await this.handleQueries(e),a=this.builder.entities.get(r.toLowerCase());if(a){let{data:i}=await this.builder.build(a.type,s);await a.table.update(n=>n.push(i)),t.status(301).redirect(`/database/${r.toLowerCase()}`);}else t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}_clear(){return async(e,t)=>{try{if(!this.database.enabled())return t.status(403).json({message:"database is not enabled or initialized."});let r=e.params.name,s=this.builder.entities.get(r.toLowerCase());s?(await s.table.read(),s.table.data.length>0&&await s.table.update(a=>a.length=0),t.status(301).redirect(`/database/${r.toLowerCase()}`)):t.status(400).redirect("/database");}catch{t.status(500).redirect("/database");}}}};var V=T.readJSONSync(y.join(d,"../package.json")),C=class{constructor(e,t,r,s,a){this.router=e;this.config=t;this.network=r;this.database=s;this.opts=a;let{pathPrefix:i}=this.config.serverOpts(this.opts.pathPrefix,this.opts.port);this.prefix=i;}prefix;async register(){let e=await N(this.config,this.opts,this.database),t=new k(e,this.database,V),r=new E(e,this.network,this.database);this.router.get("/",t.index()),this.router.get("/entities/:name",t.preview(this.prefix)),this.router.get("/database",t.db()),this.router.get("/database/:name",t.table(this.prefix)),this.router.get(`/${this.prefix}/:name`,r.entity()),this.router.get(`/${this.prefix}/database/:name`,r.getTable()),this.router.post(`/${this.prefix}/database/:name`,r.updateTable()),this.router.post(`/${this.prefix}/database/insert/:name`,r.insert()),this.router.post(`/${this.prefix}/database/flush/:name`,r.flush()),this.router.post("/__update/:name",r._update()),this.router.post("/__delete/:name",r._clear());}};var R=class o{constructor(e){this.config=e;this.options=this.config.networkOpts(),this.timeout=this.timeout.bind(this),this.error=this.error.bind(this),this.state=this.state.bind(this),this.middleware=this.middleware.bind(this),this.wait=this.wait.bind(this),this.offline=this.offline.bind(this);}options;static initHandlers(e){return new o(e)}timeout(){let e=this.chance(this.options?.timeoutRate);return e&&p.debug("Network timeout..."),e}error(){let e=this.chance(this.options?.errorRate);return e&&p.debug("Network error..."),e}state(e){switch(e){case "error":return {status:500,message:"Network error"};case "offline":return {status:503,message:"Network offline"};default:return {status:500,message:"Unknown error"}}}async wait(){let e=this.resolveDelay();e>0&&(p.debug("Network waiting (%d ms)...",e),await this.sleep(e));}offline(){return this.options?.offline??false}middleware(e,t,r){let s=this.options?.errorRate||0,a=this.options?.timeoutRate||0,i=this.options?.offline??false,n=`delay=${this.resolveDelay()},error=${s},timeout=${a},offline=${i}`;t.setHeader("X-Fakelab-Network",n),r();}resolveDelay(){let e=this.options?.delay;return typeof e=="number"?Math.round(e):Array.isArray(e)?this.random(e):0}chance(e=0){return Math.random()<e}random([e,t]){return Math.round(Math.random()*(t-e)+e)}sleep(e){return new Promise(t=>setTimeout(t,e))}};async function S(){try{let e=await new X().resolve({files:["fakelab.config.ts"]});return e||(p.error("No fakelab config file is detected."),process.exit(1)),(await bundleRequire({filepath:e})).mod.default}catch{p.error("Could not load the config file."),process.exit(1);}}var D=class o{constructor(e){this.config=e;this.enabled=this.enabled.bind(this),this.directoryPath=this.directoryPath.bind(this),this.options=this.config.databaseOpts(),this.options.enabled||T.rmSync(this.directoryPath(),{force:true,recursive:true});}options;static register(e){return new o(e)}enabled(){return this.options.enabled??false}directoryPath(){return y.resolve(b,this.options.dest)}async initialize(){if(this.enabled())try{await T.ensureDir(this.directoryPath()),await this.modifyGitignoreFile(this.options.dest);}catch{p.error("Could not create database.");}}async modifyGitignoreFile(e){try{let t=y.resolve(b,".gitignore");if((await T.readFile(t,{encoding:"utf8"})).split(`
8
+ `).some(s=>s.trim()===e.trim()))return;await T.appendFile(t,`
9
+ ${e}`);}catch{}}};var L=class o{constructor(e){this.options=e;this.start=this.start.bind(this),this.xPoweredMiddleware=this.xPoweredMiddleware.bind(this),this.setupApplication=this.setupApplication.bind(this),this.setupTemplateEngine=this.setupTemplateEngine.bind(this),this.loadLocalEnv();}static init(e){return new o(e)}async start(){let e=P(),t=P.Router(),r=te.createServer(e),s=await S(),a=R.initHandlers(s),i=D.register(s);this.setupApplication(e,a),this.setupTemplateEngine(e),await s.generateInFileRuntimeConfig(d,this.options),await i.initialize(),await new C(t,s,a,i,this.options).register(),e.use(t),this.run(r,s,i,this.options);}setupApplication(e,t){e.disable("x-powered-by"),e.use(P.json()),e.use(Z({methods:"GET"})),e.use(P.static(d+"/public")),e.use(this.xPoweredMiddleware),e.use(t.middleware);}setupTemplateEngine(e){e.set("views",y.join(d,"views")),e.set("view engine","ejs"),e.use(ee),e.set("layout","layouts/main");}listen(e,t){e.enabled()&&p.info("database: %s",e.directoryPath()),p.info("server: http://localhost:%d",t),console.log(re.textSync("FAKELAB"));}run(e,t,r,s){let{port:a}=t.serverOpts(s.pathPrefix,s.port);e.listen(a,"localhost",()=>this.listen(r,a)),e.on("close",()=>{p.close();});}xPoweredMiddleware(e,t,r){t.setHeader("x-powered-by","fakelab"),r();}loadLocalEnv(){}};var F=new Command,A=T.readJSONSync(y.join(d,"../package.json"));F.name(A.name).description(A.description).version(A.version);F.command("serve").description("start server").option("-s, --source <char>","config source path").option("-x, --pathPrefix <char>","server url path prefix").option("-p, --port <number>","server port number",parseInt).option("-l, --locale <char>","faker custom locale").action(async o=>{L.init(o).start();});F.parse();
package/lib/main.d.ts CHANGED
@@ -3,46 +3,112 @@ type FakerLocale = (typeof FAKER_LOCALES)[number];
3
3
 
4
4
  type ServerOptions = {
5
5
  /**
6
+ * Port number that the mock server will listen on.
6
7
  * @default 5200
7
8
  */
8
9
  port?: number;
9
10
  /**
11
+ * URL path prefix used for all generated endpoints.
10
12
  * @default `api`
11
13
  */
12
14
  pathPrefix?: string;
13
15
  };
14
16
  type FakerEngineOptions = {
17
+ /**
18
+ * Locale used by the faker engine when generating mock data.
19
+ * Controls language-specific values such as names, addresses, etc.
20
+ */
15
21
  locale?: FakerLocale;
16
22
  };
17
23
  type DatabaseOptions = {
24
+ /**
25
+ * Enables persistent storage for mock data.
26
+ * When enabled, `POST` mutation will be stored.
27
+ */
18
28
  enabled: boolean;
29
+ /**
30
+ * Destination directory for the local database files.
31
+ * @default `<ROOT>/db`
32
+ */
19
33
  dest?: string;
20
34
  };
21
35
  type BrowserExposeOptions = {
36
+ /**
37
+ * Name of the exposed object or module in the browser.
38
+ * @example `fakelab`
39
+ */
22
40
  name: string;
41
+ /**
42
+ * Exposure mode in the browser environment.
43
+ * - `"module"`: Exposed as an ES module
44
+ * - `"global"`: Attached to the global window object
45
+ */
23
46
  mode: "module" | "global";
24
47
  };
25
48
  type BrowserOptions = {
49
+ /**
50
+ * Controls how the runtime API is exposed in the browser.
51
+ */
26
52
  expose?: Partial<BrowserExposeOptions>;
27
53
  };
28
54
  type NetworkBehaviourOptions = {
55
+ /**
56
+ * Artificial response delay in milliseconds.
57
+ * Can be a fixed number or a range `[min, max]`.
58
+ * @example 300 or [200, 800]
59
+ */
29
60
  delay?: number | [number, number];
61
+ /**
62
+ * Probability (0โ€“1) that a request will fail with an error response.
63
+ * @example `0.1` โ†’ 10% error rate
64
+ */
30
65
  errorRate?: number;
66
+ /**
67
+ * Probability (0โ€“1) that a request will timeout.
68
+ * @example `0.05` โ†’ 5% timeout rate
69
+ */
31
70
  timeoutRate?: number;
71
+ /**
72
+ * When enabled, all requests will behave as if the network is offline.
73
+ */
32
74
  offline?: boolean;
33
75
  };
34
76
  type NetworkOptions = NetworkBehaviourOptions & {
77
+ /**
78
+ * Name of the active network preset.
79
+ */
35
80
  preset?: string;
81
+ /**
82
+ * Collection of predefined network behavior presets.
83
+ * Each preset can simulate different network conditions.
84
+ */
36
85
  presets?: Record<string, NetworkBehaviourOptions>;
37
86
  };
38
87
  type ConfigOptions = {
88
+ /**
89
+ * Path or paths to the source files that define the typescript types.
90
+ */
39
91
  sourcePath: string | string[];
92
+ /**
93
+ * Server-related configuration.
94
+ */
40
95
  server?: ServerOptions;
96
+ /**
97
+ * Faker engine configuration for data generation.
98
+ */
41
99
  faker?: FakerEngineOptions;
100
+ /**
101
+ * Database persistence configuration.
102
+ */
42
103
  database?: DatabaseOptions;
104
+ /**
105
+ * Browser runtime exposure options.
106
+ */
43
107
  browser?: BrowserOptions;
108
+ /**
109
+ * Network simulation configuration.
110
+ */
44
111
  network?: NetworkOptions;
45
- transform?: Record<string, (data: any) => any | Promise<any>>;
46
112
  };
47
113
  type ServerCLIOptions = {
48
114
  source?: string;
@@ -61,13 +127,10 @@ declare class Config {
61
127
  files(_sourcePath?: string): Promise<string[]>;
62
128
  serverOpts(prefix?: string, port?: number): Required<ServerOptions>;
63
129
  browserOpts(name?: string, mode?: "module" | "global"): Required<BrowserOptions>;
130
+ databaseOpts(): Required<DatabaseOptions>;
64
131
  networkOpts(): NetworkOptions;
65
132
  fakerOpts(locale?: FakerLocale): Required<FakerEngineOptions>;
66
133
  generateInFileRuntimeConfig(dirname: string, options: ServerCLIOptions): Promise<void>;
67
- getDatabaseDirectoryPath(): string;
68
- databaseEnabled(): boolean;
69
- private initializeDatabase;
70
- private modifyGitignoreFile;
71
134
  private tryStat;
72
135
  private isReadable;
73
136
  private resolveSourcePath;
package/lib/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import N from'fast-glob';import i from'path';import d from'fs-extra';import {stat,access,constants}from'fs/promises';import F from'is-glob';import p from'winston';import u from'picocolors';import {transform}from'esbuild';function M(a){switch(a){case "info":return u.blueBright(a.toUpperCase());case "error":return u.redBright(a.toUpperCase());case "warn":return u.yellowBright(a.toUpperCase());default:return a.toUpperCase()}}var D=p.format.printf(({level:a,message:e,timestamp:t})=>`${u.dim(`[${t}]`)} ${M(a)} ${e}`),l=p.createLogger({format:p.format.combine(p.format.timestamp(),p.format.splat(),D),transports:[new p.transports.Console]}),_=new Intl.ListFormat("en",{style:"long",type:"unit"}),s=class{static info(e,...t){return l.info(e,...t)}static warn(e,...t){return l.warn(e,...t)}static error(e,...t){return l.error(e,...t)}static debug(e,...t){if(!(typeof process>"u"||!process.env.DEBUG))return l.info(e,...t)}static list(e){return _.format(e)}static close(){l.removeAllListeners(),l.close();}};var g="fakelab",y="module",h=["af","ar","az","bn","cs","cy","da","de","dv","el","en","eo","es","fa","fi","fr","he","hr","hu","hy","id","it","ja","ka","ko","ku","lv","mk","nb","ne","nl","pl","pt","ro"];function O(){let a=Intl.DateTimeFormat().resolvedOptions().locale;if(!a)return "en";let[e]=a.split("-"),t=e.toLowerCase();return h.includes(t)?t:"en"}var T=`let fl = {};
1
+ import N from'fast-glob';import p from'path';import L from'fs-extra';import {stat,access,constants}from'fs/promises';import M from'is-glob';import l from'winston';import f from'picocolors';import {transform}from'esbuild';function F(o){switch(o){case "info":return f.blueBright(o.toUpperCase());case "error":return f.redBright(o.toUpperCase());case "warn":return f.yellowBright(o.toUpperCase());default:return o.toUpperCase()}}var x=l.format.printf(({level:o,message:t,timestamp:e})=>`${f.dim(`[${e}]`)} ${F(o)} ${t}`),c=l.createLogger({format:l.format.combine(l.format.timestamp(),l.format.splat(),x),transports:[new l.transports.Console]}),C=new Intl.ListFormat("en",{style:"long",type:"unit"}),a=class{static info(t,...e){return c.info(t,...e)}static warn(t,...e){return c.warn(t,...e)}static error(t,...e){return c.error(t,...e)}static debug(t,...e){if(!(typeof process>"u"||!process.env.DEBUG))return c.info(t,...e)}static list(t){return C.format(t)}static close(){c.removeAllListeners(),c.close();}};var y="fakelab",g="module",h=["af","ar","az","bn","cs","cy","da","de","dv","el","en","eo","es","fa","fi","fr","he","hr","hu","hy","id","it","ja","ka","ko","ku","lv","mk","nb","ne","nl","pl","pt","ro"];function w(){let o=Intl.DateTimeFormat().resolvedOptions().locale;if(!o)return "en";let[t]=o.split("-"),e=t.toLowerCase();return h.includes(e)?e:"en"}var O=`let fl = {};
2
2
  let db = {};
3
3
  fl.url = () => "http://localhost:PORT/PREFIX/";
4
4
  fl.fetch = async function (name, count) {
@@ -14,9 +14,8 @@ fl.fetch = async function (name, count) {
14
14
  };
15
15
 
16
16
  db.enabled = () => ENABLED_COND;
17
- db.get = async function (name) {
18
- if (!db.enabled()) throw new Error("[fakelab] Database is not enabled.");
19
17
 
18
+ db.get = async function (name) {
20
19
  const response = await fetch(NAME.url() + "database/" + name);
21
20
 
22
21
  if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
@@ -26,19 +25,29 @@ db.get = async function (name) {
26
25
  return result;
27
26
  };
28
27
  db.post = async function (name) {
29
- if (!db.enabled()) throw new Error("[fakelab] Database is not enabled.");
30
-
31
- const response = await fetch(NAME.url() + "database/" + name, { method: "POST" });
28
+ const response = await fetch(NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
32
29
 
33
30
  if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
34
31
  };
32
+ db.seed = async function (name, options) {
33
+ const response = await fetch(NAME.url() + "database/insert/" + name, { method: "POST", body: JSON.stringify(options), headers: {"Content-Type": "application/json" } });
34
+
35
+ if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
36
+ };
37
+ db.flush = async function (name) {
38
+ const response = await fetch(NAME.url() + "database/flush/" + name, { method: "POST" });
39
+
40
+ if (!response.ok) throw new Error("[fakelab] Failed to flush seeded data from database.");
41
+ };
35
42
 
36
43
  const NAME = Object.freeze(fl);
37
44
  const database = Object.freeze(db);
38
45
 
39
- export { NAME, database };`,A=`declare function fetch<T extends keyof Runtime$, CT extends number | undefined = undefined>(name: T, count?: CT): Promise<Result$<Runtime$[T], CT>>;
40
- declare function get<T extends keyof Runtime$>(name: T): Promise<Runtime$[T]>;
46
+ export { NAME, database };`,T=`declare function fetch<T extends keyof Runtime$, CT extends number | undefined = undefined>(name: T, count?: CT): Promise<Result$<Runtime$[T], CT>>;
47
+ declare function get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
41
48
  declare function post<T extends keyof Runtime$>(name: T): Promise<void>;
49
+ declare function seed<T extends keyof Runtime$>(name: T, options?: SeedOptions): Promise<void>;
50
+ declare function flush<T extends keyof Runtime$>(name: T): Promise<void>;
42
51
  declare function enabled(): boolean;
43
52
  declare function url(): string;
44
53
  declare const NAME: {
@@ -48,12 +57,29 @@ declare const NAME: {
48
57
  declare const database: {
49
58
  get: typeof get;
50
59
  post: typeof post;
60
+ seed: typeof seed;
61
+ flush: typeof flush;
51
62
  enabled: typeof enabled;
52
63
  };
64
+ type SeedOptions = {
65
+ /**
66
+ * Number of records to generate.
67
+ */
68
+ count?: number;
69
+ /**
70
+ * Defines how seeding interacts with existing database data.
71
+ * - \`"reset"\`: Removes all existing data and recreates it from scratch.
72
+ * - \`"once"\`: Seeds data only if the database is empty.
73
+ * - \`"merge"\`: Inserts new records and updates existing ones. The total number of items per table
74
+ * is limited to \`1000\` records.
75
+ * @default "reset"
76
+ */
77
+ strategy?: "reset" | "once" | "merge"
78
+ }
53
79
  type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
54
80
  interface Runtime$ {}
55
81
 
56
- export { NAME, database };`,L=`global.NAME = {};
82
+ export { NAME, database };`,A=`global.NAME = {};
57
83
  global.NAME.database = {};
58
84
  global.NAME.url = () => "http://localhost:PORT/PREFIX/";
59
85
  global.NAME.fetch = async function (name, count) {
@@ -69,8 +95,6 @@ global.NAME.fetch = async function (name, count) {
69
95
  };
70
96
  global.NAME.database.enabled = () => ENABLED_COND;
71
97
  global.NAME.database.get = async function (name) {
72
- if (!global.NAME.database.enabled()) throw new Error("[fakelab] Database is not enabled.");
73
-
74
98
  const response = await fetch(global.NAME.url() + "database/" + name);
75
99
 
76
100
  if (!response.ok) throw new Error("[fakelab] Failed to retreived data from database.");
@@ -80,29 +104,54 @@ global.NAME.database.get = async function (name) {
80
104
  return result;
81
105
  };
82
106
  global.NAME.database.post = async function (name) {
83
- if (!global.NAME.database.enabled()) throw new Error("[fakelab] Database is not enabled.");
84
-
85
- const response = await fetch(global.NAME.url() + "database/" + name, { method: "POST" });
107
+ const response = await fetch(global.NAME.url() + "database/" + name, { method: "POST", headers: {"Content-Type": "application/json" } });
86
108
 
87
109
  if (!response.ok) throw new Error("[fakelab] Failed to post data to database.");
88
110
  };
111
+ global.NAME.database.seed = async function (name, options) {
112
+ const response = await fetch(global.NAME.url() + "database/insert/" + name, { method: "POST", body: JSON.stringify(options), headers: {"Content-Type": "application/json" } });
113
+
114
+ if (!response.ok) throw new Error("[fakelab] Failed to seed data to database.");
115
+ };
116
+ global.NAME.database.flush = async function (name) {
117
+ const response = await fetch(global.NAME.url() + "database/flush/" + name, { method: "POST" });
118
+
119
+ if (!response.ok) throw new Error("[fakelab] Failed to flush seeded data from database.");
120
+ };
89
121
 
90
122
  const NAME = Object.freeze(fl);
91
123
  const database = Object.freeze(db);
92
124
 
93
- export { NAME, database };`,R=`export {};
125
+ export { NAME, database };`,k=`export {};
94
126
 
95
127
  declare global {
96
128
  const database: {
97
- enabled(): boolean;
98
- get<T extends keyof Runtime$>(name: T): Promise<Runtime$[T]>;
129
+ get<T extends keyof Runtime$>(name: T): Promise<Array<Runtime$[T]>>;
99
130
  post(name: keyof Runtime$): Promise<void>;
131
+ seed(name: keyof Runtime$, options?: SeedOptions): Promise<void>;
132
+ flush(name: keyof Runtime$): Promise<void>;
133
+ enabled(): boolean;
100
134
  };
101
135
  const NAME: {
102
136
  fetch<T extends keyof Runtime$, CT extends number | undefined = undefined>(name: T, count?: CT): Promise<Result$<Runtime$[T], CT>>;
103
137
  url(): string;
104
138
  database: typeof database;
105
139
  };
140
+ type SeedOptions = {
141
+ /**
142
+ * Number of records to generate.
143
+ */
144
+ count?: number;
145
+ /**
146
+ * Defines how seeding interacts with existing database data.
147
+ * - \`"reset"\`: Removes all existing data and recreates it from scratch.
148
+ * - \`"once"\`: Seeds data only if the database is empty.
149
+ * - \`"merge"\`: Inserts new records and updates existing ones. The total number of items per table
150
+ * is limited to \`1000\` records.
151
+ * @default "reset"
152
+ */
153
+ strategy?: "reset" | "once" | "merge"
154
+ }
106
155
  type Result$<T, CT> = CT extends number ? (CT extends 0 ? T : T[]) : T;
107
156
  interface Runtime$ {}
108
157
  interface Window {
@@ -114,6 +163,4 @@ declare global {
114
163
  NAME: typeof NAME;
115
164
  }
116
165
  }
117
- }`;var w=class{constructor(e,t,r,o){this.port=e;this.prefix=t;this.browserOptions=r;this.dbOptions=o;this.name=this.browserOptions?.expose?.name||"fakelab",this.databaseEnabled=this.dbOptions?.enabled??true?"true":"false";}name;databaseEnabled;transformOptions={minify:true,platform:"browser",target:"es2022"};replacer(e,t){let r=e;for(let o in t)r=r.replace(new RegExp(o,"g"),t[o].toString().trim());return r}async prepareGlobalSource(){let e=this.replacer(L,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:t}=await transform(e,this.transformOptions);return t}catch(t){return t instanceof Error&&s.warn(t.message),e}}globalDeclaration(){return this.replacer(R,{NAME:this.name})}},b=class a extends w{constructor(t,r,o,c){super(t,r,o,c);this.port=t;this.prefix=r;this.browserOptions=o;this.dbOptions=c;}static init(t,r,o,c){let E=new a(t,r,o,c);return new Proxy(E,{get(n,f){return f==="prepareSource"?o?.expose?.mode==="global"?n.prepareGlobalSource:n.prepareSource:f==="declaration"?o?.expose?.mode==="global"?n.globalDeclaration:n.declaration:n[f]}})}async prepareSource(){let t=this.replacer(T,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:r}=await transform(t,this.transformOptions);return r}catch(r){return r instanceof Error&&s.warn(r.message),t}}declaration(){return this.replacer(A,{NAME:this.name})}};var m=class{constructor(e){this.opts=e;this.files=this.files.bind(this),this.serverOpts=this.serverOpts.bind(this),this.fakerOpts=this.fakerOpts.bind(this),this.browserOpts=this.browserOpts.bind(this),this.NETWORK_DEFAULT_OPTIONS=Object.freeze({delay:this.opts.network?.delay||0,errorRate:this.opts.network?.errorRate||0,timeoutRate:this.opts.network?.timeoutRate||0,offline:this.opts.network?.offline??false});}RUNTIME_SOURCE_FILENAME="runtime.js";RUNTIME_DECL_FILENAME="runtime.d.ts";FAKELAB_PERSIST_DIR=".fakelab";NETWORK_DEFAULT_OPTIONS;async files(e){let t=this.resolveSourcePath(e||this.opts.sourcePath),r=Array.from(new Set((await Promise.all(t.map(o=>this.resolveTSFiles(o)))).flat()));return r.length===0&&(s.error("No Typescript files found in: %s",s.list(t.map(o=>i.basename(o)))),process.exit(1)),r}serverOpts(e,t){return {pathPrefix:e||this.opts.server?.pathPrefix||"api",port:t||this.opts.server?.port||5200}}browserOpts(e,t){return {expose:{mode:t||this.opts.browser?.expose?.mode||y,name:e||this.opts.browser?.expose?.name||g}}}networkOpts(){let e=this.opts.network?.preset,t=this.opts.network?.presets??{};return !e||!t[e]?this.NETWORK_DEFAULT_OPTIONS:{...t[e],...this.opts.network??{}}}fakerOpts(e){let t=(e||this.opts.faker?.locale)?.toLowerCase();return t&&h.includes(t)?{locale:t}:{locale:O()}}async generateInFileRuntimeConfig(e,t){let{port:r,pathPrefix:o}=this.serverOpts(t.pathPrefix,t.port);await this.initializeDatabase();let c=i.resolve(e,this.RUNTIME_SOURCE_FILENAME),E=i.resolve(e,this.RUNTIME_DECL_FILENAME),n=b.init(r,o,this.opts.browser,this.opts.database),f=await n.prepareSource();await Promise.all([d.writeFile(c,f),d.writeFile(E,n.declaration())]);}getDatabaseDirectoryPath(){let e=this.opts.database?.dest||"db";return i.resolve(process.cwd(),e)}databaseEnabled(){return this.opts.database?.enabled??false}async initializeDatabase(){if(this.databaseEnabled())try{let e=this.opts.database?.dest||"db";await d.ensureDir(this.getDatabaseDirectoryPath()),await this.modifyGitignoreFile(e);}catch{s.error("Could not create database.");}else (!this.opts.database||!this.databaseEnabled())&&await d.rm(this.getDatabaseDirectoryPath(),{force:true,recursive:true});}async modifyGitignoreFile(e){try{let t=i.resolve(process.cwd(),".gitignore");if((await d.readFile(t,{encoding:"utf8"})).split(`
118
- `).some(o=>o.trim()===e.trim()))return;await d.appendFile(t,`
119
- ${e}`);}catch{s.error("Could not modify .gitignore file.");}}async tryStat(e){try{return await stat(e)}catch{return null}}async isReadable(e){try{return await access(e,constants.R_OK),!0}catch{return false}}resolveSourcePath(e){return (Array.isArray(e)?e:[e]).map(r=>F(r,{strict:true})?r:i.resolve(r))}async resolveTSFiles(e){if(F(e,{strict:true}))return s.info("source: %s",e),N(e,{absolute:true,ignore:["**/*.d.ts"]});let t=i.resolve(e),r=t.endsWith(".ts")?t:t+".ts";return (await this.tryStat(r))?.isFile()?(await this.isReadable(r)||(s.error("Cannot read file: %s",r),process.exit(1)),s.info("source: %s",r),[r]):(await this.tryStat(t))?.isDirectory()?(s.info("source: %s",t),N("**/*.ts",{cwd:t,absolute:true,ignore:["**/*.d.ts"]})):(s.warn("invalid source: [REDACTED]/%s",i.basename(r)),[])}};function v(a){return new m(a)}export{v as defineConfig};
166
+ }`;var E=class{constructor(t,e,r,s){this.port=t;this.prefix=e;this.browserOptions=r;this.dbOptions=s;this.name=this.browserOptions?.expose?.name||"fakelab",this.databaseEnabled=this.dbOptions?.enabled??true?"true":"false";}name;databaseEnabled;transformOptions={minify:true,platform:"browser",target:"es2022"};replacer(t,e){let r=t;for(let s in e)r=r.replace(new RegExp(s,"g"),e[s].toString().trim());return r}async prepareGlobalSource(){let t=this.replacer(A,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:e}=await transform(t,this.transformOptions);return e}catch(e){return e instanceof Error&&a.warn(e.message),t}}globalDeclaration(){return this.replacer(k,{NAME:this.name})}},u=class o extends E{constructor(e,r,s,i){super(e,r,s,i);this.port=e;this.prefix=r;this.browserOptions=s;this.dbOptions=i;}static init(e,r,s,i){let b=new o(e,r,s,i);return new Proxy(b,{get(n,d){return d==="prepareSource"?s?.expose?.mode==="global"?n.prepareGlobalSource:n.prepareSource:d==="declaration"?s?.expose?.mode==="global"?n.globalDeclaration:n.declaration:n[d]}})}async prepareSource(){let e=this.replacer(O,{NAME:this.name,PORT:this.port,PREFIX:this.prefix,ENABLED_COND:this.databaseEnabled});try{let{code:r}=await transform(e,this.transformOptions);return r}catch(r){return r instanceof Error&&a.warn(r.message),e}}declaration(){return this.replacer(T,{NAME:this.name})}};var m=class{constructor(t){this.opts=t;this.files=this.files.bind(this),this.serverOpts=this.serverOpts.bind(this),this.fakerOpts=this.fakerOpts.bind(this),this.browserOpts=this.browserOpts.bind(this),this.NETWORK_DEFAULT_OPTIONS=Object.freeze({delay:this.opts.network?.delay||0,errorRate:this.opts.network?.errorRate||0,timeoutRate:this.opts.network?.timeoutRate||0,offline:this.opts.network?.offline??false});}RUNTIME_SOURCE_FILENAME="runtime.js";RUNTIME_DECL_FILENAME="runtime.d.ts";FAKELAB_PERSIST_DIR=".fakelab";NETWORK_DEFAULT_OPTIONS;async files(t){let e=this.resolveSourcePath(t||this.opts.sourcePath),r=Array.from(new Set((await Promise.all(e.map(s=>this.resolveTSFiles(s)))).flat()));return r.length===0&&(a.error("No Typescript files found in: %s",a.list(e.map(s=>p.basename(s)))),process.exit(1)),r}serverOpts(t,e){return {pathPrefix:t||this.opts.server?.pathPrefix||"api",port:e||this.opts.server?.port||5200}}browserOpts(t,e){return {expose:{mode:e||this.opts.browser?.expose?.mode||g,name:t||this.opts.browser?.expose?.name||y}}}databaseOpts(){return {enabled:this.opts.database?.enabled??false,dest:this.opts?.database?.dest||"db"}}networkOpts(){let t=this.opts.network?.preset,e=this.opts.network?.presets??{};return !t||!e[t]?this.NETWORK_DEFAULT_OPTIONS:{...e[t],...this.opts.network??{}}}fakerOpts(t){let e=(t||this.opts.faker?.locale)?.toLowerCase();return e&&h.includes(e)?{locale:e}:{locale:w()}}async generateInFileRuntimeConfig(t,e){let{port:r,pathPrefix:s}=this.serverOpts(e.pathPrefix,e.port),i=p.resolve(t,this.RUNTIME_SOURCE_FILENAME),b=p.resolve(t,this.RUNTIME_DECL_FILENAME),n=u.init(r,s,this.opts.browser,this.opts.database),d=await n.prepareSource();await Promise.all([L.writeFile(i,d),L.writeFile(b,n.declaration())]);}async tryStat(t){try{return await stat(t)}catch{return null}}async isReadable(t){try{return await access(t,constants.R_OK),!0}catch{return false}}resolveSourcePath(t){return (Array.isArray(t)?t:[t]).map(r=>M(r,{strict:true})?r:p.resolve(r))}async resolveTSFiles(t){if(M(t,{strict:true}))return a.info("source: %s",t),N(t,{absolute:true,ignore:["**/*.d.ts"]});let e=p.resolve(t),r=e.endsWith(".ts")?e:e+".ts";return (await this.tryStat(r))?.isFile()?(await this.isReadable(r)||(a.error("Cannot read file: %s",r),process.exit(1)),a.info("source: %s",r),[r]):(await this.tryStat(e))?.isDirectory()?(a.info("source: %s",e),N("**/*.ts",{cwd:e,absolute:true,ignore:["**/*.d.ts"]})):(a.warn("invalid source: [REDACTED]/%s",p.basename(r)),[])}};function U(o){return new m(o)}export{U as defineConfig};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fakelab",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A easy-config mock server for frontend developers.",
@@ -35,8 +35,7 @@
35
35
  "scripts": {
36
36
  "build": "tsc && tsup",
37
37
  "release": "release-it --ci",
38
- "dev": "node --import=tsx src/cli.ts serve",
39
- "serve": "yarn build && node lib/cli.js serve"
38
+ "serve": "cross-env NODE_ENV=development yarn build && node lib/cli.js serve"
40
39
  },
41
40
  "directories": {
42
41
  "lib": "./lib"
@@ -62,6 +61,7 @@
62
61
  "@types/fs-extra": "^11.0.4",
63
62
  "@types/is-glob": "^4.0.4",
64
63
  "@types/node": "^24.10.1",
64
+ "cross-env": "^10.1.0",
65
65
  "release-it": "^19.0.6",
66
66
  "tsup": "^8.5.1",
67
67
  "tsx": "^4.21.0",