Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/adamantite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: "24"
node-version-file: ".node-version"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a conscious call: nvm reads only .nvmrc and does not read .node-version, while fnm, nodenv, and setup-node read both. After this rename, contributors on nvm lose nvm use auto-resolution. adamantite@0.35.0 accepts .nvmrc through node-version-file too, so the rename was optional — if any contributor uses nvm, committing both files with identical contents keeps every version manager working, and nothing in the repo references either filename today.


- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Cache dependencies
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: |
~/.bun/install/cache
Expand All @@ -57,7 +57,7 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Generate source and types
- name: Generate source files and types
run: bun run codegen

- name: Run ${{ matrix.name }}
Expand Down
File renamed without changes.
57 changes: 38 additions & 19 deletions apps/api/src/shared/files.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { DeleteManyResult, StoredFile, UploadResult } from "files-sdk"
import { operators } from "@init/db/helpers"
import { assets, type UserId } from "@init/db/schema"
import * as z from "@init/utils/schema"
Expand All @@ -14,6 +13,24 @@ import { context } from "#shared/utils.ts"
export const FILES_MAX_UPLOAD_SIZE = 10 * 1024 * 1024
export const FILES_MAX_URL_AGE = 15 * 60

const userIdSchema = z.branded("UserId")
const uploadResultSchema = z.object({
contentType: z.string(),
etag: z.string(),
lastModified: z.number(),
metadata: z.record(z.string(), z.string()).optional(),
name: z.string(),
size: z.number(),
})
const storedFileSchema = z.object({
etag: z.string(),
lastModified: z.number(),
metadata: z.record(z.string(), z.string()).optional(),
size: z.number(),
type: z.string(),
})
const deleteManyResultSchema = z.object({ deleted: z.array(z.string()) })

