Skip to content

Commit

Permalink
feat(misskey-js): multipart/form-dataのリクエストに対応 (misskey-dev#14147)
Browse files Browse the repository at this point in the history
* feat(misskey-js): multipart/form-dataのリクエストに対応

* lint

* add test

* Update Changelog

* テストを厳しくする

* lint

* multipart/form-dataではnullのプロパティを弾くように
  • Loading branch information
kakkokari-gtyih authored Jul 7, 2024
1 parent 984d582 commit f119f8c
Show file tree
Hide file tree
Showing 7 changed files with 537 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように

### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

## 2024.5.0

### Note
Expand Down
2 changes: 1 addition & 1 deletion packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,7 @@ type FetchExternalResourcesResponse = operations['fetch-external-resources']['re
// @public (undocumented)
type FetchLike = (input: string, init?: {
method?: string;
body?: string;
body?: Blob | FormData | string;
credentials?: RequestCredentials;
cache?: RequestCache;
headers: {
Expand Down
57 changes: 55 additions & 2 deletions packages/misskey-js/generator/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ async function generateBaseTypes(
}
lines.push('');

const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
const generatedTypes = await openapiTS(openApiJsonPath, {
exportType: true,
transform(schemaObject) {
if ('format' in schemaObject && schemaObject.format === 'binary') {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}
},
});
lines.push(generatedTypes);
lines.push('');

Expand Down Expand Up @@ -56,6 +63,8 @@ async function generateEndpoints(
endpointOutputPath: string,
) {
const endpoints: Endpoint[] = [];
const endpointReqMediaTypes: EndpointReqMediaType[] = [];
const endpointReqMediaTypesSet = new Set<string>();

// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
const paths = openApiDocs.paths ?? {};
Expand All @@ -78,13 +87,24 @@ async function generateEndpoints(
const supportMediaTypes = Object.keys(reqContent);
if (supportMediaTypes.length > 0) {
// いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする
endpoint.request = new OperationTypeAlias(
const req = new OperationTypeAlias(
operationId,
path,
supportMediaTypes[0],
OperationsAliasType.REQUEST,
);
endpoint.request = req;

const reqType = new EndpointReqMediaType(path, req);
endpointReqMediaTypesSet.add(reqType.getMediaType());
endpointReqMediaTypes.push(reqType);
} else {
endpointReqMediaTypesSet.add('application/json');
endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
}
} else {
endpointReqMediaTypesSet.add('application/json');
endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
}

if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
Expand Down Expand Up @@ -137,6 +157,19 @@ async function generateEndpoints(
endpointOutputLine.push('}');
endpointOutputLine.push('');

function generateEndpointReqMediaTypesType() {
return `Record<keyof Endpoints, ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}>`;
}

endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`);

endpointOutputLine.push(
...endpointReqMediaTypes.map(it => '\t' + it.toLine()),
);

endpointOutputLine.push('};');
endpointOutputLine.push('');

await writeFile(endpointOutputPath, endpointOutputLine.join('\n'));
}

Expand Down Expand Up @@ -314,6 +347,26 @@ class Endpoint {
}
}

class EndpointReqMediaType {
public readonly path: string;
public readonly mediaType: string;

constructor(path: string, request: OperationTypeAlias, mediaType?: undefined);
constructor(path: string, request: undefined, mediaType: string);
constructor(path: string, request: OperationTypeAlias | undefined, mediaType?: string) {
this.path = path;
this.mediaType = mediaType ?? request?.mediaType ?? 'application/json';
}

getMediaType(): string {
return this.mediaType;
}

toLine(): string {
return `'${this.path}': '${this.mediaType}',`;
}
}

async function main() {
const generatePath = './built/autogen';
await mkdir(generatePath, { recursive: true });
Expand Down
51 changes: 43 additions & 8 deletions packages/misskey-js/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './autogen/apiClientJSDoc.js';

import { SwitchCaseResponseType } from './api.types.js';
import type { Endpoints } from './api.types.js';
import { endpointReqTypes } from './autogen/endpoint.js';
import type { SwitchCaseResponseType, Endpoints } from './api.types.js';

export type {
SwitchCaseResponseType,
Expand All @@ -23,7 +23,7 @@ export function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIE

export type FetchLike = (input: string, init?: {
method?: string;
body?: string;
body?: Blob | FormData | string;
credentials?: RequestCredentials;
cache?: RequestCache;
headers: { [key in string]: string }
Expand All @@ -49,20 +49,55 @@ export class APIClient {
this.fetch = opts.fetch ?? ((...args) => fetch(...args));
}

private assertIsRecord<T>(obj: T): obj is T & Record<string, any> {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}

public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>(
endpoint: E,
params: P = {} as P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>> {
return new Promise((resolve, reject) => {
this.fetch(`${this.origin}/api/${endpoint}`, {
method: 'POST',
body: JSON.stringify({
let mediaType = 'application/json';
if (endpoint in endpointReqTypes) {
mediaType = endpointReqTypes[endpoint];
}
let payload: FormData | string = '{}';

if (mediaType === 'application/json') {
payload = JSON.stringify({
...params,
i: credential !== undefined ? credential : this.credential,
}),
});
} else if (mediaType === 'multipart/form-data') {
payload = new FormData();
const i = credential !== undefined ? credential : this.credential;
if (i != null) {
payload.append('i', i);
}
if (this.assertIsRecord(params)) {
for (const key in params) {
const value = params[key];

if (value == null) continue;

if (value instanceof File || value instanceof Blob) {
payload.append(key, value);
} else if (typeof value === 'object') {
payload.append(key, JSON.stringify(value));
} else {
payload.append(key, value);
}
}
}
}

this.fetch(`${this.origin}/api/${endpoint}`, {
method: 'POST',
body: payload,
headers: {
'Content-Type': 'application/json',
'Content-Type': endpointReqTypes[endpoint],
},
credentials: 'omit',
cache: 'no-cache',
Expand Down
Loading

0 comments on commit f119f8c

Please sign in to comment.