# Kysely services

Write a service class for each Postgres table with the kysely plugin. Covers its table and key, overriding the table name, typing the services map, and reading a service from a handler.

If every handler that looks up a user writes its own Kysely query, the same query ends up in several files. Changing how you find a user then means finding every copy.

A service keeps one table's queries in one class. Extend [`KyselyService`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-service), pass the table name as its type argument, and decorate the class with a key. Handlers then call its methods through that key.

```ts title="src/postgres/Users.ts"
import {
    KyselyService,
    RegisterKyselyService
} from '@seedcord/plugin-kysely-postgres';


@RegisterKyselyService('users')
export class Users extends KyselyService<'users'> {
    public findByDiscordId(discordId: string) {
        return this.db
            .selectFrom(this.table)
            .selectAll()
            .where('discord_id', '=', discordId)
            .executeTakeFirst();
    }
}

declare module '@seedcord/plugin-kysely-postgres' {
    interface KyselyServices {
        users: Users;
    }
}
```

Here's what each part of `Users.ts` does.

* **`@RegisterKyselyService('users')`** gives the class its key. Your handlers read the service from `services` under that key, and the plugin also uses it as the table name unless you [pass another one](#the-key-is-also-the-table-name).
* **`KyselyService<'users'>`** is the base class, and its type argument is a table from [your schema](/plugins/kysely-schema). `this.table` holds that name, so `selectFrom(this.table)` checks `discord_id` against the `users` columns.
* **`this.db`** returns the plugin's [`connection`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-postgres#connection), so every service queries through the same pool. It can query every table in your schema, so a method can join `users` with another table.
* **`findByDiscordId`** keeps the lookup inside the class. A handler calls that method and never writes the query itself.
* **The `declare module` block** adds `users` to the keys the decorator accepts. [Typing the registry](#typing-the-registry) explains why you write it.

## You never construct a service

The plugin builds your services itself, after it has connected and run your migrations, so `this.db` has a working connection by the time any method runs. It imports every `.ts` and `.js` file under the `dir` you gave the [kysely plugin](/plugins/kysely), including subfolders. For each exported class that extends `KyselyService` and carries the decorator, it constructs the class. That constructor call adds the instance to the plugin's `services` map under its key.

> **Warning**
>
> If a class isn't exported or has no [`@RegisterKyselyService`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/register-kysely-service), the plugin skips it without throwing or logging anything. `services.users` then reads `undefined` even though TypeScript types it as `Users`.

## The key is also the table name

A service registered as `'users'` reads and writes the `users` table, because the plugin uses the key as the table name. That fits most services. When the table's name isn't one you want as a key, like an `app_users` table named with a prefix, pass `table`.

```ts
declare module '@seedcord/plugin-kysely-postgres' {
    // write this in src/database.ts. it's only here for the example
    interface KyselyDatabase {
        schema: { app_users: UsersTable };
    }

    interface KyselyServices {
        users: Users;
    }
}

@RegisterKyselyService('users', { table: 'app_users' })
export class Users extends KyselyService<'app_users'> {}
```

`Users` queries the `app_users` table, while handlers use it as `services.users`. The two names come from the two interfaces in that `declare module` block:

* **`'users'`** is a key in `KyselyServices`. The decorator's first argument only accepts keys declared there.
* **`'app_users'`** is a table in the `schema` you declared on `KyselyDatabase`, which [Your schema](/plugins/kysely-schema) covers. `table` only accepts a table from that schema, and the decorator checks the class's type argument against it.

That schema is also where your editor gets the table names it suggests for `table`. With a `bans` table added, it offers both:

```ts
declare module '@seedcord/plugin-kysely-postgres' {
    // write this in src/database.ts. it's only here for the example
    interface KyselyDatabase {
        schema: { app_users: UsersTable; bans: BansTable };
    }

    interface KyselyServices {
        users: Users;
    }
}

RegisterKyselyService('users', { table: '
```

The decorator checks that `table` matches the class's type argument, so TypeScript reports an error if you pass `{ table: 'app_users' }` for a `KyselyService<'bans'>`. When you leave `table` out, the decorator uses the key as the table name. `users` isn't a table in that schema, so `Users` needs `table`, and TypeScript reports an error on the decorator without it.

## Typing the registry

[`KyselyServices`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-services) starts empty, so the decorator doesn't accept any key until you declare one. The `declare module` block in `src/postgres/Users.ts` above adds `users`. The decorator then takes `'users'` and checks that the class matches `Users`, the type you declared for that key. If you pass `'guilds'` to the decorator before adding a `guilds` entry, TypeScript reports an error.

```ts
declare module '@seedcord/plugin-kysely-postgres' {
    interface KyselyServices {
        users: Users;
        guilds: Guilds;
    }
}

RegisterKyselyService('
```

The decorator offers the two keys that `declare module` block adds to `KyselyServices`, `users` and `guilds`.

You add each entry by hand when you write a new service, since codegen doesn't write this block. It only writes the `Core` block from [Typing a plugin](/plugins/typing).

## Reading a service

A handler gets a service from the plugin's [`services`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-postgres#services) map, under the key you declared.

```ts
const found =
    await this.core.db.services.users.findByDiscordId(
        this.event.user.id
    );

await this.reply(
    found ? `Streak ${String(found.streak)}.` : 'No record.'
);
```

`findByDiscordId` resolves to `undefined` when no row matches, so the handler checks `found` before it reads `streak`.

If you read `services` before the plugin's `init()` has connected, migrated, and loaded every class, it throws `PluginKyselyServicesNotReady`. Your handlers never read it that early, though, because they only start receiving interactions once `init()` has finished.