export const files = createFiles({
adapter: bunS3({
accessKeyId: ENV.S3_ACCESS_KEY_ID,
Expand All @@ -29,13 +46,13 @@ export const files = createFiles({

switch (event.type) {
case "upload":
if (event.key) handleUpload(event.key, event.result as UploadResult)
if (event.key) handleUpload(event.key, uploadResultSchema.parse(event.result))
break
case "head":
if (event.key) handleUpload(event.key, event.result as StoredFile)
if (event.key) handleUpload(event.key, storedFileSchema.parse(event.result))
break
case "delete": {
const keys = event.key ? [event.key] : (event.result as DeleteManyResult).deleted
const keys = event.key ? [event.key] : deleteManyResultSchema.parse(event.result).deleted

handleDelete(keys)
break
Expand Down Expand Up @@ -67,10 +84,17 @@ export const files = createFiles({
],
})

function handleUpload(key: string, file: UploadResult | StoredFile) {
type ParsedUploadResult = z.infer<typeof uploadResultSchema>
type ParsedStoredFile = z.infer<typeof storedFileSchema>

function handleUpload(key: string, file: ParsedUploadResult | ParsedStoredFile) {
const ctx = context<AuthenticatedAppContext>()
const mimeType = "contentType" in file ? file.contentType : file.type
const name = "name" in file ? file.name : (key.split("/").at(-1) ?? key)
const userId: UserId = userIdSchema.parse(ctx.var.session.user.id)
const logFailure = (cause: unknown) => {
ctx.var.logger.error(`Failed to record asset: ${String(cause)}`)
}

void ctx.var.db
.insert(assets)
Expand All @@ -80,10 +104,10 @@ function handleUpload(key: string, file: UploadResult | StoredFile) {
lastModified: file.lastModified,
metadata: "metadata" in file ? file.metadata : undefined,
name,
ownerId: ctx.var.session.user.id as UserId,
ownerId: userId,
size: file.size,
type: mimeType,
uploaderId: ctx.var.session.user.id as UserId,
uploaderId: userId,
})
.onConflictDoUpdate({
set: {
Expand All @@ -97,25 +121,20 @@ function handleUpload(key: string, file: UploadResult | StoredFile) {
},
target: assets.key,
})
.catch((error: unknown) => {
ctx.var.logger.error(`Failed to record asset: ${String(error)}`)
})
.catch(logFailure)
}

function handleDelete(keys: string[]) {
if (keys.length === 0) return

const ctx = context<AuthenticatedAppContext>()
const userId: UserId = userIdSchema.parse(ctx.var.session.user.id)
const logFailure = (cause: unknown) => {
ctx.var.logger.error(`Failed to delete asset records: ${String(cause)}`)
}

void ctx.var.db
.delete(assets)
.where(
operators.and(
operators.inArray(assets.key, keys),
operators.eq(assets.ownerId, ctx.var.session.user.id as UserId)
)
)
.catch((error: unknown) => {
ctx.var.logger.error(`Failed to delete asset records: ${String(error)}`)
})
.where(operators.and(operators.inArray(assets.key, keys), operators.eq(assets.ownerId, userId)))
.catch(logFailure)
}
9 changes: 6 additions & 3 deletions apps/api/src/shared/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ export function createTRPCContext(opts: FetchCreateContextFnOptions, c: Context<
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>

export const t = initTRPC.context<TRPCContext>().create({
errorFormatter({ shape, error }) {
errorFormatter(formatterInput) {
const { error } = formatterInput
const formattedError = formatterInput["shape"]

return {
...shape,
...formattedError,
data: {
...shape.data,
...formattedError.data,
zodError: error.cause instanceof z.ZodError ? error.cause.flatten() : null,
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useForm } from "@init/ui/components/form"
import { toast } from "@init/ui/components/toast"
import { useServerFn } from "@tanstack/react-start"
import { forgotPassword } from "#features/auth/server/functions.ts"
import { ForgotPasswordFormSchema as schema } from "#features/auth/validation.ts"
import { EmailSchema, ForgotPasswordFormSchema as schema } from "#features/auth/validation.ts"

export default function ForgotPasswordForm() {
const execute = useServerFn(forgotPassword)
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function ForgotPasswordForm() {
>
<form.AppForm>
<FieldGroup>
<form.AppField name="email" validators={{ onBlur: schema.shape.email }}>
<form.AppField name="email" validators={{ onBlur: EmailSchema }}>
{(field) => (
<field.Field>
<field.Label>Email address</field.Label>
Expand Down
9 changes: 3 additions & 6 deletions apps/app/src/features/auth/components/reset-password-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FieldGroup } from "@init/ui/components/field"
import { useForm } from "@init/ui/components/form"
import { toast } from "@init/ui/components/toast"
import { useNavigate } from "@tanstack/react-router"
import { ResetPasswordFormSchema as schema } from "#features/auth/validation.ts"
import { PasswordSchema, ResetPasswordFormSchema as schema } from "#features/auth/validation.ts"
import { authClient } from "#shared/auth.ts"

export default function ResetPasswordForm({ token }: { token: string }) {
Expand Down Expand Up @@ -39,7 +39,7 @@ export default function ResetPasswordForm({ token }: { token: string }) {
>
<form.AppForm>
<FieldGroup>
<form.AppField name="password" validators={{ onBlur: schema.shape.password }}>
<form.AppField name="password" validators={{ onBlur: PasswordSchema }}>
{(field) => (
<field.Field>
<field.Label>New password</field.Label>
Expand All @@ -48,10 +48,7 @@ export default function ResetPasswordForm({ token }: { token: string }) {
</field.Field>
)}
</form.AppField>
<form.AppField
name="confirmPassword"
validators={{ onBlur: schema.shape.confirmPassword }}
>
<form.AppField name="confirmPassword" validators={{ onBlur: PasswordSchema }}>
{(field) => (
<field.Field>
<field.Label>Confirm new password</field.Label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { useForm } from "@init/ui/components/form"
import { toast } from "@init/ui/components/toast"
import { Link, useNavigate } from "@tanstack/react-router"
import { AUTHENTICATED_PATHNAME } from "#features/auth/constants.ts"
import { SignInWithPasswordFormSchema as schema } from "#features/auth/validation.ts"
import {
EmailSchema,
PasswordSchema,
SignInWithPasswordFormSchema as schema,
} from "#features/auth/validation.ts"
import { signIn } from "#shared/auth.ts"

export default function SignInWithPasswordForm() {
Expand Down Expand Up @@ -37,7 +41,7 @@ export default function SignInWithPasswordForm() {
>
<form.AppForm>
<FieldGroup>
<form.AppField name="email" validators={{ onBlur: schema.shape.email }}>
<form.AppField name="email" validators={{ onBlur: EmailSchema }}>
{(field) => (
<field.Field>
<field.Label>Email address</field.Label>
Expand All @@ -47,7 +51,7 @@ export default function SignInWithPasswordForm() {
</field.Field>
)}
</form.AppField>
<form.AppField name="password" validators={{ onBlur: schema.shape.password }}>
<form.AppField name="password" validators={{ onBlur: PasswordSchema }}>
{(field) => (
<field.Field>
<field.Label>Password</field.Label>
Expand Down
22 changes: 12 additions & 10 deletions apps/app/src/features/auth/components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useForm } from "@init/ui/components/form"
import { useNavigate } from "@tanstack/react-router"
import { AUTHENTICATED_PATHNAME } from "#features/auth/constants.ts"
import { checkEmailAvailability } from "#features/auth/server/functions.ts"
import { SignUpFormSchema as schema } from "#features/auth/validation.ts"
import {
EmailSchema,
NameSchema,
PasswordSchema,
SignUpFormSchema as schema,
} from "#features/auth/validation.ts"
import { signUp } from "#shared/auth.ts"

export default function SignUpForm() {
Expand Down Expand Up @@ -32,7 +37,7 @@ export default function SignUpForm() {
>
<form.AppForm>
<FieldGroup>
<form.AppField name="name" validators={{ onBlur: schema.shape.name }}>
<form.AppField name="name" validators={{ onBlur: NameSchema }}>
{(field) => (
<field.Field>
<field.Label>Name</field.Label>
Expand All @@ -46,7 +51,7 @@ export default function SignUpForm() {
<form.AppField
name="email"
validators={{
onBlur: schema.shape.email,
onBlur: EmailSchema,
onBlurAsync: async ({ value }) => {
const { isAvailable } = await checkEmailAvailability({
data: {
Expand All @@ -71,7 +76,7 @@ export default function SignUpForm() {
</field.Field>
)}
</form.AppField>
<form.AppField name="password" validators={{ onBlur: schema.shape.password }}>
<form.AppField name="password" validators={{ onBlur: PasswordSchema }}>
{(field) => (
<field.Field>
<field.Label>Password</field.Label>
Expand All @@ -85,12 +90,9 @@ export default function SignUpForm() {
<form.AppField
name="confirmPassword"
validators={{
onBlur: schema.shape.confirmPassword.refine(
(v) => v === form.getFieldValue("password"),
{
message: "Passwords don't match",
}
),
onBlur: PasswordSchema.refine((v) => v === form.getFieldValue("password"), {
message: "Passwords don't match",
}),
onBlurListenTo: ["password"],
onChangeListenTo: ["password"],
}}
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/features/auth/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export const PasswordSchema = z
.min(8, { error: "Password must be more than 8 characters" })
.max(32, { error: "Password must be less than 32 characters" })

export const NameSchema = z.string().min(1, { error: "Name is required" })

export const SignUpFormSchema = z.object({
confirmPassword: PasswordSchema,
email: EmailSchema,
name: z.string().min(1, { error: "Name is required" }),
name: NameSchema,
password: PasswordSchema,
})

Expand Down
61 changes: 33 additions & 28 deletions apps/docs/src/shared/components/last-updated.astro
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
---
import { spawnSync } from "node:child_process"
import { basename, dirname, resolve } from "node:path"
import { spawnSync } from "node:child_process";
import { basename, dirname, resolve } from "node:path";

const { entry, lang, lastUpdated: routeLastUpdated } = Astro.locals.starlightRoute
const {
entry,
lang,
lastUpdated: routeLastUpdated,
} = Astro.locals.starlightRoute;
const lastUpdated =
routeLastUpdated ?? (entry.filePath ? getLastUpdated(entry.filePath) : undefined)
routeLastUpdated ??
(entry.filePath ? getLastUpdated(entry.filePath) : undefined);

function getLastUpdated(filePath: string) {
const absolutePath = resolve(import.meta.dirname, "../../..", filePath)
const result = spawnSync(
"git",
["log", "--format=%ct", "--max-count=1", basename(absolutePath)],
{
cwd: dirname(absolutePath),
encoding: "utf8",
},
)
const timestamp = typeof result.stdout === "string" ? Number(result.stdout.trim()) : Number.NaN
return result.status === 0 && Number.isFinite(timestamp) && timestamp > 0
? new Date(timestamp * 1000)
: undefined
const absolutePath = resolve(import.meta.dirname, "../../..", filePath);
const result = spawnSync(
"git",
["log", "--format=%ct", "--max-count=1", basename(absolutePath)],
{
cwd: dirname(absolutePath),
encoding: "utf8",
},
);
const timestamp = Number(result.stdout?.trim() ?? Number.NaN);
return result.status === 0 && Number.isFinite(timestamp) && timestamp > 0
? new Date(timestamp * 1000)
: undefined;
}
---

{
lastUpdated && (
<p>
{Astro.locals.t("page.lastUpdated")} {" "}
<time datetime={lastUpdated.toISOString()}>
{lastUpdated.toLocaleDateString(lang, {
dateStyle: "medium",
timeZone: "UTC",
})}
</time>
</p>
)
lastUpdated && (
<p>
{Astro.locals.t("page.lastUpdated")}{" "}
<time datetime={lastUpdated.toISOString()}>
{lastUpdated.toLocaleDateString(lang, {
dateStyle: "medium",
timeZone: "UTC",
})}
</time>
</p>
)
}
3 changes: 1 addition & 2 deletions apps/mobile/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ function checkIsLocale(value: string): value is Locale {
}

export default function Screen() {
const backgroundValue = useCSSVariable("--color-background")
const background = typeof backgroundValue === "string" ? backgroundValue : undefined
const background = useCSSVariable("--color-background")?.toString()
const [isSearchFocused, setIsSearchFocused] = useState(false)
const [locale, setLocale] = useState<Locale>(() => getLocale())
const [searchQuery, setSearchQuery] = useState("")
Expand Down
Loading