refactor: noteテーブルのインデックス整理と配列カラムへのクエリでインデックスを使うように (#12993)

* Optimize note model index

* enhance(backend): ANY()をやめる (MisskeyIO#239)

* add small e2e test drive endpoint

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
YS 2024-01-15 08:19:27 +09:00 committed by GitHub
parent 0ea8e0c25c
commit d92aaf81c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 27 deletions

View file

@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { MiNote } from '@/models/Note.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => {
let Notes: Repository<MiNote>;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
test('ファイルURLからアップロードできる', async () => {
// utils.js uploadUrl の処理だがAPIレスポンスも見るためここで同様の処理を書いている
const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'
const catcher = makeStreamCatcher(
alice,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
10 * 1000);
const res = await api('drive/files/upload-from-url', {
url,
marker,
force: true,
}, alice);
const file = await catcher;
assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg');
})
test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body?.type, 'image/jpeg');
})
test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id)
const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id)
assert.strictEqual(attached0.body[1].id, note0.id)
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id)
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0)
})
test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice);
await post(alice, { fileIds: [file.body!.id] });
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true);
})
});

View file

@ -16,6 +16,7 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
import { Packed } from '@/misc/json-schema.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@ -114,6 +115,20 @@ export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', len
return randomString;
}
/**
* @brief
* @param p
* @param timeout
*/
function timeoutPromise<T>(p: Promise<T>, timeout: number): Promise<T> {
return Promise.race([
p,
new Promise((reject) =>{
setTimeout(() => { reject(new Error('timed out')); }, timeout)
}) as never
]);
}
export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']>): Promise<NonNullable<misskey.Endpoints['signup']['res']>> => {
const q = Object.assign({
username: randomString(),
@ -320,17 +335,16 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
};
};
export const uploadUrl = async (user: UserToken, url: string) => {
let resolve: unknown;
const file = new Promise(ok => resolve = ok);
export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => {
const marker = Math.random().toString();
const ws = await connectStream(user, 'main', (msg) => {
if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) {
ws.close();
resolve(msg.body.file);
}
});
const catcher = makeStreamCatcher(
user,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
60 * 1000
);
await api('drive/files/upload-from-url', {
url,
@ -338,7 +352,7 @@ export const uploadUrl = async (user: UserToken, url: string) => {
force: true,
}, user);
return file;
return catcher;
};
export function connectStream(user: UserToken, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
@ -410,6 +424,35 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any
});
};
/**
* @brief WebSocketストリームから特定条件の通知を拾うプロミスを生成
* @param user
* @param channel
* @param cond
* @param extractor
* @param timeout
* @returns extractorを通した値を得る
*/
export function makeStreamCatcher<T>(
user: UserToken,
channel: string,
cond: (message: Record<string, any>) => boolean,
extractor: (message: Record<string, any>) => T,
timeout = 60 * 1000): Promise<T> {
let ws: WebSocket
const p = new Promise<T>(async (resolve) => {
ws = await connectStream(user, channel, (msg) => {
if (cond(msg)) {
resolve(extractor(msg))
}
});
}).finally(() => {
ws?.close();
});
return timeoutPromise(p, timeout);
}
export type SimpleGetResponse = {
status: number,
body: any | JSDOM | null,