mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-20 09:45:21 +03:00
Squashes the current state of the internal working tree onto this history. The two trees had grown apart with no common ancestor, so this is a content sync rather than a merge, and the published history is preserved rather than rewritten — a force-push here would break every existing fork and clone to no benefit. What lands: - Waitlist: API route, core module, and migration 0006 alongside game aliases. - User verification. - CI, oxfmt config, editor settings. - Assorted fixes across the API routes and core modules. The repository's own README, the wordmark and the per-package READMEs are kept from this side; the internal tree had dropped them and they are what a stranger arriving here reads first. The marketing site in the internal tree is deliberately not here. It is a separate product with its own repo and its own licence, and this repo is the open one — a closed component does not belong in it regardless of how convenient the directory looked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
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 setEmail = fn(
|
|
Info.pick({ id: true }).extend({ email: z.email(), emailVerified: z.boolean() }),
|
|
async (input) => {
|
|
return Database.use(async (tx) => {
|
|
const [row] = await tx
|
|
.update(UserTable)
|
|
.set({
|
|
email: input.email,
|
|
emailVerified: input.emailVerified
|
|
})
|
|
.where(eq(UserTable.id, input.id))
|
|
.returning();
|
|
return row ? serialize(row) : null;
|
|
});
|
|
}
|
|
);
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|