feat: Sync to OSS repo

This commit is contained in:
Wanjohi
2026-08-06 22:13:51 +03:00
parent 46d2a56180
commit 3faac3008f
144 changed files with 27561 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid, utc } from '../db/types.js';
import { UserTable } from './user.sql.js';
export const UserFingerprintTable = pgTable(
'user_fingerprint',
{
...id,
...timestamps,
userId: ulid('user_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
fingerprint: text('fingerprint').notNull(),
name: text('name'),
lastSeen: utc('last_seen')
},
(t) => [
uniqueIndex('user_fingerprint_fingerprint_unique').on(t.fingerprint),
index('user_fingerprint_user_idx').on(t.userId)
]
);

View File

@@ -0,0 +1,167 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { UserFingerprintTable } from './fingerprint.sql.js';
import { User } from './index.js';
import { LinkedAccount } from './linked-account.js';
export namespace Fingerprint {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the fingerprint record',
example: Examples.Fingerprint.id
}),
userId: z.string().meta({
description: 'The user this fingerprint belongs to',
example: Examples.Fingerprint.userId
}),
fingerprint: z.string().meta({
description: 'MD5 hex of the SSH public key',
example: Examples.Fingerprint.fingerprint
}),
name: z.string().optional().nullable().meta({
description: 'Human-readable label (e.g. "MacBook Air")',
example: Examples.Fingerprint.name
}),
lastSeen: z.iso.datetime().optional().nullable().meta({
description: 'Timestamp of last connection using this key',
example: Examples.Fingerprint.lastSeen
})
})
.meta({
ref: 'Fingerprint',
description: 'An SSH public key fingerprint linked to a user account',
example: Examples.Fingerprint
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info.pick({ id: true, userId: true, fingerprint: true, name: true }),
async (input) => {
await Database.use(async (tx) => {
await tx.insert(UserFingerprintTable).values({
id: input.id,
userId: input.userId,
fingerprint: input.fingerprint,
name: input.name ?? null,
lastSeen: null
});
});
return input.id;
}
);
export const findByFingerprint = fn(Info.shape.fingerprint, async (fingerprint) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserFingerprintTable)
.where(
and(
eq(UserFingerprintTable.fingerprint, fingerprint),
isNull(UserFingerprintTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
});
export const listByUser = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserFingerprintTable)
.where(
and(eq(UserFingerprintTable.userId, userId), isNull(UserFingerprintTable.timeDeleted))
)
.orderBy(UserFingerprintTable.timeCreated);
});
});
export const updateName = fn(Info.pick({ id: true, name: true }), async (input) => {
await Database.use(async (tx) => {
await tx
.update(UserFingerprintTable)
.set({ name: input.name ?? null })
.where(eq(UserFingerprintTable.id, input.id));
});
});
export const touchLastSeen = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(UserFingerprintTable)
.set({ lastSeen: sql`now()` })
.where(eq(UserFingerprintTable.id, id));
});
});
export const remove = fn(Info.shape.fingerprint, async (fingerprint) => {
await Database.use(async (tx) => {
await tx
.update(UserFingerprintTable)
.set({ timeDeleted: sql`now()` })
.where(eq(UserFingerprintTable.fingerprint, fingerprint));
});
});
export const repoint = fn(Info.pick({ fingerprint: true, userId: true }), async (input) => {
await Database.use(async (tx) => {
await tx
.update(UserFingerprintTable)
.set({
userId: input.userId,
timeUpdated: sql`now()`
})
.where(
and(
eq(UserFingerprintTable.fingerprint, input.fingerprint),
isNull(UserFingerprintTable.timeDeleted)
)
);
});
});
export const mergeFingerprint = fn(
Info.pick({ fingerprint: true }).extend({ targetUserId: z.string() }),
async (input) => {
const fp = await findByFingerprint(input.fingerprint);
if (!fp) {
throw new Error('Fingerprint not found');
}
if (fp.userId === input.targetUserId) {
return { merged: false as const, reason: 'already_owned' as const };
}
const orphanLinkedAccounts = await LinkedAccount.listByUser(fp.userId);
if (orphanLinkedAccounts.length > 0) {
throw new Error(
'This device already has linked accounts. Unlink them first before merging.'
);
}
await Database.transaction(async () => {
await repoint({ fingerprint: input.fingerprint, userId: input.targetUserId });
await User.remove(fp.userId);
});
return { merged: true as const, targetUserId: input.targetUserId };
}
);
export function serialize(input: typeof UserFingerprintTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
userId: input.userId,
fingerprint: input.fingerprint,
name: input.name,
lastSeen: input.lastSeen?.toISOString() ?? null
};
}
}

