Skip to content

Commit 569b2b3

Browse files
committed
feat(code-gen): drop R.files()
BREAKING CHANGE: - Use `R.body()` instead of `R.files()` - When using the Koa router, change usages of `ctx.validatedFiles` with `ctx.validatedBody` - Auto-generated type names for files inputs like `PostSetHeaderImageFiles` will be renamed to `PostSetHeaderImageBody`. - Executing this change on the server doesn't require immediate regeneration of api clients. The way they currently send files is compatible.
1 parent 80429b0 commit 569b2b3

File tree

19 files changed

+78
-252
lines changed

19 files changed

+78
-252
lines changed

docs/features/file-handling.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# File handling
22

33
Compas also comes with various utilities across the stack to handle files in a
4-
consistent way.
4+
consistent way. See the [file-handling](/examples/file-handling.html) example
5+
for a project implementing this.
56

67
## Generated router & validators
78

@@ -13,20 +14,29 @@ const T = new TypeCreator();
1314
const R = T.router("/");
1415

1516
R.post("/upload")
16-
.files({
17+
.body({
1718
myFile: T.file(),
1819
})
1920
.response({ success: true });
2021

2122
R.get("/download").response(T.file());
2223
```
2324

24-
Files are handled separately by the generator and validators, and are put on
25-
`ctx.validatedFiles` with help from
26-
[formidable](https://www.npmjs.com/package/formidable). In the generated api
27-
clients we generate the correct type (`ReadableStream` or `Blob`) depending on
28-
the context. And allow for setting custom file parsing options
29-
`createBodyParser` provided by `@compas/server`
25+
Files are handled like any other 'POST'-body in Compas, but have some
26+
restrictions. When a `T.file()` is used, we automatically switch the request
27+
encoding to use `multipart/form-data` instead of `application/json`. We also add
28+
restrictions on what other fields we can accept in the same request body. This
29+
is limited to only use 'simple' types like `T.uuid()`, `T.string()`,
30+
`T.number()`, `T.bool()`.
31+
32+
The generated api clients automatically determine which type it should generated
33+
for the target library and runtime. For Fetch clients we use `Blob`, but for an
34+
Axios & Node.js target, we generate a `ReadableStream` based type.
35+
36+
The default body parser created via `createBodyParser`, from `@compas/server`,
37+
doesn't parse `multipart/form-data` by default. You can enable this via the
38+
`multipart: true` option, and customize limitations via the `multipartOptions`
39+
option.
3040

3141
## Saving files
3242

gen/code-gen.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,6 @@ export function extendWithCodeGen(generator) {
631631
query: T.reference("structure", "referenceDefinition").optional(),
632632
params: T.reference("structure", "referenceDefinition").optional(),
633633
body: T.reference("structure", "referenceDefinition").optional(),
634-
files: T.reference("structure", "referenceDefinition").optional(),
635634
response: T.reference("structure", "referenceDefinition").optional(),
636635
invalidations: [
637636
T.reference("structure", "routeInvalidationDefinition"),

packages/code-gen/src/api-client/generator.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ export function apiClientGenerator(generateContext) {
121121
const types = {
122122
params: route.params,
123123
query: route.query,
124-
files: route.files,
125124
body: route.body,
126125
response: route.response,
127126
};

packages/code-gen/src/api-client/js-axios.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,6 @@ export function jsAxiosGenerateFunction(
181181
}} body`,
182182
);
183183
}
184-
if (route.files) {
185-
args.push("files");
186-
fileWrite(file, `@param {${contextNames.filesTypeName}} files`);
187-
}
188184

189185
// Allow overwriting any request config
190186
args.push("requestConfig");
@@ -209,8 +205,8 @@ export function jsAxiosGenerateFunction(
209205
)}(${args.join(", ")})`,
210206
);
211207

212-
if (route.files || route.metadata?.requestBodyType === "form-data") {
213-
const parameter = route.body ? "body" : "files";
208+
if (route.metadata?.requestBodyType === "form-data") {
209+
const parameter = "body";
214210

215211
fileWrite(
216212
file,
@@ -225,7 +221,7 @@ export function jsAxiosGenerateFunction(
225221
generateContext.structure,
226222

227223
// @ts-expect-error
228-
route.body ?? route.files,
224+
route.body,
229225
);
230226

231227
for (const key of Object.keys(type.keys)) {
@@ -278,15 +274,15 @@ export function jsAxiosGenerateFunction(
278274
fileWrite(file, `params: query,`);
279275
}
280276

281-
if (route.files || route.metadata?.requestBodyType === "form-data") {
277+
if (route.metadata?.requestBodyType === "form-data") {
282278
fileWrite(file, `data,`);
283279
}
284280

285281
if (route.body && route.metadata?.requestBodyType !== "form-data") {
286282
fileWrite(file, `data: body,`);
287283
}
288284

289-
if (route.files) {
285+
if (route.metadata?.requestBodyType === "form-data") {
290286
fileWrite(
291287
file,
292288
`headers: typeof data.getHeaders === "function" ? data.getHeaders() : {},`,

packages/code-gen/src/api-client/js-fetch.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,6 @@ export function jsFetchGenerateFunction(
205205
}} body`,
206206
);
207207
}
208-
if (route.files) {
209-
args.push("files");
210-
fileWrite(file, `@param {${contextNames.filesTypeName}} files`);
211-
}
212208

