fexapi 0.1.4 → 0.1.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAAE,KACjB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAWtC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,EAAE,KACrB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAQlB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,YAAY,MAAM,EAAE,KACnB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAQlB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,MAAM,EAAE,KAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAkDxE,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EAAE,KAEf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,KAAK,EAAE,MAAM,CAAA;CAwDlB,CAAC"}
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAAE,KACjB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAgBtC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,EAAE,KACrB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAalB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,YAAY,MAAM,EAAE,KACnB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAalB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,MAAM,EAAE,KAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CA6ExE,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EAAE,KAEf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,KAAK,EAAE,MAAM,CAAA;CAoFlB,CAAC"}
package/dist/cli/args.js CHANGED
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseDevOptions = exports.parseServeOptions = exports.parseFormatOptions = exports.parseGenerateOptions = exports.parseInitOptions = void 0;
4
+ const findDuplicateFlags = (args, flags) => {
5
+ return flags.filter((flag) => args.filter((value) => value === flag).length > 1);
6
+ };
4
7
  const parseInitOptions = (initArgs) => {
5
8
  const validFlags = new Set(["--force"]);
6
9
  const invalidFlags = initArgs.filter((value) => value.startsWith("-") && !validFlags.has(value));
7
10
  if (invalidFlags.length > 0) {
8
11
  return { error: `Unknown option(s): ${invalidFlags.join(", ")}` };
9
12
  }
13
+ const positionalArgs = initArgs.filter((value) => !value.startsWith("-"));
14
+ if (positionalArgs.length > 0) {
15
+ return { error: `Unexpected argument(s): ${positionalArgs.join(", ")}` };
16
+ }
10
17
  return { force: initArgs.includes("--force") };
11
18
  };
12
19
  exports.parseInitOptions = parseInitOptions;
@@ -15,6 +22,10 @@ const parseGenerateOptions = (generateArgs) => {
15
22
  if (invalidFlags.length > 0) {
16
23
  return { error: `Unknown option(s): ${invalidFlags.join(", ")}` };
17
24
  }
25
+ const positionalArgs = generateArgs.filter((value) => !value.startsWith("-"));
26
+ if (positionalArgs.length > 0) {
27
+ return { error: `Unexpected argument(s): ${positionalArgs.join(", ")}` };
28
+ }
18
29
  return {};
19
30
  };
20
31
  exports.parseGenerateOptions = parseGenerateOptions;
@@ -23,10 +34,22 @@ const parseFormatOptions = (formatArgs) => {
23
34
  if (invalidFlags.length > 0) {
24
35
  return { error: `Unknown option(s): ${invalidFlags.join(", ")}` };
25
36
  }
37
+ const positionalArgs = formatArgs.filter((value) => !value.startsWith("-"));
38
+ if (positionalArgs.length > 0) {
39
+ return { error: `Unexpected argument(s): ${positionalArgs.join(", ")}` };
40
+ }
26
41
  return {};
27
42
  };
28
43
  exports.parseFormatOptions = parseFormatOptions;
