Skip to content

Backend notes

Things to keep in mind when writing backend code (packages/backend).

save issues a SELECT internally. If you only need to create a record, use insert.

const foo = await Foos.findOne({
bar: Not(null),
});

does not work as “bar is not null”. Use IsNull() instead.

const foo = await Foos.findOne({
bar: Not(IsNull()),
});
const users = await Users.find({
id: In(userIds),
});

If userIds is [], this issues user.id IN (), which is invalid SQL. If the array can be empty, check its length first.

const users = userIds.length > 0 ? await Users.find({
id: In(userIds),
}) : [];

When building SQL with the query builder, don’t use the same placeholder name more than once.

query.andWhere(new Brackets(qb => {
for (const type of ps.fileType) {
qb.orWhere(':type = ANY(note.attachedFileTypes)', { type: type });
}
}));

uses type several times in the loop, which breaks the query. Number the names to keep them apart.

query.andWhere(new Brackets(qb => {
for (const type of ps.fileType) {
const i = ps.fileType.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
}
}));

Branch the SQL when a parameter can be null

Section titled “Branch the SQL when a parameter can be null”
query.where('file.folderId = :folderId', { folderId: ps.folderId });

If ps.folderId is null, this becomes file.folderId = null, which is not correct SQL and does not return the expected result.

if (ps.folderId) {
query.where('file.folderId = :folderId', { folderId: ps.folderId });
} else {
query.where('file.folderId IS NULL');
}

IN never matches null. Handle null separately, for example with OR.

Array indexes in SQL are 1-based. To get a from [a, b, c], write [1], not [0].

Run this in packages/backend.

ターミナルウィンドウ
pnpm typeorm migration:generate -d ormconfig.js -o --esm migration/MigrationName

The generated file may contain unnecessary changes. Keep only what you need.

Migrations run on every startup until applied, so if one rewrites a large table, keep the load down, for example by narrowing the rows it touches.

To remove an enum value, every record with that value must be deleted. If that is too heavy or impossible, leave the value in place.

First, try forwardRef.

export class FooService {
constructor(
@Inject(forwardRef(() => BarService))
private barService: BarService,
) {
}
}

If that doesn’t help, use OnModuleInit and ModuleRef.

import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { BarService } from '@/core/BarService';
@Injectable()
export class FooService implements OnModuleInit {
private barService: BarService; // moved out of the constructor
constructor(
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.barService = this.moduleRef.get(BarService.name);
}
public async niceMethod() {
return await this.barService.incredibleMethod({ hoge: 'fuga' });
}
}

In that case, unit tests need to call onModuleInit themselves.

describe('test', () => {
let app: TestingModule;
let fooService: FooService;
let barService: BarService;
beforeEach(async () => {
app = await Test.createTestingModule({
imports: ...,
providers: [
FooService,
{ // mock it (optional)
provide: BarService,
useFactory: () => ({
incredibleMethod: vi.fn(),
}),
},
{ // make it available by name
provide: BarService.name,
useExisting: BarService,
},
],
})
.useMocker(...)
.compile();
fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as Mocked<BarService>;
// run onModuleInit
await fooService.onModuleInit();
});
test('nice', async () => {
await fooService.niceMethod();
expect(barService.incredibleMethod).toHaveBeenCalled();
expect(barService.incredibleMethod.mock.lastCall![0])
.toEqual({ hoge: 'fuga' });
});
});

The rules for adding your own properties to ActivityPub payloads are in ActivityPub payload changes in CONTRIBUTING.md. In short:

  • Prefix the name with _misskey_ (e.g. _misskey_quote).
  • Declare it as optional in packages/backend/src/core/activitypub/type.ts.
  • Add it to the context definition in packages/backend/src/core/activitypub/misc/contexts.ts.