213209
// Allow overwriting any request config
214210
args.push("requestConfig");
@@ -235,8 +231,8 @@ export function jsFetchGenerateFunction(
235231
)}(${args.join(", ")})`,
236232
);
237233

238-
if (route.files || route.metadata?.requestBodyType === "form-data") {
239-
const parameter = route.body ? "body" : "files";
234+
if (route.metadata?.requestBodyType === "form-data") {
235+
const parameter = "body";
240236
fileWrite(
241237
file,
242238
`const data = ${parameter} instanceof FormData ? ${parameter} : new FormData();`,
@@ -250,7 +246,7 @@ export function jsFetchGenerateFunction(
250246
generateContext.structure,
251247

252248
// @ts-expect-error
253-
route.body ?? route.files,
249+
route.body,
254250
);
255251

256252
for (const key of Object.keys(type.keys)) {
@@ -339,7 +335,7 @@ export function jsFetchGenerateFunction(
339335

340336
fileWrite(file, `method: "${route.method}",`);
341337

342-
if (route.files || route.metadata?.requestBodyType === "form-data") {
338+
if (route.metadata?.requestBodyType === "form-data") {
343339
fileWrite(file, `body: data,`);
344340
}
345341

packages/code-gen/src/api-client/react-query.js

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -229,25 +229,6 @@ export function reactQueryGenerateFunction(
229229
}
230230
}
231231

232-
if (route.files) {
233-
const files = structureResolveReference(
234-
generateContext.structure,
235-
route.files,
236-
);
237-
if (files.type !== "object") {
238-
fileWrite(file, "\n\n");
239-
fileWrite(
240-
file,
241-
fileFormatInlineComment(
242-
file,
243-
`Skipped generation of '${hookName}' since a custom files type is used.`,
244-
),
245-
);
246-
fileWrite(file, "\n\n");
247-
return;
248-
}
249-
}
250-
251232
// Import the corresponding api client function.
252233
const importCollector = JavascriptImportCollector.getImportCollector(file);
253234
importCollector.destructure("./apiClient", `${apiName}`);
@@ -278,7 +259,6 @@ export function reactQueryGenerateFunction(
278259
contextNames.paramsTypeName,
279260
contextNames.queryTypeName,
280261
contextNames.bodyTypeName,
281-
contextNames.filesTypeName,
282262
withRequestConfig
283263
? `{ requestConfig?: ${
284264
distilledTargetInfo.isAxios
@@ -404,33 +384,6 @@ export function reactQueryGenerateFunction(
404384
.join(", ")} }, `;
405385
}
406386

407-
if (route.files) {
408-
/** @type {import("../generated/common/types.d.ts").StructureObjectDefinition} */
409-
// @ts-expect-error
410-
const files = structureResolveReference(
411-
generateContext.structure,
412-
route.files,
413-
);
414-
415-
result += `{ ${Object.keys(files.keys)
416-
.map((it) => {
417-
if (
418-
defaultToNull &&
419-
referenceUtilsGetProperty(
420-
generateContext,
421-
files.keys[it],
422-
["isOptional"],
423-
false,
424-
)
425-
) {
426-
return `"${it}": ${prefix}["${it}"] ?? null`;
427-
}
428-
429-
return `"${it}": ${prefix}["${it}"]`;
430-
})
431-
.join(", ")} }, `;
432-
}
433-
434387
if (withRequestConfig) {
435388
result += `${prefix}?.requestConfig`;
436389
}
@@ -463,8 +416,7 @@ export function reactQueryGenerateFunction(
463416
);
464417

465418
// When no arguments are required, the whole opts object is optional
466-
const routeHasMandatoryInputs =
467-
route.params || route.query || route.body || route.files;
419+
const routeHasMandatoryInputs = route.params || route.query || route.body;
468420

469421
fileWrite(file, `opts: `);
470422
fileWrite(
@@ -786,7 +738,7 @@ function reactQueryCheckIfRequiredVariablesArePresent(generateContext, route) {
786738
function reactQueryGetRequiredFields(generateContext, route) {
787739
const keysAffectingEnabled = [];
788740

789-
for (const key of ["params", "query", "body", "files"]) {
741+
for (const key of ["params", "query", "body"]) {
790742
if (!route[key]) {
791743
continue;
792744
}

packages/code-gen/src/api-client/ts-axios.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ export function tsAxiosGenerateFunction(
150150
}`,
151151
);
152152
}
153-
if (route.files) {
154-
args.push(`files: ${contextNames.filesTypeName}`);
155-
}
156153

