fix(backend): correct invalid schema format specifying only `required` for `anyOf` (#16089)
* fix(backend): correct invalid schema format specifying only `required` for `anyOf` * refactor(backend): make types derived from `allOf` or `anyOf` more strong
This commit is contained in:
parent
ed3a844f5d
commit
d27075c5f5
|
@ -581,27 +581,6 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
|||
- 生成後、ファイルをmigration下に移してください
|
||||
- 作成されたスクリプトは不必要な変更を含むため除去してください
|
||||
|
||||
### JSON SchemaのobjectでanyOfを使うとき
|
||||
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
|
||||
バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます)
|
||||
https://github.com/misskey-dev/misskey/pull/10082
|
||||
|
||||
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
|
||||
|
||||
```ts
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hoge: { type: 'string', minLength: 1 },
|
||||
fuga: { type: 'string', minLength: 1 },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['hoge'] },
|
||||
{ required: ['fuga'] },
|
||||
],
|
||||
} as const;
|
||||
```
|
||||
|
||||
### コネクションには`markRaw`せよ
|
||||
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
||||
|
||||
|
|
|
@ -218,7 +218,17 @@ type NullOrUndefined<p extends Schema, T> =
|
|||
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||
// Get intersection from union
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
|
||||
|
||||
type ArrayToIntersection<T extends ReadonlyArray<Schema>> =
|
||||
T extends readonly [infer Head, ...infer Tail]
|
||||
? Head extends Schema
|
||||
? Tail extends ReadonlyArray<Schema>
|
||||
? Tail extends []
|
||||
? SchemaType<Head>
|
||||
: SchemaType<Head> & ArrayToIntersection<Tail>
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
||||
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
|
||||
|
@ -236,8 +246,8 @@ type ObjectSchemaTypeDef<p extends Schema> =
|
|||
: never
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> :
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? ArrayToIntersection<p['allOf']> :
|
||||
p['additionalProperties'] extends true ? Record<string, any> :
|
||||
p['additionalProperties'] extends Schema ?
|
||||
p['additionalProperties'] extends infer AdditionalProperties ?
|
||||
|
@ -277,7 +287,8 @@ export type SchemaTypeDef<p extends Schema> =
|
|||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> :
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? ArrayToIntersection<p['allOf']> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
|
||||
|
|
|
@ -162,14 +162,21 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['fileId'] },
|
||||
{ required: ['url'] },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['fileId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -186,15 +193,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({
|
||||
where: [{
|
||||
url: ps.url,
|
||||
}, {
|
||||
thumbnailUrl: ps.url,
|
||||
}, {
|
||||
webpublicUrl: ps.url,
|
||||
}],
|
||||
});
|
||||
const file = await this.driveFilesRepository.findOneBy(
|
||||
'fileId' in ps
|
||||
? { id: ps.fileId }
|
||||
: [{ url: ps.url }, { thumbnailUrl: ps.url }, { webpublicUrl: ps.url }],
|
||||
);
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
|
|
|
@ -37,29 +37,45 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['id'] },
|
||||
{ required: ['name'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -78,10 +94,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
// JSON schemeのanyOfの型変換がうまくいっていないらしい
|
||||
const required = { id: ps.id, name: ps.name } as
|
||||
| { id: MiEmoji['id']; name?: string }
|
||||
| { id?: MiEmoji['id']; name: string };
|
||||
const required = 'id' in ps
|
||||
? { id: ps.id, name: 'name' in ps ? ps.name as string : undefined }
|
||||
: { name: ps.name };
|
||||
|
||||
const error = await this.customEmojiService.update({
|
||||
...required,
|
||||
|
|
|
@ -43,14 +43,21 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['fileId'] },
|
||||
{ required: ['url'] },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['fileId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -64,21 +71,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let file: MiDriveFile | null = null;
|
||||
|
||||
if (ps.fileId) {
|
||||
file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
} else if (ps.url) {
|
||||
file = await this.driveFilesRepository.findOne({
|
||||
where: [{
|
||||
url: ps.url,
|
||||
}, {
|
||||
webpublicUrl: ps.url,
|
||||
}, {
|
||||
thumbnailUrl: ps.url,
|
||||
}],
|
||||
});
|
||||
}
|
||||
const file = await this.driveFilesRepository.findOneBy(
|
||||
'fileId' in ps
|
||||
? { id: ps.fileId }
|
||||
: [{ url: ps.url }, { webpublicUrl: ps.url }, { thumbnailUrl: ps.url }],
|
||||
);
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
|
|
|
@ -15,14 +15,21 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tokenId: { type: 'string', format: 'misskey:id' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['tokenId'] },
|
||||
{ required: ['token'] },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
tokenId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['tokenId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['token'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -33,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private accessTokensRepository: AccessTokensRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.tokenId) {
|
||||
if ('tokenId' in ps) {
|
||||
const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } });
|
||||
|
||||
if (tokenExist) {
|
||||
|
|
|
@ -28,38 +28,53 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reply: { type: 'boolean', nullable: true, default: null },
|
||||
renote: { type: 'boolean', nullable: true, default: null },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
poll: { type: 'boolean', nullable: true, default: null },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
|
||||
tag: { type: 'string', minLength: 1 },
|
||||
query: {
|
||||
type: 'array',
|
||||
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
tag: { type: 'string', minLength: 1 },
|
||||
},
|
||||
required: ['tag'],
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
minItems: 1,
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'array',
|
||||
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
reply: { type: 'boolean', nullable: true, default: null },
|
||||
renote: { type: 'boolean', nullable: true, default: null },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
poll: { type: 'boolean', nullable: true, default: null },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['tag'] },
|
||||
{ required: ['query'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -87,12 +102,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if ('tag' in ps) {
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
|
||||
query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] });
|
||||
} else {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
for (const tags of ps.query) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
|
||||
|
|
|
@ -33,15 +33,22 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pageId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['pageId'] },
|
||||
{ required: ['name', 'username'] },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
pageId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['pageId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
},
|
||||
required: ['name', 'username'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -59,9 +66,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
let page: MiPage | null = null;
|
||||
|
||||
if (ps.pageId) {
|
||||
if ('pageId' in ps) {
|
||||
page = await this.pagesRepository.findOneBy({ id: ps.pageId });
|
||||
} else if (ps.name && ps.username) {
|
||||
} else {
|
||||
const author = await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: ps.username.toLowerCase(),
|
||||
|
|
|
@ -47,23 +47,38 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: ['username', 'host'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
{ required: ['username', 'host'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -85,9 +100,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy(ps.userId != null
|
||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
|
||||
: { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
|
|
|
@ -54,25 +54,39 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: ['username', 'host'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
birthday: { ...birthdaySchema, nullable: true },
|
||||
},
|
||||
},
|
||||
|
||||
birthday: { ...birthdaySchema, nullable: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
{ required: ['username', 'host'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -94,9 +108,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy(ps.userId != null
|
||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
|
||||
: { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
|
|
|
@ -114,7 +114,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
userId: {
|
||||
anyOf: [
|
||||
oneOf: [
|
||||
{ type: 'string', format: 'misskey:id' },
|
||||
{
|
||||
type: 'array',
|
||||
|
|
|
@ -26,17 +26,32 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
detail: { type: 'boolean', default: true },
|
||||
|
||||
username: { type: 'string', nullable: true },
|
||||
host: { type: 'string', nullable: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['username'] },
|
||||
{ required: ['host'] },
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['username'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['host'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
detail: { type: 'boolean', default: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -47,8 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, (ps, me) => {
|
||||
return this.userSearchService.searchByUsernameAndHost({
|
||||
username: ps.username,
|
||||
host: ps.host,
|
||||
username: 'username' in ps ? ps.username : undefined,
|
||||
host: 'host' in ps ? ps.host : undefined,
|
||||
}, {
|
||||
limit: ps.limit,
|
||||
detail: ps.detail,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
|
||||
import { paramDef } from './show.js';
|
||||
|
||||
const VALID = true;
|
||||
const INVALID = false;
|
||||
|
||||
describe('api:users/show', () => {
|
||||
describe('validation', () => {
|
||||
const v = getValidator(paramDef);
|
||||
|
||||
test('Reject empty', () => expect(v({})).toBe(INVALID));
|
||||
test('Reject host only', () => expect(v({ host: 'misskey.test' })).toBe(INVALID));
|
||||
test('Accept userId only', () => expect(v({ userId: '1' })).toBe(VALID));
|
||||
test('Accept username and host', () => expect(v({ username: 'alice', host: 'misskey.test' })).toBe(VALID));
|
||||
});
|
||||
});
|
|
@ -59,23 +59,44 @@ export const meta = {
|
|||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
userIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
userIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
},
|
||||
required: ['userIds'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
},
|
||||
required: ['username'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
{ required: ['userIds'] },
|
||||
{ required: ['username'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@ -102,9 +123,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
let user;
|
||||
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
ps.username = ps.username?.trim();
|
||||
if ('username' in ps) {
|
||||
ps.username = ps.username.trim();
|
||||
}
|
||||
|
||||
if (ps.userIds) {
|
||||
if ('userIds' in ps) {
|
||||
if (ps.userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -129,7 +152,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return _users.map(u => _userMap.get(u.id)!);
|
||||
} else {
|
||||
// Lookup user
|
||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||
if (typeof ps.host === 'string' && 'username' in ps) {
|
||||
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
@ -139,7 +162,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
||||
});
|
||||
} else {
|
||||
const q: FindOptionsWhere<MiUser> = ps.userId != null
|
||||
const q: FindOptionsWhere<MiUser> = 'userId' in ps
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: IsNull() };
|
||||
|
||||
|
|
|
@ -89,7 +89,8 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
|||
schema.required = undefined;
|
||||
}
|
||||
|
||||
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
|
||||
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1)
|
||||
|| ['allOf', 'oneOf', 'anyOf'].some(o => (Array.isArray(schema[o]) && schema[o].length >= 0));
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
|
||||
|
|
|
@ -7306,8 +7306,9 @@ export type operations = {
|
|||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
fileId?: string;
|
||||
url?: string;
|
||||
fileId: string;
|
||||
} | {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -8093,10 +8094,12 @@ export type operations = {
|
|||
admin___emoji___update: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': ({
|
||||
/** Format: misskey:id */
|
||||
id?: string;
|
||||
name?: string;
|
||||
id: string;
|
||||
} | {
|
||||
name: string;
|
||||
}) & ({
|
||||
/** Format: misskey:id */
|
||||
fileId?: string;
|
||||
/** @description Use `null` to reset the category. */
|
||||
|
@ -8106,7 +8109,7 @@ export type operations = {
|
|||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
@ -16996,8 +16999,9 @@ export type operations = {
|
|||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
fileId?: string;
|
||||
url?: string;
|
||||
fileId: string;
|
||||
} | {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -23096,9 +23100,10 @@ export type operations = {
|
|||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
tokenId?: string;
|
||||
token?: string | null;
|
||||
};
|
||||
tokenId: string;
|
||||
} | ({
|
||||
token: string | null;
|
||||
});
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
@ -25784,7 +25789,12 @@ export type operations = {
|
|||
'notes___search-by-tag': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': ({
|
||||
tag: string;
|
||||
} | {
|
||||
/** @description The outer arrays are chained with OR, the inner arrays are chained with AND. */
|
||||
query: string[][];
|
||||
}) & ({
|
||||
/** @default null */
|
||||
reply?: boolean | null;
|
||||
/** @default null */
|
||||
|
@ -25802,10 +25812,7 @@ export type operations = {
|
|||
untilId?: string;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
tag?: string;
|
||||
/** @description The outer arrays are chained with OR, the inner arrays are chained with AND. */
|
||||
query?: string[][];
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
@ -26890,9 +26897,10 @@ export type operations = {
|
|||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
pageId?: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
pageId: string;
|
||||
} | {
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -28979,18 +28987,20 @@ export type operations = {
|
|||
users___followers: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': ({
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
} | ({
|
||||
username: string;
|
||||
/** @description The local host is represented with `null`. */
|
||||
host: string | null;
|
||||
})) & {
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
/** Format: misskey:id */
|
||||
userId?: string;
|
||||
username?: string;
|
||||
/** @description The local host is represented with `null`. */
|
||||
host?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -29042,20 +29052,22 @@ export type operations = {
|
|||
users___following: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': ({
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
} | ({
|
||||
username: string;
|
||||
/** @description The local host is represented with `null`. */
|
||||
host: string | null;
|
||||
})) & ({
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
/** Format: misskey:id */
|
||||
userId?: string;
|
||||
username?: string;
|
||||
/** @description The local host is represented with `null`. */
|
||||
host?: string | null;
|
||||
birthday?: string | null;
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
@ -30337,13 +30349,15 @@ export type operations = {
|
|||
'users___search-by-username-and-host': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': (({
|
||||
username: string | null;
|
||||
}) | ({
|
||||
host: string | null;
|
||||
})) & {
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
/** @default true */
|
||||
detail?: boolean;
|
||||
username?: string | null;
|
||||
host?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -30395,14 +30409,17 @@ export type operations = {
|
|||
users___show: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': ({
|
||||
/** Format: misskey:id */
|
||||
userId?: string;
|
||||
userIds?: string[];
|
||||
username?: string;
|
||||
userId: string;
|
||||
} | {
|
||||
userIds: string[];
|
||||
} | {
|
||||
username: string;
|
||||
}) & ({
|
||||
/** @description The local host is represented with `null`. */
|
||||
host?: string | null;
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
Loading…
Reference in New Issue