View File

@@ -0,0 +1,124 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { UserTable } from './user.sql.js';
export namespace User {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the user record',
example: Examples.User.id
}),
name: z.string().meta({
description: 'The display name associated with this account',
example: Examples.User.name
}),
email: z.email().optional().nullable().optional().meta({
description:
'Primary email address for account notifications and billing. May be null for gaming-only accounts.',
example: Examples.User.email
}),
emailVerified: z.boolean().meta({
description: 'Indicates whether the email address has been verified',
example: Examples.User.emailVerified
}),
image: z.string().nullable().optional().meta({
description: "URL pointing to the user's profile picture",
example: Examples.User.image
})
})
.meta({
ref: 'User',
description: 'User account entity with core identification details',
example: Examples.User
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info.pick({ id: true, name: true, email: true, emailVerified: true, image: true }),
async (input) => {
await Database.use(async (tx) => {
await tx
.insert(UserTable)
.values({
id: input.id,
name: input.name || 'Player',
email: input.email ?? null,
emailVerified: input.emailVerified ?? false,
image: input.image ?? null
})
.onConflictDoNothing({ target: UserTable.id });
});
return input.id;
}
);
export const fromID = fn(Info.shape.id, async (id) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserTable)
.where(and(eq(UserTable.id, id), isNull(UserTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export const fromEmail = fn(z.email(), async (email) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserTable)
.where(and(eq(UserTable.email, email), isNull(UserTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export async function list() {
return Database.use(async (tx) => {
return tx
.select()
.from(UserTable)
.where(isNull(UserTable.timeDeleted))
.orderBy(UserTable.timeCreated);
});
}
export const updateEmail = fn(
Info.pick({ id: true, emailVerified: true }).extend({ email: z.email() }),
async (input) => {
await Database.use(async (tx) => {
await tx
.update(UserTable)
.set({
email: input.email,
emailVerified: input.emailVerified ?? false
})
.where(eq(UserTable.id, input.id));
});
}
);
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(UserTable)
.set({ timeDeleted: sql`now()` })
.where(eq(UserTable.id, id));
});
});
export function serialize(input: typeof UserTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
email: input.email,
emailVerified: input.emailVerified,
image: input.image
};
}
}

View File

@@ -0,0 +1,27 @@
import { index, integer, pgTable, uniqueIndex } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid, utc } from '../db/types.js';
import { GameTable } from '../game/game.sql.js';
import { UserTable } from '../user/user.sql.js';
export const UserLibraryTable = pgTable(
'user_library',
{
...id,
...timestamps,
userId: ulid('user_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
gameId: ulid('game_id')
.notNull()
.references(() => GameTable.id, { onDelete: 'cascade' }),
playtime2w: integer('playtime_2w'),
playtimeForever: integer('playtime_forever'),
lastPlayed: utc('last_played')
},
(t) => [
uniqueIndex('user_library_user_game_unique').on(t.userId, t.gameId),
index('user_library_user_idx').on(t.userId),
index('user_library_game_idx').on(t.gameId)
]
);

View File

@@ -0,0 +1,234 @@
import { eq, and, isNull, sql, inArray } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { GameDownload } from '../game/download.js';
import { GameDownloadTable } from '../game/download.sql.js';
import { GameTable } from '../game/game.sql.js';
import { Game } from '../game/index.js';
import { UserLibraryTable } from './library.sql.js';
export namespace Library {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the library entry',
example: Examples.Library.id
}),
userId: z.string().meta({
description: 'The user who owns this game',
example: Examples.Library.userId
}),
gameId: z.string().meta({
description: 'The game in the library',
example: Examples.Library.gameId
}),
playtime2w: z.number().int().nullable().optional().meta({
description: 'Playtime in seconds over the last 2 weeks',
example: Examples.Library.playtime2w
}),
playtimeForever: z.number().int().nullable().optional().meta({
description: 'Total playtime in seconds',
example: Examples.Library.playtimeForever
}),
lastPlayed: z.string().nullable().optional().meta({
description: 'Last time the game was played (ISO 8601)',
example: Examples.Library.lastPlayed
})
})
.meta({
ref: 'Library',
description: 'Links a user to a game in their library with playtime info',
example: Examples.Library
});
export type Info = z.infer<typeof Info>;
export const create = fn(Info, async (input) => {
await Database.use(async (tx) => {
await tx.insert(UserLibraryTable).values({
id: input.id,
userId: input.userId,
gameId: input.gameId,
playtime2w: input.playtime2w ?? null,
playtimeForever: input.playtimeForever ?? null,
lastPlayed: input.lastPlayed ? new Date(input.lastPlayed) : null
});
});
return input.id;
});
export const upsert = fn(
Info.pick({
id: true,
userId: true,
gameId: true,
playtime2w: true,
playtimeForever: true,
lastPlayed: true
}),
async (input) => {
await Database.use(async (tx) => {
await tx
.insert(UserLibraryTable)
.values({
id: input.id,
userId: input.userId,
gameId: input.gameId,
playtime2w: input.playtime2w ?? null,
playtimeForever: input.playtimeForever ?? null,
lastPlayed: input.lastPlayed ? new Date(input.lastPlayed) : null
})
.onConflictDoUpdate({
target: [UserLibraryTable.userId, UserLibraryTable.gameId],
set: {
playtime2w: input.playtime2w ?? null,
playtimeForever: input.playtimeForever ?? null,
lastPlayed: input.lastPlayed ? new Date(input.lastPlayed) : null
}
});
});
return input.id;
}
);
export const fromID = fn(Info.shape.id, async (id) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserLibraryTable)
.where(and(eq(UserLibraryTable.id, id), isNull(UserLibraryTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export const findByUserAndGame = fn(Info.pick({ userId: true, gameId: true }), async (input) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserLibraryTable)
.where(
and(
eq(UserLibraryTable.userId, input.userId),
eq(UserLibraryTable.gameId, input.gameId),
isNull(UserLibraryTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
});
export const listByUser = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserLibraryTable)
.where(and(eq(UserLibraryTable.userId, userId), isNull(UserLibraryTable.timeDeleted)))
.orderBy(UserLibraryTable.timeCreated);
});
});
export const listByUserWithGames = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
const rows = await tx
.select({
library: UserLibraryTable,
game: GameTable
})
.from(UserLibraryTable)
.leftJoin(GameTable, eq(UserLibraryTable.gameId, GameTable.id))
.where(and(eq(UserLibraryTable.userId, userId), isNull(UserLibraryTable.timeDeleted)))
.orderBy(UserLibraryTable.timeCreated);
const gameIds = [
...new Set(rows.filter((row) => row.game !== null).map((row) => row.library.gameId))
];
const downloadRows =
gameIds.length > 0
? await tx
.select()
.from(GameDownloadTable)
.where(
and(
inArray(GameDownloadTable.gameId, gameIds),
isNull(GameDownloadTable.timeDeleted)
)
)
: [];
// A game may have one state row per host; surface the most recently
// updated state for the library listing.
const downloadByGame = new Map<string, (typeof downloadRows)[number]>();
for (const row of downloadRows) {
const existing = downloadByGame.get(row.gameId);
if (!existing || row.timeUpdated > existing.timeUpdated) {
downloadByGame.set(row.gameId, row);
}
}
return rows
.filter((row) => row.game !== null)
.map((row) => {
const download = downloadByGame.get(row.library.gameId);
return {
id: row.library.id,
game: Game.serialize(row.game!),
playtime2w: row.library.playtime2w,
playtimeForever: row.library.playtimeForever,
lastPlayed: row.library.lastPlayed?.toISOString() ?? null,
download: download ? GameDownload.serialize(download) : null
};
});
});
});
export const listByGame = fn(Info.shape.gameId, async (gameId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(UserLibraryTable)
.where(and(eq(UserLibraryTable.gameId, gameId), isNull(UserLibraryTable.timeDeleted)));
});
});
export const listByUserAndGameIDs = fn(
z.object({ userId: z.string(), gameIds: z.array(z.string()) }),
async (input) => {
if (input.gameIds.length === 0) return [];
return Database.use(async (tx) => {
return tx
.select()
.from(UserLibraryTable)
.where(
and(
eq(UserLibraryTable.userId, input.userId),
inArray(UserLibraryTable.gameId, input.gameIds),
isNull(UserLibraryTable.timeDeleted)
)
);
});
}
);
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(UserLibraryTable)
.set({ timeDeleted: sql`now()` })
.where(eq(UserLibraryTable.id, id));
});
});
export function serialize(input: typeof UserLibraryTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
userId: input.userId,
gameId: input.gameId,
playtime2w: input.playtime2w,
playtimeForever: input.playtimeForever,
lastPlayed: input.lastPlayed?.toISOString() ?? null
};
}
}