157154
// Allow overwriting any request config
158155
args.push(`requestConfig?: AxiosRequestConfig`);
@@ -167,8 +164,8 @@ export function tsAxiosGenerateFunction(
167164
)}(${args.join(", ")}): Promise<${contextNames.responseTypeName}>`,
168165
);
169166

170-
if (route.files || route.metadata?.requestBodyType === "form-data") {
171-
const parameter = route.body ? "body" : "files";
167+
if (route.metadata?.requestBodyType === "form-data") {
168+
const parameter = "body";
172169

173170
fileWrite(
174171
file,
@@ -183,7 +180,7 @@ export function tsAxiosGenerateFunction(
183180
generateContext.structure,
184181

185182
// @ts-expect-error
186-
route.body ?? route.files,
183+
route.body,
187184
);
188185

189186
for (const key of Object.keys(type.keys)) {
@@ -241,7 +238,7 @@ export function tsAxiosGenerateFunction(
241238
fileWrite(file, `params: query,`);
242239
}
243240

244-
if (route.files || route.metadata?.requestBodyType === "form-data") {
241+
if (route.metadata?.requestBodyType === "form-data") {
245242
fileWrite(file, `data,`);
246243

247244
if (distilledTargetInfo.isReactNative) {

packages/code-gen/src/api-client/ts-fetch.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,6 @@ export function tsFetchGenerateFunction(
208208
}`,
209209
);
210210
}
211-
if (route.files) {
212-
args.push(`files: ${contextNames.filesTypeName}`);
213-
}
214211

215212
// Allow overwriting any request config
216213
args.push(`requestConfig?: RequestInit`);
@@ -227,8 +224,8 @@ export function tsFetchGenerateFunction(
227224
}>`,
228225
);
229226

230-
if (route.files || route.metadata?.requestBodyType === "form-data") {
231-
const parameter = route.body ? "body" : "files";
227+
if (route.metadata?.requestBodyType === "form-data") {
228+
const parameter = "body";
232229

233230
fileWrite(
234231
file,
@@ -243,7 +240,7 @@ export function tsFetchGenerateFunction(
243240
generateContext.structure,
244241

245242
// @ts-expect-error
246-
route.body ?? route.files,
243+
route.body,
247244
);
248245

249246
for (const key of Object.keys(type.keys)) {
@@ -337,7 +334,7 @@ export function tsFetchGenerateFunction(
337334

338335
fileWrite(file, `method: "${route.method}",`);
339336

340-
if (route.files || route.metadata?.requestBodyType === "form-data") {
337+
if (route.metadata?.requestBodyType === "form-data") {
341338
fileWrite(file, `body: data,`);
342339

343340
if (distilledTargetInfo.isReactNative) {

packages/code-gen/src/builders/RouteBuilder.d.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ export class RouteBuilder extends TypeBuilder {
1515
bodyBuilder:
1616
| import("../../types/advanced-types.js").TypeBuilderLike
1717
| undefined;
18-
filesBuilder:
19-
| import("../../types/advanced-types.js").TypeBuilderLike
20-
| undefined;
2118
responseBuilder:
2219
| import("../../types/advanced-types.js").TypeBuilderLike
2320
| undefined;
@@ -49,11 +46,6 @@ export class RouteBuilder extends TypeBuilder {
4946
* @returns {RouteBuilder}
5047
*/
5148
body(builder: import("../../index").TypeBuilderLike): RouteBuilder;
52-
/**
53-
* @param {import("../../index").TypeBuilderLike} builder
54-
* @returns {RouteBuilder}
55-
*/
56-
files(builder: import("../../index").TypeBuilderLike): RouteBuilder;
5749
/**
5850
* Specify routes that can be invalidated when this route is called.
5951
*
@@ -93,9 +85,6 @@ export class RouteCreator {
9385
bodyBuilder:
9486
| import("../../types/advanced-types.js").TypeBuilderLike
9587
| undefined;
96-
filesBuilder:
97-
| import("../../types/advanced-types.js").TypeBuilderLike
98-
| undefined;
9988
responseBuilder:
10089
| import("../../types/advanced-types.js").TypeBuilderLike
10190
| undefined;
@@ -119,11 +108,6 @@ export class RouteCreator {
119108
* @returns {RouteCreator}
120109
*/
121110
body(builder: import("../../index").TypeBuilderLike): RouteCreator;
122-
/**
123-
* @param {import("../../index").TypeBuilderLike} builder
124-
* @returns {RouteCreator}
125-
*/
126-
files(builder: import("../../index").TypeBuilderLike): RouteCreator;
127111
/**
128112
* @param {import("../../index").TypeBuilderLike} builder
129113
* @returns {RouteCreator}

0 commit comments

Comments
 (0)