29
44
  const parseServeOptions = (serveArgs) => {
45
+ const duplicateFlags = findDuplicateFlags(serveArgs, [
46
+ "--host",
47
+ "--port",
48
+ "--log",
49
+ ]);
50
+ if (duplicateFlags.length > 0) {
51
+ return { error: `Duplicate option(s): ${duplicateFlags.join(", ")}` };
52
+ }
30
53
  const getFlagValue = (flagName) => {
31
54
  const index = serveArgs.indexOf(flagName);
32
55
  if (index === -1) {
@@ -53,6 +76,20 @@ const parseServeOptions = (serveArgs) => {
53
76
  if (portValue && typeof portValue !== "string") {
54
77
  return portValue;
55
78
  }
79
+ const consumedIndexes = new Set();
80
+ serveArgs.forEach((value, index) => {
81
+ if (value === "--log") {
82
+ consumedIndexes.add(index);
83
+ }
84
+ if ((value === "--host" || value === "--port") && serveArgs[index + 1]) {
85
+ consumedIndexes.add(index);
86
+ consumedIndexes.add(index + 1);
87
+ }
88
+ });
89
+ const positionalArgs = serveArgs.filter((_value, index) => !consumedIndexes.has(index));
90
+ if (positionalArgs.length > 0) {
91
+ return { error: `Unexpected argument(s): ${positionalArgs.join(", ")}` };
92
+ }
56
93
  const host = hostValue ?? "127.0.0.1";
57
94
  const port = portValue ? Number(portValue) : undefined;
58
95
  if (port !== undefined &&
@@ -63,6 +100,15 @@ const parseServeOptions = (serveArgs) => {
63
100
  };
64
101
  exports.parseServeOptions = parseServeOptions;
65
102
  const parseDevOptions = (devArgs) => {
103
+ const duplicateFlags = findDuplicateFlags(devArgs, [
104
+ "--host",
105
+ "--port",
106
+ "--watch",
107
+ "--log",
108
+ ]);
109
+ if (duplicateFlags.length > 0) {
110
+ return { error: `Duplicate option(s): ${duplicateFlags.join(", ")}` };
111
+ }
66
112
  const getFlagValue = (flagName) => {
67
113
  const index = devArgs.indexOf(flagName);
68
114
  if (index === -1) {
@@ -90,6 +136,20 @@ const parseDevOptions = (devArgs) => {
90
136
  if (portValue && typeof portValue !== "string") {
91
137
  return portValue;
92
138
  }
139
+ const consumedIndexes = new Set();
140
+ devArgs.forEach((value, index) => {
141
+ if (value === "--watch" || value === "--log") {
142
+ consumedIndexes.add(index);
143
+ }
144
+ if ((value === "--host" || value === "--port") && devArgs[index + 1]) {
145
+ consumedIndexes.add(index);
146
+ consumedIndexes.add(index + 1);
147
+ }
148
+ });
149
+ const positionalArgs = devArgs.filter((_value, index) => !consumedIndexes.has(index));
150
+ if (positionalArgs.length > 0) {
151
+ return { error: `Unexpected argument(s): ${positionalArgs.join(", ")}` };
152
+ }
93
153
  const host = hostValue ?? "127.0.0.1";
94
154
  const port = portValue ? Number(portValue) : undefined;
95
155
  if (port !== undefined &&
package/dist/cli/help.js CHANGED
@@ -34,7 +34,7 @@ const printHelp = () => {
34
34
  console.log(" fexapi.config.json");
35
35
  console.log(" fexapi.config.js");
36
36
  console.log(" fexapi/schema.fexapi");
37
- console.log(" schemas/*.yaml (optional, via wizard)");
37
+ console.log(" fexapi/schemas/*.yaml (optional, via wizard)");
38
38
  console.log("");
39
39
  console.log("Init wizard asks:");
40
40
  console.log(" What port? (default: 3000)");
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,aAAa,GAAI,2CAK3B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB,KAAG,MAuHH,CAAC"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,aAAa,GAAI,2CAK3B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB,KAAG,MAsHH,CAAC"}
@@ -18,7 +18,7 @@ const isWatchedPath = (projectRoot, changedPath) => {
18
18
  if (relativePath.startsWith("fexapi/")) {
19
19
  return true;
20
20
  }
21
- return (relativePath.startsWith("schemas/") &&
21
+ return (relativePath.startsWith("fexapi/schemas/") &&
22
22
  (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")));
23
23
  };
24
24
  const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
@@ -71,7 +71,6 @@ const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
71
71
  const activeWatchers = [];
72
72
  const watchTargets = [
73
73
  (0, node_path_1.join)(projectRoot, "fexapi"),
74
- (0, node_path_1.join)(projectRoot, "schemas"),
75
74
  projectRoot,
76
75
  ];
77
76
  for (const watchTarget of watchTargets) {
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAwKA,eAAO,MAAM,iBAAiB,GAAU,YAErC;IACD,KAAK,EAAE,OAAO,CAAC;CAChB,KAAG,OAAO,CAAC,MAAM,CAmJjB,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA0LA,eAAO,MAAM,iBAAiB,GAAU,YAErC;IACD,KAAK,EAAE,OAAO,CAAC;CAChB,KAAG,OAAO,CAAC,MAAM,CAmJjB,CAAC"}
@@ -82,8 +82,8 @@ const getRuntimeConfigTemplate = ({ port, cors, includeSampleRoutes, }) => {
82
82
  const routeSection = includeSampleRoutes
83
83
  ? [
84
84
  " routes: {",
85
- ' "/users": { count: 25, schema: "user" },',
86
- ' "/posts": { count: 40, schema: "post" },',
85
+ ' "/users": { count: 10, schema: "user" },',
86
+ ' "/posts": { count: 20, schema: "post" },',
87
87
  " },",
88
88
  ]
89
89
  : [];
@@ -102,12 +102,22 @@ const SAMPLE_USER_SCHEMA = [
102
102
  "fullName:",
103
103
  " type: name",
104
104
  " faker: person.fullName",
105
+ "username:",
106
+ " type: string",
107
+ " faker: internet.username",
105
108
  "email:",
106
109
  " type: email",
107
110
  " faker: internet.email",
108
111
  "avatarUrl:",
109
112
  " type: url",
110
113
  " faker: image.avatar",
114
+ "bio:",
115
+ " type: string",
116
+ " faker: lorem.sentence",
117
+ "isActive:",
118
+ " type: boolean",
119
+ "joinedAt:",
120
+ " type: date",
111
121
  ].join("\n");
112
122
  const SAMPLE_POST_SCHEMA = [
113
123
  "id:",
@@ -117,7 +127,15 @@ const SAMPLE_POST_SCHEMA = [
117
127
  " faker: lorem.sentence",
118
128
  "body:",
119
129
  " type: string",
120
- " faker: lorem.paragraph",
130
+ " faker: lorem.paragraphs",
131
+ "authorId:",
132
+ " type: uuid",
133
+ "published:",
134
+ " type: boolean",
135
+ "likes:",
136
+ " type: number",
137
+ " min: 0",
138
+ " max: 500",
121
139
  "createdAt:",
122
140
  " type: date",
123
141
  ].join("\n");
@@ -133,7 +151,7 @@ const initializeProject = async ({ force, }) => {
133
151
  const schemaPath = (0, node_path_1.join)(fexapiDirectoryPath, "schema.fexapi");
134
152
  const configPath = (0, node_path_1.join)(projectRoot, "fexapi.config.json");
135
153
  const runtimeConfigPath = (0, node_path_1.join)(projectRoot, "fexapi.config.js");
136
- const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "schemas");
154
+ const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi", "schemas");
137
155
  const userSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "user.yaml");
138
156
  const postSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "post.yaml");
139
157
  const wizardAnswers = await askInitWizardQuestions();
@@ -20,23 +20,23 @@ const isRecord = (value) => {
20
20
  };
21
21
  const parseSchemaDefinition = (schemaName, rawValue) => {
22
22
  if (!isRecord(rawValue)) {
23
- console.error(`Invalid schemas/${schemaName}.yaml: expected root object of field definitions.`);
23
+ console.error(`Invalid fexapi/schemas/${schemaName}.yaml: expected root object of field definitions.`);
24
24
  return undefined;
25
25
  }
26
26
  const result = {};
27
27
  for (const [fieldName, rawFieldConfig] of Object.entries(rawValue)) {
28
28
  if (!isRecord(rawFieldConfig)) {
29
- console.error(`Invalid schemas/${schemaName}.yaml field \`${fieldName}\`: expected object with type/faker/min/max.`);
29
+ console.error(`Invalid fexapi/schemas/${schemaName}.yaml field \`${fieldName}\`: expected object with type/faker/min/max.`);
30
30
  continue;
31
31
  }
32
32
  const rawType = rawFieldConfig.type;
33
33
  if (typeof rawType !== "string") {
34
- console.error(`Invalid schemas/${schemaName}.yaml field \`${fieldName}\`: missing string \`type\`.`);
34
+ console.error(`Invalid fexapi/schemas/${schemaName}.yaml field \`${fieldName}\`: missing string \`type\`.`);
35
35
  continue;
36
36
  }
37
37
  const normalizedType = rawType.trim().toLowerCase();
38
38
  if (!VALID_TYPES.has(normalizedType)) {
39
- console.error(`Invalid schemas/${schemaName}.yaml field \`${fieldName}\`: unknown type \`${rawType}\`.`);
39
+ console.error(`Invalid fexapi/schemas/${schemaName}.yaml field \`${fieldName}\`: unknown type \`${rawType}\`.`);
40
40
  continue;
41
41
  }
42
42
  const minValue = rawFieldConfig.min;
@@ -57,7 +57,7 @@ const parseSchemaDefinition = (schemaName, rawValue) => {
57
57
  return Object.keys(result).length > 0 ? result : undefined;
58
58
  };
59
59
  const loadSchemaDefinitions = (projectRoot) => {
60
- const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "schemas");
60
+ const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi", "schemas");
61
61
  if (!(0, node_fs_1.existsSync)(schemasDirectoryPath)) {
62
62
  return {};
63
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/project/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAyD5E,eAAO,MAAM,aAAa,GACxB,iBAAiB,MAAM,EACvB,aAAa,MAAM,KAClB,eAwDF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,kBAAkB,EAC7B,aAAW,KACV,MA+BF,CAAC"}
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/project/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAyD5E,eAAO,MAAM,aAAa,GACxB,iBAAiB,MAAM,EACvB,aAAa,MAAM,KAClB,eAwDF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,kBAAkB,EAC7B,aAAW,KACV,MAmGF,CAAC"}
@@ -94,33 +94,101 @@ const detectProject = (packageJsonPath, projectRoot) => {
94
94
  };
95
95
  exports.detectProject = detectProject;
96
96
  const getSchemaTemplate = (framework, port = 4000) => {
97
- const frameworkHint = framework === "nextjs"
98
- ? "# Framework: Next.js"
97
+ const frameworkLabel = framework === "nextjs"
98
+ ? "Next.js"
99
99
  : framework === "reactjs"
100
- ? "# Framework: React"
101
- : "# Framework: unknown";
100
+ ? "React"
101
+ : framework === "vue"
102
+ ? "Vue"
103
+ : framework === "nuxt"
104
+ ? "Nuxt"
105
+ : framework === "svelte"
106
+ ? "Svelte"
107
+ : framework === "sveltekit"
108
+ ? "SvelteKit"
109
+ : framework === "angular"
110
+ ? "Angular"
111
+ : framework === "solid"
112
+ ? "Solid"
113
+ : framework === "remix"
114
+ ? "Remix"
115
+ : framework === "astro"
116
+ ? "Astro"
117
+ : "unknown";
102
118
  return [
103
- frameworkHint,
119
+ `# Framework: ${frameworkLabel}`,
120
+ "",
121
+ "# ──────────────────────────────────────────────",
104
122
  "# Server",
123
+ "# ──────────────────────────────────────────────",
105
124
  `port: ${port}`,
106
125
  "",
126
+ "# ──────────────────────────────────────────────",
127
+ "# Available types",
128
+ "# string → random words",
129
+ "# number → random integer",
130
+ "# boolean → true / false",
131
+ "# uuid → unique id",
132
+ "# email → fake email",
133
+ "# name → full name",
134
+ "# url → fake URL",
135
+ "# phone → phone number",
136
+ "# date → ISO date string",
137
+ "# ──────────────────────────────────────────────",
138
+ "",
139
+ "# ──────────────────────────────────────────────",
107
140
  "# Routes",
108
- "# Format (single-line): METHOD /endpoint: field:type,field:type",
109
- "# Format (multi-line):",
110
- "# METHOD /endpoint:",
111
- "# field:type",
112
- "# field:type",
141
+ "#",
142
+ "# Single-line: GET /items: id:uuid, name:string",
143
+ "# Multi-line:",
144
+ "# GET /items:",
145
+ "# id:uuid",
146
+ "# name:string",
147
+ "# ──────────────────────────────────────────────",
148
+ "",
149
+ "# ── Users ────────────────────────────────────",
150
+ "",
113
151
  "GET /users:",
114
152
  " id:uuid",
115
153
  " fullName:name",
116
154
  " username:string",
117
155
  " email:email",
156
+ " phone:phone",
118
157
  " avatarUrl:url",
158
+ " joinedAt:date",
159
+ "",
160
+ "POST /users:",
161
+ " id:uuid",
162
+ " fullName:name",
163
+ " username:string",
164
+ " email:email",
165
+ "",
166
+ "# ── Posts ────────────────────────────────────",
167
+ "",
119
168
  "GET /posts:",
120
169
  " id:uuid",
121
170
  " title:string",
122
171
  " body:string",
172
+ " authorId:uuid",
173
+ " published:boolean",
123
174
  " createdAt:date",
175
+ "",
176
+ "POST /posts:",
177
+ " id:uuid",
178
+ " title:string",
179
+ " body:string",
180
+ " authorId:uuid",
181
+ "",
182
+ "PUT /posts:",
183
+ " id:uuid",
184
+ " title:string",
185
+ " body:string",
186
+ "",
187
+ "DELETE /posts: id:uuid",
188
+ "",
189
+ "# ── Comments (single-line example) ───────────",
190
+ "",
191
+ "GET /comments: id:uuid, body:string, postId:uuid, authorId:uuid, createdAt:date",
124
192
  ].join("\n");
125
193
  };
126
194
  exports.getSchemaTemplate = getSchemaTemplate;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAqHF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,KACjB;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAmG3C,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAqKF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,KACjB;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAmH3C,CAAC"}
package/dist/schema.js CHANGED
@@ -12,10 +12,27 @@ const VALID_TYPES = [
12
12
  "name",
13
13
  "phone",
14
14
  ];
15
+ const VALID_METHODS = new Set([
16
+ "GET",
17
+ "POST",
18
+ "PUT",
19
+ "PATCH",
20
+ "DELETE",
21
+ "HEAD",
22
+ "OPTIONS",
23
+ ]);
15
24
  const DEFAULT_PORT = 4000;
16
25
  const ROUTE_FORMAT_ERROR_MESSAGE = "Invalid route definition. Expected format: " +
17
26
  "METHOD /endpoint: field:type,field:type (or multiline fields under METHOD /endpoint:)";
27
+ const FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
18
28
  const isPortLine = (line) => /^port\s*:/i.test(line.trim());
29
+ const stripInlineComment = (line) => {
30
+ const commentIndex = line.indexOf("#");
31
+ if (commentIndex === -1) {
32
+ return line.trim();
33
+ }
34
+ return line.slice(0, commentIndex).trim();
35
+ };
19
36
  const parseRouteHeader = (line) => {
20
37
  const separatorIndex = line.indexOf(":");
21
38
  if (separatorIndex === -1) {
@@ -36,12 +53,23 @@ const parseRouteHeader = (line) => {
36
53
  };
37
54
  };
38
55
  const parseField = (rawField) => {
39
- const [rawName, rawType] = rawField.split(":");
56
+ const parts = rawField.split(":");
57
+ if (parts.length !== 2) {
58
+ return {
59
+ error: `Invalid field "${rawField}". Expected format name:type.`,
60
+ };
61
+ }
62
+ const [rawName, rawType] = parts;
40
63
  const name = rawName?.trim();
41
64
  const type = rawType?.trim().toLowerCase();
42
65
  if (!name) {
43
66
  return { error: `Invalid field "${rawField}". Missing field name.` };
44
67
  }
68
+ if (!FIELD_NAME_PATTERN.test(name)) {
69
+ return {
70
+ error: `Invalid field name "${name}". Use letters, numbers, and underscore only (must not start with a number).`,
71
+ };
72
+ }
45
73
  if (!type) {
46
74
  return { error: `Invalid field "${rawField}". Missing field type.` };
47
75
  }
@@ -59,10 +87,16 @@ const parseRoute = ({ method, path, rawFields, }) => {
59
87
  if (!method || !path) {
60
88
  return { error: ROUTE_FORMAT_ERROR_MESSAGE };
61
89
  }
90
+ if (!VALID_METHODS.has(method)) {
91
+ return {
92
+ error: `Unsupported HTTP method "${method}" for route ${method} ${path}. Valid methods: ${Array.from(VALID_METHODS).join(", ")}`,
93
+ };
94
+ }
62
95
  const fields = [];
96
+ const seenFieldNames = new Set();
63
97
  for (const rawFieldLine of rawFields) {
64
98
  for (const part of rawFieldLine.split(",")) {
65
- const trimmedPart = part.trim();
99
+ const trimmedPart = stripInlineComment(part);
66
100
  if (!trimmedPart) {
67
101
  continue;
68
102
  }
@@ -70,6 +104,12 @@ const parseRoute = ({ method, path, rawFields, }) => {
70
104
  if ("error" in parsedField) {
71
105
  return { error: parsedField.error };
72
106
  }
107
+ if (seenFieldNames.has(parsedField.name)) {
108
+ return {
109
+ error: `Duplicate field "${parsedField.name}" in route ${method} ${path}.`,
110
+ };
111
+ }
112
+ seenFieldNames.add(parsedField.name);
73
113
  fields.push(parsedField);
74
114
  }
75
115
  }
@@ -83,9 +123,10 @@ const parseFexapiSchema = (schemaText) => {
83
123
  const routes = [];
84
124
  const errors = [];
85
125
  const lines = schemaText.split(/\r?\n/);
126
+ const seenRoutes = new Set();
86
127
  for (let index = 0; index < lines.length; index += 1) {
87
128
  const rawLine = lines[index] ?? "";
88
- const trimmedLine = rawLine.trim();
129
+ const trimmedLine = stripInlineComment(rawLine);
89
130
  if (!trimmedLine || trimmedLine.startsWith("#")) {
90
131
  continue;
91
132
  }
@@ -104,12 +145,18 @@ const parseFexapiSchema = (schemaText) => {
104
145
  }
105
146
  const header = parseRouteHeader(trimmedLine);
106
147
  if (!header) {
107
- errors.push(ROUTE_FORMAT_ERROR_MESSAGE);
148
+ errors.push(`${ROUTE_FORMAT_ERROR_MESSAGE} (line ${index + 1})`);
149
+ continue;
150
+ }
151
+ const routeKey = `${header.method} ${header.path}`;
152
+ if (seenRoutes.has(routeKey)) {
153
+ errors.push(`Duplicate route definition: ${routeKey} (line ${index + 1})`);
108
154
  continue;
109
155
  }
110
156
  const rawFields = [];
111
- if (header.inlineFields.trim()) {
112
- rawFields.push(header.inlineFields);
157
+ const inlineFields = stripInlineComment(header.inlineFields);
158
+ if (inlineFields) {
159
+ rawFields.push(inlineFields);
113
160
  }
114
161
  let lookaheadIndex = index + 1;
115
162
  while (lookaheadIndex < lines.length) {
@@ -124,7 +171,10 @@ const parseFexapiSchema = (schemaText) => {
124
171
  break;
125
172
  }
126
173
  if (/^\s+/.test(lookaheadRawLine)) {
127
- rawFields.push(lookaheadTrimmedLine.replace(/^-+\s*/, ""));
174
+ const normalizedFieldLine = stripInlineComment(lookaheadTrimmedLine.replace(/^-+\s*/, ""));
175
+ if (normalizedFieldLine) {
176
+ rawFields.push(normalizedFieldLine);
177
+ }
128
178
  lookaheadIndex += 1;
129
179
  continue;
130
180
  }
@@ -136,9 +186,10 @@ const parseFexapiSchema = (schemaText) => {
136
186
  rawFields,
137
187
  });
138
188
  if ("error" in parsedRoute) {
139
- errors.push(parsedRoute.error);
189
+ errors.push(`${parsedRoute.error} (line ${index + 1})`);
140
190
  continue;
141
191
  }
192
+ seenRoutes.add(routeKey);
142
193
  routes.push(parsedRoute);
143
194
  index = lookaheadIndex - 1;
144
195
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAyLF,eAAO,MAAM,WAAW,GAAI,0EAOzB,aAAkB,wFAmGpB,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAyLF,eAAO,MAAM,WAAW,GAAI,0EAOzB,aAAkB,wFAuIpB,CAAC"}
package/dist/server.js CHANGED
@@ -187,11 +187,39 @@ const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, runtim
187
187
  if (apiSpec) {
188
188
  const matchedRoute = apiSpec.routes.find((route) => route.method === request.method && route.path === pathname);
189
189
  if (matchedRoute) {
190
- const count = getCountFromUrl(request.url, 5);
191
- const payloadKey = toCollectionKey(matchedRoute.path);
192
- sendJson(response, 200, {
193
- [payloadKey]: Array.from({ length: count }, () => createRecordFromRoute(matchedRoute)),
194
- }, { cors: corsEnabled, delay: responseDelay });
190
+ const method = request.method ?? "GET";
191
+ if (method === "GET") {
192
+ const count = getCountFromUrl(request.url, 5);
193
+ const payloadKey = toCollectionKey(matchedRoute.path);
194
+ sendJson(response, 200, {
195
+ [payloadKey]: Array.from({ length: count }, () => createRecordFromRoute(matchedRoute)),
196
+ }, { cors: corsEnabled, delay: responseDelay });
197
+ return;
198
+ }
199
+ if (method === "DELETE") {
200
+ sendJson(response, 200, { message: `Deleted resource at ${matchedRoute.path}` }, { cors: corsEnabled, delay: responseDelay });
201
+ return;
202
+ }
203
+ const bodyChunks = [];
204
+ request.on("data", (chunk) => bodyChunks.push(chunk));
205
+ request.on("end", () => {
206
+ let requestBody = {};
207
+ try {
208
+ const raw = Buffer.concat(bodyChunks).toString("utf-8");
209
+ if (raw.trim()) {
210
+ requestBody = JSON.parse(raw);
211
+ }
212
+ }
213
+ catch {
214
+ }
215
+ const generatedRecord = createRecordFromRoute(matchedRoute);
216
+ const merged = { ...generatedRecord, ...requestBody };
217
+ const statusCode = method === "POST" ? 201 : 200;
218
+ sendJson(response, statusCode, merged, {
219
+ cors: corsEnabled,
220
+ delay: responseDelay,
221
+ });
222
+ });
195
223
  return;
196
224
  }
197
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fexapi",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Mock API generation CLI tool for local development and testing",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",