View File

@@ -0,0 +1,24 @@
import { index, jsonb, pgEnum, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid } from '../db/types.js';
import { UserTable } from '../user/user.sql.js';
export const ProviderEnum = pgEnum('linked_account_provider', ['steam', 'ssh', 'discord']);
export const LinkedAccountTable = pgTable(
'linked_account',
{
...id,
...timestamps,
userId: ulid('user_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
provider: ProviderEnum('provider').notNull(),
providerAccountId: text('provider_account_id').notNull(),
profile: jsonb('profile').$type<{}>()
},
(t) => [
uniqueIndex('linked_account_provider_unique').on(t.provider, t.providerAccountId),
index('linked_account_user_idx').on(t.userId)
]
);

View File

@@ -0,0 +1,167 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { LinkedAccountTable, ProviderEnum } from './linked-account.sql.js';
export namespace LinkedAccount {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the linked account record',
example: Examples.LinkedAccount.id
}),
userId: z.string().meta({
description: 'The user this account belongs to',
example: Examples.LinkedAccount.userId
}),
provider: z.enum(ProviderEnum.enumValues).meta({
description: 'Authentication provider',
example: Examples.LinkedAccount.provider
}),
providerAccountId: z.string().meta({
description: 'The account ID from the provider',
example: Examples.LinkedAccount.providerAccountId
}),
profile: z.record(z.string(), z.unknown()).nullable().optional().meta({
description: 'Platform-specific profile data (name, avatar, etc.)',
example: Examples.LinkedAccount.profile
})
})
.meta({
ref: 'LinkedAccount',
description: 'A linked gaming or OAuth identity (Steam, Epic Games, GitHub, Discord, etc.)',
example: Examples.LinkedAccount
});
export type Info = z.infer<typeof Info>;
export const create = fn(Info, async (input) => {
await Database.use(async (tx) => {
await tx.insert(LinkedAccountTable).values({
id: input.id,
userId: input.userId,
provider: input.provider,
providerAccountId: input.providerAccountId,
profile: input.profile ?? null
});
});
return input.id;
});
export const fromID = fn(Info.shape.id, async (id) => {
return Database.use(async (tx) => {
return tx
.select()
.from(LinkedAccountTable)
.where(and(eq(LinkedAccountTable.id, id), isNull(LinkedAccountTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export const findByProvider = fn(
Info.pick({ provider: true, providerAccountId: true }),
async (input) => {
return Database.use(async (tx) => {
return tx
.select()
.from(LinkedAccountTable)
.where(
and(
eq(LinkedAccountTable.provider, input.provider),
eq(LinkedAccountTable.providerAccountId, input.providerAccountId),
isNull(LinkedAccountTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
}
);
export const findSshByFingerprint = fn(Info.shape.providerAccountId, async (fingerprint) => {
return Database.use(async (tx) => {
return tx
.select()
.from(LinkedAccountTable)
.where(
and(
eq(LinkedAccountTable.provider, 'ssh'),
eq(LinkedAccountTable.providerAccountId, fingerprint),
isNull(LinkedAccountTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
});
export const findSteamByUser = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(LinkedAccountTable)
.where(
and(
eq(LinkedAccountTable.userId, userId),
eq(LinkedAccountTable.provider, 'steam'),
isNull(LinkedAccountTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
});
export const repoint = fn(Info.pick({ id: true, userId: true }), async (input) => {
await Database.use(async (tx) => {
await tx
.update(LinkedAccountTable)
.set({
userId: input.userId,
timeUpdated: sql`now()`
})
.where(and(eq(LinkedAccountTable.id, input.id), isNull(LinkedAccountTable.timeDeleted)));
});
});
export const updateProfile = fn(Info.pick({ id: true, profile: true }), async (input) => {
await Database.use(async (tx) => {
await tx
.update(LinkedAccountTable)
.set({
profile: input.profile ?? null,
timeUpdated: sql`now()`
})
.where(and(eq(LinkedAccountTable.id, input.id), isNull(LinkedAccountTable.timeDeleted)));
});
});
export const listByUser = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(LinkedAccountTable)
.where(and(eq(LinkedAccountTable.userId, userId), isNull(LinkedAccountTable.timeDeleted)))
.orderBy(LinkedAccountTable.timeCreated);
});
});
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(LinkedAccountTable)
.set({ timeDeleted: sql`now()` })
.where(eq(LinkedAccountTable.id, id));
});
});
export function serialize(input: typeof LinkedAccountTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
userId: input.userId,
provider: input.provider,
providerAccountId: input.providerAccountId,
profile: input.profile
};
}
}

View File

@@ -0,0 +1,12 @@
import { boolean, pgTable, text } from 'drizzle-orm/pg-core';
import { id, timestamps } from '../db/types.js';
export const UserTable = pgTable('user', {
...id,
...timestamps,
name: text('name').notNull(),
email: text('email'),
emailVerified: boolean('email_verified').notNull().default(false),
image: text('image')
});