chore: server-and-serverless template
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
.wrangler
|
||||
|
||||
# Generated by @pikku/cli — regenerated on `pikku all`.
|
||||
# Ignore the contents (not the directory) so negation below can whitelist
|
||||
# the `api.gen.ts` stub that keeps the skeleton compilable pre-`pikku all`.
|
||||
packages/functions/.pikku/client/*
|
||||
!packages/functions/.pikku/client/api.gen.ts
|
||||
|
||||
# Scaffold wirings are generated by `pikku all` — never commit them.
|
||||
packages/functions/src/scaffold/*.gen.ts
|
||||
packages/functions/src/scaffold/*.gen.js
|
||||
|
||||
# Yarn — commit the root lockfile; ignore PnP/cache and generated per-unit lockfiles
|
||||
.yarn/
|
||||
.pnp.*
|
||||
yarn.lock
|
||||
!/yarn.lock
|
||||
|
||||
# Vendored packages injected at sandbox runtime — never commit
|
||||
vendor/
|
||||
BIN
.pikku-runtime/dev.db
Normal file
BIN
.pikku-runtime/dev.db
Normal file
Binary file not shown.
4
.yarnrc.yml
Normal file
4
.yarnrc.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Force node_modules linker — some toolchain deps (uWebSockets.js native
|
||||
# addon, pikku CLI bin, esbuild native) resolve cleanly only with a
|
||||
# traditional tree.
|
||||
nodeLinker: node-modules
|
||||
61
apps/app/messages/en.json
Normal file
61
apps/app/messages/en.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"common__loading": "Loading…",
|
||||
"common__email": "Email",
|
||||
"common__email_placeholder": "you@example.com",
|
||||
"common__password": "Password",
|
||||
"common__password_placeholder": "Your password",
|
||||
"landing__eyebrow": "Starter template",
|
||||
"landing__title": "Start with the smallest real Pikku app.",
|
||||
"landing__subtitle": "This starter gives an AI the full path: routed frontend, generated data hooks, auth, session guard, and a tiny backend it can safely expand.",
|
||||
"landing__create_account": "Create account",
|
||||
"landing__sign_in": "Sign in",
|
||||
"landing__cards__frontend__title": "Frontend",
|
||||
"landing__cards__frontend__description": "Vite, TanStack Router, React Query, and generated Pikku hooks.",
|
||||
"landing__cards__backend__title": "Backend",
|
||||
"landing__cards__backend__description": "One protected read function, one update function, and matching HTTP routes.",
|
||||
"landing__cards__auth__title": "Auth",
|
||||
"landing__cards__auth__description": "Email and password login, cookie sessions, and a protected app route.",
|
||||
"auth__eyebrow": "Starter template",
|
||||
"auth__login__title": "Welcome back",
|
||||
"auth__login__description": "Sign in with your email and password.",
|
||||
"auth__login__cta": "Sign in",
|
||||
"auth__login__invalid_credentials": "That email and password don't match.",
|
||||
"auth__login__error": "Unable to sign in right now.",
|
||||
"auth__login__footer_prompt": "Need an account?",
|
||||
"auth__login__footer_action": "Create one",
|
||||
"auth__signup__title": "Create your account",
|
||||
"auth__signup__description": "Pick an email and password to get started.",
|
||||
"auth__signup__cta": "Create account",
|
||||
"auth__signup__email_in_use": "An account with this email already exists.",
|
||||
"auth__signup__error": "Unable to create your account right now.",
|
||||
"auth__signup__footer_prompt": "Already have an account?",
|
||||
"auth__signup__footer_action": "Sign in",
|
||||
"auth__verify_email__title": "Confirm your email",
|
||||
"auth__verify_email__verifying": "Confirming your email…",
|
||||
"auth__verify_email__success": "Your email is confirmed. You're all set.",
|
||||
"auth__verify_email__error": "This confirmation link is invalid or has expired.",
|
||||
"auth__verify_email__no_token": "This page needs a confirmation link from your email.",
|
||||
"auth__verify_email__back_to_app": "Go to your app",
|
||||
"app_shell__eyebrow": "Pikku + Vite + Auth.js",
|
||||
"app_shell__title": "Starter",
|
||||
"app_shell__signed_in": "Signed in",
|
||||
"app_shell__sign_out": "Sign out",
|
||||
"app_shell__nav__message": "Message",
|
||||
"app_shell__nav__message_description": "Starter read + update flow",
|
||||
"preferences__language": "Language",
|
||||
"preferences__theme": "Theme",
|
||||
"message__loading": "Loading your starter app…",
|
||||
"message__load_error": "The starter app could not load its message.",
|
||||
"message__read__eyebrow": "Read hook",
|
||||
"message__read__title": "Current message",
|
||||
"message__read__description": "This data comes from a generated `usePikkuQuery` hook.",
|
||||
"message__read__last_updated_by": "Last updated by {name}",
|
||||
"message__read__nobody": "Nobody yet",
|
||||
"message__update__eyebrow": "Mutate hook",
|
||||
"message__update__title": "Update the message",
|
||||
"message__update__description": "This form calls a generated `usePikkuMutation` hook and refreshes the read state.",
|
||||
"message__update__field_label": "Message",
|
||||
"message__update__placeholder": "Write the next starter message",
|
||||
"message__update__cta": "Update message"
|
||||
}
|
||||
61
apps/app/messages/fr.json
Normal file
61
apps/app/messages/fr.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"common__loading": "Chargement…",
|
||||
"common__email": "Email",
|
||||
"common__email_placeholder": "vous@exemple.com",
|
||||
"common__password": "Mot de passe",
|
||||
"common__password_placeholder": "Votre mot de passe",
|
||||
"landing__eyebrow": "Modèle de démarrage",
|
||||
"landing__title": "Commencez avec la plus petite vraie application Pikku.",
|
||||
"landing__subtitle": "Ce modèle donne à une IA le chemin complet : frontend routé, hooks de données générés, auth, garde de session et un petit backend qu'elle peut étendre.",
|
||||
"landing__create_account": "Créer un compte",
|
||||
"landing__sign_in": "Se connecter",
|
||||
"landing__cards__frontend__title": "Frontend",
|
||||
"landing__cards__frontend__description": "Vite, TanStack Router, React Query et des hooks Pikku générés.",
|
||||
"landing__cards__backend__title": "Backend",
|
||||
"landing__cards__backend__description": "Une fonction de lecture protégée, une fonction de mise à jour et les routes HTTP correspondantes.",
|
||||
"landing__cards__auth__title": "Authentification",
|
||||
"landing__cards__auth__description": "Connexion par email et mot de passe, sessions en cookies et route d'application protégée.",
|
||||
"auth__eyebrow": "Modèle de démarrage",
|
||||
"auth__login__title": "Bon retour",
|
||||
"auth__login__description": "Connectez-vous avec votre email et votre mot de passe.",
|
||||
"auth__login__cta": "Se connecter",
|
||||
"auth__login__invalid_credentials": "Cet email et ce mot de passe ne correspondent pas.",
|
||||
"auth__login__error": "Impossible de se connecter pour l'instant.",
|
||||
"auth__login__footer_prompt": "Pas encore de compte ?",
|
||||
"auth__login__footer_action": "En créer un",
|
||||
"auth__signup__title": "Créez votre compte",
|
||||
"auth__signup__description": "Choisissez un email et un mot de passe pour commencer.",
|
||||
"auth__signup__cta": "Créer un compte",
|
||||
"auth__signup__email_in_use": "Un compte avec cet email existe déjà.",
|
||||
"auth__signup__error": "Impossible de créer votre compte pour l'instant.",
|
||||
"auth__signup__footer_prompt": "Vous avez déjà un compte ?",
|
||||
"auth__signup__footer_action": "Se connecter",
|
||||
"auth__verify_email__title": "Confirmez votre email",
|
||||
"auth__verify_email__verifying": "Confirmation de votre email…",
|
||||
"auth__verify_email__success": "Votre email est confirmé. Tout est prêt.",
|
||||
"auth__verify_email__error": "Ce lien de confirmation est invalide ou a expiré.",
|
||||
"auth__verify_email__no_token": "Cette page nécessite un lien de confirmation depuis votre email.",
|
||||
"auth__verify_email__back_to_app": "Aller à votre application",
|
||||
"app_shell__eyebrow": "Pikku + Vite + Auth.js",
|
||||
"app_shell__title": "Starter",
|
||||
"app_shell__signed_in": "Connecté",
|
||||
"app_shell__sign_out": "Se déconnecter",
|
||||
"app_shell__nav__message": "Message",
|
||||
"app_shell__nav__message_description": "Flux lecture + mise à jour de démarrage",
|
||||
"message__loading": "Chargement de votre application…",
|
||||
"message__load_error": "L'application n'a pas pu charger son message.",
|
||||
"message__read__eyebrow": "Hook de lecture",
|
||||
"message__read__title": "Message actuel",
|
||||
"message__read__description": "Ces données proviennent d'un hook `usePikkuQuery` généré.",
|
||||
"message__read__last_updated_by": "Dernière mise à jour par {name}",
|
||||
"message__read__nobody": "Personne encore",
|
||||
"message__update__eyebrow": "Hook de mutation",
|
||||
"message__update__title": "Modifier le message",
|
||||
"message__update__description": "Ce formulaire appelle un hook `usePikkuMutation` généré et actualise l'état de lecture.",
|
||||
"message__update__field_label": "Message",
|
||||
"message__update__placeholder": "Écrivez le prochain message de démarrage",
|
||||
"message__update__cta": "Mettre à jour le message",
|
||||
"preferences__language": "Langue",
|
||||
"preferences__theme": "Thème"
|
||||
}
|
||||
36
apps/app/package.json
Normal file
36
apps/app/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@project/app",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 7104",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 7104",
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^9.2.1",
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@pikku/mantine": "^0.12.5",
|
||||
"@pikku/react": "^0.12.4",
|
||||
"@project/functions-sdk": "workspace:*",
|
||||
"@project/mantine-themes": "workspace:*",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@tanstack/react-router": "^1.132.0",
|
||||
"@tanstack/react-start": "^1.132.0",
|
||||
"better-auth": "^1.6.18",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@inlang/paraglide-js": "^2.20.0",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"typescript": "^5.9",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
11
apps/app/project.inlang/settings.json
Normal file
11
apps/app/project.inlang/settings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"baseLocale": "en",
|
||||
"locales": ["en", "fr"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
}
|
||||
}
|
||||
103
apps/app/src/components/AppShell.tsx
Normal file
103
apps/app/src/components/AppShell.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
AppShell as MantineAppShell,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
NavLink,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@pikku/mantine/core'
|
||||
import { Link, Outlet } from '@tanstack/react-router'
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { useState } from 'react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { usePikkuQuery } from '@project/functions-sdk/pikku/api.gen'
|
||||
import { signOut } from '@/lib/auth'
|
||||
import { LanguageSelector } from './LanguageSelector'
|
||||
import { ThemeSelector } from './ThemeSelector'
|
||||
|
||||
export function AppShell() {
|
||||
useLocale()
|
||||
const session = usePikkuQuery('getSession', {})
|
||||
const [isSigningOut, setIsSigningOut] = useState(false)
|
||||
|
||||
return (
|
||||
<MantineAppShell
|
||||
header={{ height: 88 }}
|
||||
navbar={{ width: 250, breakpoint: 'sm' }}
|
||||
padding="lg"
|
||||
styles={{
|
||||
main: {
|
||||
background:
|
||||
'radial-gradient(circle at top left, rgba(96, 165, 250, 0.12), transparent 26%), linear-gradient(180deg, #10203a 0%, #07111e 56%, #040913 100%)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MantineAppShell.Header px="lg" py="md">
|
||||
<Group justify="space-between" align="center" h="100%">
|
||||
<div>
|
||||
<Text tt="uppercase" fw={700} fz="xs" c="blue.3" style={{ letterSpacing: '0.18em' }}>
|
||||
{m.app_shell__eyebrow()}
|
||||
</Text>
|
||||
<Title order={2}>{m.app_shell__title()}</Title>
|
||||
</div>
|
||||
<Group gap="md">
|
||||
<LanguageSelector />
|
||||
<ThemeSelector />
|
||||
<Box
|
||||
px="md"
|
||||
py="xs"
|
||||
style={{
|
||||
border: '1px solid var(--mantine-color-dark-4)',
|
||||
borderRadius: '1rem',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
}}
|
||||
>
|
||||
<Text size="xs" c="dimmed">
|
||||
{session.data?.name ? asI18n(session.data.name) : m.app_shell__signed_in()}
|
||||
</Text>
|
||||
<Text fw={600}>
|
||||
{session.data?.email ? asI18n(session.data.email) : m.common__loading()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
radius="md"
|
||||
loading={isSigningOut}
|
||||
onClick={async () => {
|
||||
setIsSigningOut(true)
|
||||
try {
|
||||
await signOut()
|
||||
window.location.href = '/login'
|
||||
} finally {
|
||||
setIsSigningOut(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{m.app_shell__sign_out()}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</MantineAppShell.Header>
|
||||
|
||||
<MantineAppShell.Navbar p="md">
|
||||
<Stack gap="xs">
|
||||
<NavLink
|
||||
component={Link}
|
||||
to="/app"
|
||||
label={m.app_shell__nav__message()}
|
||||
description={m.app_shell__nav__message_description()}
|
||||
variant="filled"
|
||||
activeOptions={{ exact: true }}
|
||||
/>
|
||||
</Stack>
|
||||
</MantineAppShell.Navbar>
|
||||
|
||||
<MantineAppShell.Main>
|
||||
<Outlet />
|
||||
</MantineAppShell.Main>
|
||||
</MantineAppShell>
|
||||
)
|
||||
}
|
||||
105
apps/app/src/components/AuthCard.tsx
Normal file
105
apps/app/src/components/AuthCard.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { I18nNode, I18nString } from '@pikku/react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@pikku/mantine/core'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
|
||||
type AuthCardProps = {
|
||||
title: I18nString
|
||||
description: I18nString
|
||||
cta: I18nString
|
||||
email: string
|
||||
password: string
|
||||
// 'current-password' on login (let the browser fill a saved password),
|
||||
// 'new-password' on signup (let it suggest a strong one).
|
||||
passwordAutoComplete: 'current-password' | 'new-password'
|
||||
busy: boolean
|
||||
message: I18nString | null
|
||||
error: I18nString | null
|
||||
footer: I18nNode
|
||||
onEmailChange: (value: string) => void
|
||||
onPasswordChange: (value: string) => void
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void
|
||||
}
|
||||
|
||||
export function AuthCard(props: AuthCardProps) {
|
||||
useLocale()
|
||||
|
||||
return (
|
||||
<Box
|
||||
mih="100vh"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(circle at top left, rgba(96, 165, 250, 0.16), transparent 30%), linear-gradient(180deg, #10203a 0%, #07111e 56%, #040913 100%)',
|
||||
}}
|
||||
py="xl"
|
||||
>
|
||||
<Container size={560}>
|
||||
<Card radius="xl" padding="xl" withBorder shadow="xl" bg="rgba(10, 20, 36, 0.86)">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
c="blue.3"
|
||||
mb="sm"
|
||||
style={{ letterSpacing: '0.18em' }}
|
||||
>
|
||||
{m.auth__eyebrow()}
|
||||
</Text>
|
||||
<Title order={1}>{props.title}</Title>
|
||||
<Text c="dimmed" mt="sm" maw={440}>
|
||||
{props.description}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label={m.common__email()}
|
||||
placeholder={m.common__email_placeholder()}
|
||||
value={props.email}
|
||||
onChange={(event) => props.onEmailChange(event.currentTarget.value)}
|
||||
required
|
||||
radius="md"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
<PasswordInput
|
||||
label={m.common__password()}
|
||||
placeholder={m.common__password_placeholder()}
|
||||
value={props.password}
|
||||
onChange={(event) => props.onPasswordChange(event.currentTarget.value)}
|
||||
required
|
||||
radius="md"
|
||||
autoComplete={props.passwordAutoComplete}
|
||||
/>
|
||||
<Button type="submit" loading={props.busy} radius="md" size="md">
|
||||
{props.cta}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
{props.message ? <Text c="teal.3">{props.message}</Text> : null}
|
||||
{props.error ? <Text c="red.3">{props.error}</Text> : null}
|
||||
|
||||
<Text c="dimmed" size="sm">
|
||||
{props.footer}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
32
apps/app/src/components/LanguageSelector.tsx
Normal file
32
apps/app/src/components/LanguageSelector.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Select } from '@pikku/mantine/core'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { supportedLocales } from '@/i18n/config'
|
||||
import { usePreferences } from '@/contexts/preferences'
|
||||
|
||||
const LOCALE_LABELS: Record<string, string> = {
|
||||
en: 'English',
|
||||
fr: 'Français',
|
||||
}
|
||||
|
||||
export function LanguageSelector() {
|
||||
useLocale()
|
||||
const { locale, setLocale } = usePreferences()
|
||||
|
||||
const data = supportedLocales.map((code) => ({
|
||||
value: code,
|
||||
label: LOCALE_LABELS[code] ?? code.toUpperCase(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={m.preferences__language()}
|
||||
data={data}
|
||||
value={locale}
|
||||
onChange={(v) => v && setLocale(v)}
|
||||
size="xs"
|
||||
w={110}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
51
apps/app/src/components/MessageCard.tsx
Normal file
51
apps/app/src/components/MessageCard.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Card, Stack, Text, Title } from '@pikku/mantine/core'
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
|
||||
type MessageCardProps = {
|
||||
message: string
|
||||
updatedAt: string
|
||||
updatedBy: {
|
||||
email: string
|
||||
name: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export function MessageCard(props: MessageCardProps) {
|
||||
useLocale()
|
||||
const updatedBy = props.updatedBy?.name || props.updatedBy?.email || m.message__read__nobody()
|
||||
|
||||
return (
|
||||
<Card radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
c="blue.3"
|
||||
mb="sm"
|
||||
style={{ letterSpacing: '0.18em' }}
|
||||
>
|
||||
{m.message__read__eyebrow()}
|
||||
</Text>
|
||||
<Title order={2}>{m.message__read__title()}</Title>
|
||||
<Text c="dimmed" mt="sm">
|
||||
{m.message__read__description()}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Card radius="lg" padding="xl" bg="rgba(255,255,255,0.03)" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Title order={1} fz="2.35rem">
|
||||
{asI18n(props.message)}
|
||||
</Title>
|
||||
<Text c="dimmed">{m.message__read__last_updated_by({ name: updatedBy })}</Text>
|
||||
<Text c="dimmed">{asI18n(new Date(props.updatedAt).toLocaleString())}</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
27
apps/app/src/components/ThemeSelector.tsx
Normal file
27
apps/app/src/components/ThemeSelector.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Select } from '@pikku/mantine/core'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { palettes } from '@project/mantine-themes'
|
||||
import { usePreferences } from '@/contexts/preferences'
|
||||
|
||||
export function ThemeSelector() {
|
||||
useLocale()
|
||||
const { themeId, setThemeId } = usePreferences()
|
||||
|
||||
const data = Object.entries(palettes as Record<string, { name: string }>).map(([id, p]) => ({
|
||||
value: id,
|
||||
label: p.name,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={m.preferences__theme()}
|
||||
data={data}
|
||||
value={themeId}
|
||||
onChange={(v) => v && setThemeId(v)}
|
||||
size="xs"
|
||||
w={110}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
19
apps/app/src/contexts/preferences.tsx
Normal file
19
apps/app/src/contexts/preferences.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export interface PreferencesContextValue {
|
||||
locale: string
|
||||
themeId: string
|
||||
setLocale: (locale: string) => void
|
||||
setThemeId: (themeId: string) => void
|
||||
}
|
||||
|
||||
export const PreferencesContext = createContext<PreferencesContextValue>({
|
||||
locale: 'en',
|
||||
themeId: 'default',
|
||||
setLocale: () => {},
|
||||
setThemeId: () => {},
|
||||
})
|
||||
|
||||
export function usePreferences(): PreferencesContextValue {
|
||||
return useContext(PreferencesContext)
|
||||
}
|
||||
46
apps/app/src/hooks/useAuthGate.ts
Normal file
46
apps/app/src/hooks/useAuthGate.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { getAuthSession } from '@/lib/auth'
|
||||
|
||||
// Client-side auth gates. They run only after hydration (useEffect never fires
|
||||
// during SSR), so the redirect decision never depends on the SSR worker seeing
|
||||
// the session cookie — which it can't on the multi-tenant pikkufabric.dev
|
||||
// domain, where the Auth.js cookie is host-only on the API origin and so is not
|
||||
// sent to the app host on a server-rendered request. The client fetch to the
|
||||
// API host does carry that cookie, so the check is correct on every deploy
|
||||
// topology (local, platform subdomains, custom domain).
|
||||
//
|
||||
// Trade-off: a logged-out visitor to /app sees the shell for a moment before
|
||||
// being redirected. On a custom domain (single tenant, cookie Domain=.<parent>)
|
||||
// you could add a createServerFn + server-side gate to remove that flash —
|
||||
// deferred; not safe to do generically on the shared platform domain.
|
||||
|
||||
export function useRedirectIfAuthenticated() {
|
||||
const navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void getAuthSession().then((session) => {
|
||||
if (!cancelled && session.user?.email) {
|
||||
void navigate({ to: '/app' })
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [navigate])
|
||||
}
|
||||
|
||||
export function useRequireAuthentication() {
|
||||
const navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void getAuthSession().then((session) => {
|
||||
if (!cancelled && !session.user?.email) {
|
||||
void navigate({ to: '/login' })
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [navigate])
|
||||
}
|
||||
5
apps/app/src/hooks/useMessage.ts
Normal file
5
apps/app/src/hooks/useMessage.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { usePikkuQuery } from '@project/functions-sdk/pikku/api.gen'
|
||||
|
||||
export function useMessage() {
|
||||
return usePikkuQuery('getMessage', {})
|
||||
}
|
||||
14
apps/app/src/hooks/useUpdateMessage.ts
Normal file
14
apps/app/src/hooks/useUpdateMessage.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePikkuMutation } from '@project/functions-sdk/pikku/api.gen'
|
||||
|
||||
export function useUpdateMessage() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return usePikkuMutation('updateMessage', {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['getMessage', {}],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
79
apps/app/src/i18n/config.ts
Normal file
79
apps/app/src/i18n/config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// i18n configuration — Paraglide JS (inlang). Messages live in `messages/*.json`
|
||||
// and are compiled to `src/paraglide/` by the Vite plugin (gitignored). This
|
||||
// module is the locale-plumbing entry point; `@/i18n/messages` exposes the typed
|
||||
// message functions (`m`).
|
||||
//
|
||||
// Add a language: drop `messages/<lang>.json` next to `en.json`, add the code to
|
||||
// `project.inlang/settings.json` `locales`, and recompile. Content is reachable
|
||||
// via the `/<lang>` URL prefix (e.g. `/fr`); the base locale (`en`) needs none.
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { locales, baseLocale, overwriteGetLocale } from '../paraglide/runtime.js'
|
||||
|
||||
export const supportedLocales = locales
|
||||
export const defaultLocale = baseLocale
|
||||
export type Locale = (typeof locales)[number]
|
||||
|
||||
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])
|
||||
// Direction for a locale — RTL for Arabic/Hebrew/Farsi/Urdu, else LTR. Set this
|
||||
// on <html dir> at the root so the browser (and Mantine) mirror the layout.
|
||||
export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {
|
||||
return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'
|
||||
}
|
||||
|
||||
export function detectLocale(pathname: string): Locale {
|
||||
const segment = pathname.split('/')[1]
|
||||
if (supportedLocales.includes(segment as Locale)) return segment as Locale
|
||||
if (typeof navigator !== 'undefined') {
|
||||
const browserLang = navigator.language?.split('-')[0]
|
||||
if (supportedLocales.includes(browserLang as Locale)) return browserLang as Locale
|
||||
}
|
||||
return defaultLocale
|
||||
}
|
||||
|
||||
// ── reactive locale store ────────────────────────────────────────────────────
|
||||
// Paraglide's `getLocale()` is a module-global, decoupled from React. We bridge
|
||||
// it to a tiny external store: `getLocale` reads `activeLocale`; components
|
||||
// subscribe via `useLocale()` (useSyncExternalStore) so `m.*()` re-renders on
|
||||
// switch — no page reload, no Paraglide `setLocale`, no app-specific context.
|
||||
// The app's own setLocale (persist + <html dir>) calls `setActiveLocale`.
|
||||
let activeLocale: Locale = defaultLocale
|
||||
const listeners = new Set<() => void>()
|
||||
overwriteGetLocale(() => activeLocale)
|
||||
|
||||
export function setActiveLocale(next: Locale): void {
|
||||
if (next === activeLocale) return
|
||||
activeLocale = next
|
||||
for (const fn of listeners) fn()
|
||||
}
|
||||
|
||||
function subscribe(fn: () => void): () => void {
|
||||
listeners.add(fn)
|
||||
return () => listeners.delete(fn)
|
||||
}
|
||||
|
||||
// Subscribe a component to locale changes so `m.*()` re-renders on switch. The
|
||||
// codemod injects a bare `useLocale()` wherever `const { t } = useTranslation()`
|
||||
// used to live; it also returns the active locale + direction for components
|
||||
// that need them (e.g. the language switcher).
|
||||
export function useLocale(): { locale: Locale; dir: 'ltr' | 'rtl'; setLocale: (l: Locale) => void } {
|
||||
const locale = useSyncExternalStore<Locale>(subscribe, () => activeLocale, () => activeLocale)
|
||||
return { locale, dir: localeDir(locale), setLocale: setActiveLocale }
|
||||
}
|
||||
|
||||
// ── i18n-debug masking ───────────────────────────────────────────────────────
|
||||
// When enabled, every *translated* string is masked to block glyphs (█) so any
|
||||
// readable text left on screen is text that never went through a message (a
|
||||
// hardcoded/inlined string) — missing i18n at a glance. Toggle with `?i18n-debug`
|
||||
// in the URL or `localStorage['i18n-debug'] = '1'` (`I18N_DEBUG=1` for SSR).
|
||||
export function isI18nDebug(): boolean {
|
||||
if (typeof process !== 'undefined' && process.env?.I18N_DEBUG === '1') return true
|
||||
if (typeof window === 'undefined') return false
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.has('i18n-debug')) return params.get('i18n-debug') !== '0'
|
||||
return window.localStorage?.getItem('i18n-debug') === '1'
|
||||
}
|
||||
|
||||
/** Mask a rendered string when debug mode is on; otherwise pass it through. */
|
||||
export function maskI18n(s: string): string {
|
||||
return isI18nDebug() ? s.replace(/\S/g, '█') : s
|
||||
}
|
||||
24
apps/app/src/i18n/messages.ts
Normal file
24
apps/app/src/i18n/messages.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Typed message functions for the app. `m.landing_title()` returns a branded
|
||||
// I18nString, so it satisfies the @pikku/mantine i18n gate exactly where the old
|
||||
// `t('landing.title')` used to — no call-site boilerplate.
|
||||
//
|
||||
// Each message is wrapped once so i18n-debug masking (█) still works (parity with
|
||||
// the old i18next postProcessor). Wrapping the whole namespace forgoes Paraglide
|
||||
// per-message tree-shaking — fine here: locale files are KB and the app bundled
|
||||
// all of en.json under i18next anyway.
|
||||
import { m as _m } from '../paraglide/messages.js'
|
||||
import type { I18nString } from '@pikku/react'
|
||||
import { maskI18n } from './config.js'
|
||||
|
||||
type BrandReturn<T> = T extends (...args: infer A) => unknown ? (...args: A) => I18nString : T
|
||||
type Branded<T> = { [K in keyof T]: BrandReturn<T[K]> }
|
||||
|
||||
const _raw = _m as unknown as Record<string, (args?: Record<string, unknown>) => string>
|
||||
const _wrapped: Record<string, unknown> = {}
|
||||
for (const key of Object.keys(_raw)) {
|
||||
const fn = _raw[key]
|
||||
_wrapped[key] = typeof fn === 'function' ? (...args: unknown[]) => maskI18n((fn as (...a: unknown[]) => string)(...args)) : fn
|
||||
}
|
||||
|
||||
/** Branded, debug-maskable message namespace. Drop-in for `t('...')`. */
|
||||
export const m = _wrapped as unknown as Branded<typeof _m>
|
||||
85
apps/app/src/lib/auth.ts
Normal file
85
apps/app/src/lib/auth.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { createAuthClient } from 'better-auth/client'
|
||||
import { apiUrl } from './env'
|
||||
|
||||
// Better Auth browser client. It targets the worker's catch-all `/api/auth/**`
|
||||
// routes (generated from src/auth.ts in the functions package). Pass the FULL
|
||||
// auth base (`<apiUrl>/auth`): better-auth's withPath only appends its default
|
||||
// `/api/auth` when baseURL has no path, and apiUrl() already carries `/api`, so
|
||||
// a bare apiUrl() would leave the client calling `/api/get-session` (404) and
|
||||
// the app would loop back to /login. Cookies ride every request so the session
|
||||
// round-trips across origins.
|
||||
export const authClient = createAuthClient({ baseURL: `${apiUrl()}/auth` })
|
||||
|
||||
export type AuthSession = {
|
||||
user?: {
|
||||
email?: string | null
|
||||
name?: string | null
|
||||
image?: string | null
|
||||
} | null
|
||||
expires?: string
|
||||
}
|
||||
|
||||
// Thrown by signInWithPassword when the email/password pair is wrong.
|
||||
export const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
|
||||
// Thrown by registerWithPassword when the email is already taken.
|
||||
export const EMAIL_IN_USE = 'EMAIL_IN_USE'
|
||||
|
||||
export async function getAuthSession(): Promise<AuthSession> {
|
||||
const { data } = await authClient.getSession()
|
||||
if (!data?.user) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
user: {
|
||||
email: data.user.email,
|
||||
name: data.user.name ?? null,
|
||||
image: data.user.image ?? null,
|
||||
},
|
||||
expires: data.session?.expiresAt
|
||||
? new Date(data.session.expiresAt).toISOString()
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Sign in with an email and password. On success Better Auth sets the session
|
||||
// cookie; on bad credentials this throws INVALID_CREDENTIALS.
|
||||
export async function signInWithPassword(
|
||||
email: string,
|
||||
password: string,
|
||||
_redirectPath = '/app',
|
||||
) {
|
||||
const { error } = await authClient.signIn.email({ email, password })
|
||||
if (error) {
|
||||
throw new Error(INVALID_CREDENTIALS)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new account. Better Auth signs the user in on success (sets the
|
||||
// session cookie), so they land logged in.
|
||||
export async function registerWithPassword(
|
||||
email: string,
|
||||
password: string,
|
||||
options: { name?: string; redirectPath?: string } = {},
|
||||
) {
|
||||
const { error } = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
// Better Auth requires a name; fall back to the local-part of the email.
|
||||
name: options.name?.trim() || email.split('@')[0],
|
||||
})
|
||||
|
||||
if (error) {
|
||||
// Better Auth returns 422 (UNPROCESSABLE_ENTITY) when the email is taken.
|
||||
if (error.status === 422 || /exist|taken/i.test(error.message ?? '')) {
|
||||
throw new Error(EMAIL_IN_USE)
|
||||
}
|
||||
throw new Error('Unable to create account')
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
const { error } = await authClient.signOut()
|
||||
if (error) {
|
||||
throw new Error('Unable to sign out')
|
||||
}
|
||||
}
|
||||
18
apps/app/src/lib/env.ts
Normal file
18
apps/app/src/lib/env.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Endpoints come from env, never hardcoded.
|
||||
//
|
||||
// In local dev VITE_API_URL is set at build time via .dev.vars and Vite
|
||||
// replaces the import.meta.env reference inline. In CF Workers deployments
|
||||
// VITE_API_URL is a runtime text binding — invisible to Vite at build time —
|
||||
// so import.meta.env.VITE_API_URL is undefined in the bundle. We fall back to
|
||||
// the current origin + /api, which is correct because the dispatcher worker
|
||||
// routes every /api/* path to the API units on the same hostname.
|
||||
export function apiUrl(): string {
|
||||
if (import.meta.env.SSR) {
|
||||
// SSR context: PikkuProvider is a client-side provider and its serverUrl
|
||||
// is only consumed by hooks that run in the browser. Return the build-time
|
||||
// var when available (local dev) or a placeholder so SSR never throws.
|
||||
return import.meta.env.VITE_API_URL ?? '/__api'
|
||||
}
|
||||
// Client: use the build-time var (local dev) or derive from current origin.
|
||||
return import.meta.env.VITE_API_URL ?? (window.location.origin + '/api')
|
||||
}
|
||||
76
apps/app/src/pages/LandingPage.tsx
Normal file
76
apps/app/src/pages/LandingPage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Button, Card, Container, Grid, Group, Stack, Text, Title } from '@pikku/mantine/core'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { m } from '@/i18n/messages'
|
||||
import type { I18nString } from '@pikku/react'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
|
||||
type CardKey = 'frontend' | 'backend' | 'auth'
|
||||
|
||||
const CARD_LABELS: Record<CardKey, { title: () => I18nString; description: () => I18nString }> = {
|
||||
frontend: { title: m.landing__cards__frontend__title, description: m.landing__cards__frontend__description },
|
||||
backend: { title: m.landing__cards__backend__title, description: m.landing__cards__backend__description },
|
||||
auth: { title: m.landing__cards__auth__title, description: m.landing__cards__auth__description },
|
||||
}
|
||||
|
||||
export function LandingPage() {
|
||||
useLocale()
|
||||
|
||||
const cards = ['frontend', 'backend', 'auth'] as const
|
||||
|
||||
return (
|
||||
<Container
|
||||
size="lg"
|
||||
py={72}
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Grid gap="xl" align="center">
|
||||
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Text
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
c="blue.3"
|
||||
mb="sm"
|
||||
style={{ letterSpacing: '0.18em' }}
|
||||
>
|
||||
{m.landing__eyebrow()}
|
||||
</Text>
|
||||
<Title order={1} fz={{ base: '2.8rem', md: '4.5rem' }} lh={0.95}>
|
||||
{m.landing__title()}
|
||||
</Title>
|
||||
<Text c="dimmed" mt="lg" maw={620} size="lg">
|
||||
{m.landing__subtitle()}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Group>
|
||||
<Button component={Link} to="/signup" radius="md" size="md">
|
||||
{m.landing__create_account()}
|
||||
</Button>
|
||||
<Button component="a" href="/login" variant="light" radius="md" size="md">
|
||||
{m.landing__sign_in()}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||
<Stack gap="md">
|
||||
{cards.map((key) => (
|
||||
<Card key={key} radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
|
||||
<Text fw={700}>{CARD_LABELS[key].title()}</Text>
|
||||
<Text c="dimmed" mt="xs">
|
||||
{CARD_LABELS[key].description()}
|
||||
</Text>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
61
apps/app/src/pages/LoginPage.tsx
Normal file
61
apps/app/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Anchor } from '@pikku/mantine/core'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import type { I18nString } from '@pikku/react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { AuthCard } from '@/components/AuthCard'
|
||||
import { INVALID_CREDENTIALS, signInWithPassword } from '@/lib/auth'
|
||||
|
||||
export function LoginPage() {
|
||||
useLocale()
|
||||
const navigate = useNavigate()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<I18nString | null>(null)
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await signInWithPassword(email, password, '/app')
|
||||
await navigate({ to: '/app' })
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error && err.message === INVALID_CREDENTIALS
|
||||
? m.auth__login__invalid_credentials()
|
||||
: m.auth__login__error(),
|
||||
)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title={m.auth__login__title()}
|
||||
description={m.auth__login__description()}
|
||||
cta={m.auth__login__cta()}
|
||||
email={email}
|
||||
password={password}
|
||||
passwordAutoComplete="current-password"
|
||||
busy={busy}
|
||||
message={null}
|
||||
error={error}
|
||||
onEmailChange={setEmail}
|
||||
onPasswordChange={setPassword}
|
||||
onSubmit={handleSubmit}
|
||||
footer={
|
||||
<>
|
||||
{m.auth__login__footer_prompt()}{' '}
|
||||
<Anchor component={Link} to="/signup">
|
||||
{m.auth__login__footer_action()}
|
||||
</Anchor>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
88
apps/app/src/pages/MessagePage.tsx
Normal file
88
apps/app/src/pages/MessagePage.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Button, Card, Grid, Stack, Text, Textarea, Title } from '@pikku/mantine/core'
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { MessageCard } from '@/components/MessageCard'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
import { useUpdateMessage } from '@/hooks/useUpdateMessage'
|
||||
|
||||
export function MessagePage() {
|
||||
useLocale()
|
||||
const messageQuery = useMessage()
|
||||
const updateMessage = useUpdateMessage()
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
await updateMessage.mutateAsync({ message })
|
||||
setMessage('')
|
||||
}
|
||||
|
||||
if (messageQuery.isLoading) {
|
||||
return (
|
||||
<Card radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
|
||||
<Text c="dimmed">{m.message__loading()}</Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (messageQuery.error || !messageQuery.data) {
|
||||
return (
|
||||
<Card radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
|
||||
<Text c="red.3">{m.message__load_error()}</Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid gap="lg">
|
||||
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||
<MessageCard
|
||||
message={messageQuery.data.message}
|
||||
updatedAt={messageQuery.data.updatedAt}
|
||||
updatedBy={messageQuery.data.updatedBy}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||
<Card radius="xl" padding="xl" withBorder bg="rgba(10, 20, 36, 0.82)">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
c="blue.3"
|
||||
mb="sm"
|
||||
style={{ letterSpacing: '0.18em' }}
|
||||
>
|
||||
{m.message__update__eyebrow()}
|
||||
</Text>
|
||||
<Title order={2}>{m.message__update__title()}</Title>
|
||||
<Text c="dimmed" mt="sm">
|
||||
{m.message__update__description()}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<Textarea
|
||||
label={m.message__update__field_label()}
|
||||
rows={4}
|
||||
placeholder={m.message__update__placeholder()}
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.currentTarget.value)}
|
||||
required
|
||||
radius="md"
|
||||
/>
|
||||
<Button type="submit" loading={updateMessage.isPending} radius="md">
|
||||
{m.message__update__cta()}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
61
apps/app/src/pages/SignupPage.tsx
Normal file
61
apps/app/src/pages/SignupPage.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Anchor } from '@pikku/mantine/core'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import type { I18nString } from '@pikku/react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { AuthCard } from '@/components/AuthCard'
|
||||
import { EMAIL_IN_USE, registerWithPassword } from '@/lib/auth'
|
||||
|
||||
export function SignupPage() {
|
||||
useLocale()
|
||||
const navigate = useNavigate()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<I18nString | null>(null)
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await registerWithPassword(email, password, { redirectPath: '/app' })
|
||||
await navigate({ to: '/app' })
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error && err.message === EMAIL_IN_USE
|
||||
? m.auth__signup__email_in_use()
|
||||
: m.auth__signup__error(),
|
||||
)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title={m.auth__signup__title()}
|
||||
description={m.auth__signup__description()}
|
||||
cta={m.auth__signup__cta()}
|
||||
email={email}
|
||||
password={password}
|
||||
passwordAutoComplete="new-password"
|
||||
busy={busy}
|
||||
message={null}
|
||||
error={error}
|
||||
onEmailChange={setEmail}
|
||||
onPasswordChange={setPassword}
|
||||
onSubmit={handleSubmit}
|
||||
footer={
|
||||
<>
|
||||
{m.auth__signup__footer_prompt()}{' '}
|
||||
<Anchor component={Link} to="/login">
|
||||
{m.auth__signup__footer_action()}
|
||||
</Anchor>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
20
apps/app/src/router.tsx
Normal file
20
apps/app/src/router.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
// TanStack Start discovers this `getRouter` factory to build a router per
|
||||
// request (server) and once on the client (hydration). routeTree.gen.ts is
|
||||
// generated by the tanstackStart() Vite plugin from src/routes/** — don't
|
||||
// hand-edit it.
|
||||
export function getRouter() {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: 'intent',
|
||||
})
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
118
apps/app/src/routes/__root.tsx
Normal file
118
apps/app/src/routes/__root.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
|
||||
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@pikku/mantine/core'
|
||||
import '@mantine/core/styles.css'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { PikkuProvider, createPikku } from '@pikku/react'
|
||||
import { PikkuFetch } from '@project/functions-sdk/pikku/pikku-fetch.gen'
|
||||
import { PikkuRPC } from '@project/functions-sdk/pikku/pikku-rpc.gen'
|
||||
import { activeTheme, buildTheme, themes, type Palette } from '@project/mantine-themes'
|
||||
import { type MantineThemeOverride } from '@pikku/mantine/core'
|
||||
import { defaultLocale, localeDir, supportedLocales, setActiveLocale } from '@/i18n/config'
|
||||
import { apiUrl } from '@/lib/env'
|
||||
import { PreferencesContext } from '@/contexts/preferences'
|
||||
|
||||
const LOCALE_KEY = 'app-locale'
|
||||
const THEME_KEY = 'app-theme'
|
||||
|
||||
// Root route owns the full HTML document for SSR (<html lang dir> so the tree
|
||||
// mirrors for RTL locales) and wraps the app in the Mantine + react-query +
|
||||
// Pikku providers. Mantine v8 SSR needs ColorSchemeScript in <head> and
|
||||
// mantineHtmlProps on <html>; @mantine/core/styles.css ships the static CSS.
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'Pikku App' },
|
||||
],
|
||||
}),
|
||||
component: RootDocument,
|
||||
})
|
||||
|
||||
function RootDocument() {
|
||||
// Server-safe defaults; useEffect syncs from localStorage after hydration.
|
||||
const [themeId, setThemeIdRaw] = useState('default')
|
||||
const [locale, setLocaleRaw] = useState(defaultLocale)
|
||||
// Transient override from fabric console live-preview (not persisted).
|
||||
const [previewTheme, setPreviewTheme] = useState<MantineThemeOverride | null>(null)
|
||||
|
||||
const effectiveTheme = previewTheme ?? themes[themeId] ?? activeTheme
|
||||
|
||||
// Sync preferences from localStorage after hydration.
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem(THEME_KEY)
|
||||
if (savedTheme && themes[savedTheme]) setThemeIdRaw(savedTheme)
|
||||
|
||||
const savedLocale = localStorage.getItem(LOCALE_KEY)
|
||||
if (savedLocale && supportedLocales.includes(savedLocale as (typeof supportedLocales)[number])) {
|
||||
setLocaleRaw(savedLocale)
|
||||
setActiveLocale(savedLocale as (typeof supportedLocales)[number])
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Keep <html lang dir> in sync with locale changes after hydration.
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale
|
||||
document.documentElement.dir = localeDir(locale)
|
||||
}, [locale])
|
||||
|
||||
// Fabric console live-preview: postMessage overrides the active theme
|
||||
// without persisting, so the user sees the palette instantly in an iframe.
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = event.data
|
||||
if (data?.source !== 'fabric-console' || data.type !== 'set-theme') return
|
||||
try {
|
||||
setPreviewTheme(data.palette ? buildTheme(data.palette as Palette) : null)
|
||||
} catch (err) {
|
||||
console.warn('[mantine-themes] ignoring bad theme payload', err)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => window.removeEventListener('message', onMessage)
|
||||
}, [])
|
||||
|
||||
const setLocale = (next: string) => {
|
||||
localStorage.setItem(LOCALE_KEY, next)
|
||||
setLocaleRaw(next)
|
||||
setActiveLocale(next as (typeof supportedLocales)[number])
|
||||
}
|
||||
|
||||
const setThemeId = (next: string) => {
|
||||
localStorage.setItem(THEME_KEY, next)
|
||||
setThemeIdRaw(next)
|
||||
// Clear any live-preview override so the persisted theme takes effect.
|
||||
setPreviewTheme(null)
|
||||
}
|
||||
|
||||
// Per-render instances keep server requests from sharing client/cache state.
|
||||
const [queryClient] = useState(() => new QueryClient())
|
||||
const [pikku] = useState(() =>
|
||||
createPikku(PikkuFetch, PikkuRPC, {
|
||||
serverUrl: apiUrl(),
|
||||
credentials: 'include',
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={localeDir(locale)} {...mantineHtmlProps}>
|
||||
<head>
|
||||
<HeadContent />
|
||||
<ColorSchemeScript defaultColorScheme="dark" />
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider theme={effectiveTheme} defaultColorScheme="dark">
|
||||
<PreferencesContext.Provider value={{ locale, themeId, setLocale, setThemeId }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PikkuProvider pikku={pikku}>
|
||||
<Outlet />
|
||||
</PikkuProvider>
|
||||
</QueryClientProvider>
|
||||
</PreferencesContext.Provider>
|
||||
</MantineProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
6
apps/app/src/routes/app.index.tsx
Normal file
6
apps/app/src/routes/app.index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { MessagePage } from '@/pages/MessagePage'
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: MessagePage,
|
||||
})
|
||||
12
apps/app/src/routes/app.tsx
Normal file
12
apps/app/src/routes/app.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { AppShell } from '@/components/AppShell'
|
||||
import { useRequireAuthentication } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
component: AppLayout,
|
||||
})
|
||||
|
||||
function AppLayout() {
|
||||
useRequireAuthentication()
|
||||
return <AppShell />
|
||||
}
|
||||
12
apps/app/src/routes/index.tsx
Normal file
12
apps/app/src/routes/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { LandingPage } from '@/pages/LandingPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: LandingRoute,
|
||||
})
|
||||
|
||||
function LandingRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <LandingPage />
|
||||
}
|
||||
12
apps/app/src/routes/login.tsx
Normal file
12
apps/app/src/routes/login.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { LoginPage } from '@/pages/LoginPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/login')({
|
||||
component: LoginRoute,
|
||||
})
|
||||
|
||||
function LoginRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <LoginPage />
|
||||
}
|
||||
12
apps/app/src/routes/signup.tsx
Normal file
12
apps/app/src/routes/signup.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { SignupPage } from '@/pages/SignupPage'
|
||||
import { useRedirectIfAuthenticated } from '@/hooks/useAuthGate'
|
||||
|
||||
export const Route = createFileRoute('/signup')({
|
||||
component: SignupRoute,
|
||||
})
|
||||
|
||||
function SignupRoute() {
|
||||
useRedirectIfAuthenticated()
|
||||
return <SignupPage />
|
||||
}
|
||||
23
apps/app/tsconfig.json
Normal file
23
apps/app/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"types": ["vite/client", "node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "wrangler.jsonc"]
|
||||
}
|
||||
29
apps/app/vite.config.ts
Normal file
29
apps/app/vite.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import { paraglideVitePlugin } from '@inlang/paraglide-js'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// Plain TanStack Start config (Node target) — used for local sandbox dev. This
|
||||
// template is deploy-provider agnostic: it ships NO @cloudflare/vite-plugin. At
|
||||
// deploy, fabric CI injects the CF adapter (vite.config.cf.ts merges cloudflare()
|
||||
// on top of this config) to emit the CF Workers SSR bundle. Do not add cloudflare
|
||||
// here — that would double-apply it and couple the template to a provider.
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// Compile messages/*.json → src/paraglide so `m`/`mKey` resolve, with HMR
|
||||
// on message edits. Must run first.
|
||||
paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' }),
|
||||
tanstackStart(),
|
||||
react(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: true,
|
||||
},
|
||||
})
|
||||
9
apps/app/wrangler.jsonc
Normal file
9
apps/app/wrangler.jsonc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
// Consumed by @cloudflare/vite-plugin. `main` is TanStack Start's built-in
|
||||
// server entry (no hand-written worker file); the plugin wires client assets
|
||||
// and the ssr environment automatically.
|
||||
"name": "app",
|
||||
"main": "@tanstack/react-start/server-entry",
|
||||
"compatibility_date": "2024-12-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
}
|
||||
8
db/annotations.gen.json
Normal file
8
db/annotations.gen.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"account": {},
|
||||
"audit": {},
|
||||
"message_state": {},
|
||||
"session": {},
|
||||
"user": {},
|
||||
"verification": {}
|
||||
}
|
||||
16
db/annotations.ts
Normal file
16
db/annotations.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { DbClassificationMap } from '/Users/yasser/git/pikku/fabric/templates/server-and-serverless/.pikku/db/classification-map.gen.d.ts'
|
||||
|
||||
export const classifications = {
|
||||
"account": {
|
||||
},
|
||||
"audit": {
|
||||
},
|
||||
"message_state": {
|
||||
},
|
||||
"session": {
|
||||
},
|
||||
"user": {
|
||||
},
|
||||
"verification": {
|
||||
},
|
||||
} satisfies DbClassificationMap
|
||||
3
db/sqlite-seed.sql
Normal file
3
db/sqlite-seed.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
INSERT INTO message_state (id, message)
|
||||
VALUES (1, 'Hello from Starter')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
10
db/sqlite-test-seed.sql
Normal file
10
db/sqlite-test-seed.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Test-only seed: inserts a minimal user row so updateMessage's inner join
|
||||
-- on (user.id = messageState.updatedByUserId) resolves. This file is only
|
||||
-- used by the function-tests harness (never in the sandbox or production).
|
||||
INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at)
|
||||
VALUES ('user-001', 'Test User', 'test@example.com', 1, datetime('now'), datetime('now'))
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO message_state (id, message)
|
||||
VALUES (1, 'Hello from Starter')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
67
db/sqlite/0001-init.sql
Normal file
67
db/sqlite/0001-init.sql
Normal file
@@ -0,0 +1,67 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Better Auth core schema. These four tables (user, session, account,
|
||||
-- verification) are owned and written entirely by Better Auth — the app never
|
||||
-- inserts into them directly; sign-up / sign-in / sessions all flow through the
|
||||
-- generated `/api/auth/**` routes (see src/auth.ts).
|
||||
--
|
||||
-- The schema is generated by Better Auth's own migration generator
|
||||
-- (`getMigrations(...).compileMigrations()`); the columns are spelled in
|
||||
-- snake_case to match the rest of the app's DB convention. Better Auth is handed
|
||||
-- the app's kysely (with CamelCasePlugin) in src/auth.ts, so its camelCase field
|
||||
-- names (emailVerified, userId, createdAt, ...) compile to exactly these
|
||||
-- snake_case columns at runtime. To change this schema (add a provider field, a
|
||||
-- plugin table, ...), re-run the generator rather than hand-editing.
|
||||
--
|
||||
-- Email + password is the default sign-in method. The credential hash is stored
|
||||
-- by Better Auth in account.password (provider_id = 'credential'), never on the
|
||||
-- user row.
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" text not null primary key,
|
||||
"name" text not null,
|
||||
"email" text not null unique,
|
||||
"email_verified" integer not null,
|
||||
"image" text,
|
||||
"created_at" date not null,
|
||||
"updated_at" date not null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "session" (
|
||||
"id" text not null primary key,
|
||||
"expires_at" date not null,
|
||||
"token" text not null unique,
|
||||
"created_at" date not null,
|
||||
"updated_at" date not null,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text not null references "user" ("id") on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "account" (
|
||||
"id" text not null primary key,
|
||||
"account_id" text not null,
|
||||
"provider_id" text not null,
|
||||
"user_id" text not null references "user" ("id") on delete cascade,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" date,
|
||||
"refresh_token_expires_at" date,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" date not null,
|
||||
"updated_at" date not null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "verification" (
|
||||
"id" text not null primary key,
|
||||
"identifier" text not null,
|
||||
"value" text not null,
|
||||
"expires_at" date not null,
|
||||
"created_at" date not null,
|
||||
"updated_at" date not null
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "session" ("user_id");
|
||||
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "account" ("user_id");
|
||||
CREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" ("identifier");
|
||||
12
db/sqlite/0002-message.sql
Normal file
12
db/sqlite/0002-message.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Demo "hello world" persistence: a single editable message that shows the full
|
||||
-- loop (sign in -> read from the DB -> write back -> see who last changed it).
|
||||
-- Singleton row, always id = 1. Safe to delete this table and its functions
|
||||
-- (get-message / update-message) once you start building your own thing.
|
||||
CREATE TABLE IF NOT EXISTS message_state (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_by_user_id TEXT REFERENCES "user" ("id")
|
||||
);
|
||||
26
db/sqlite/0003-audit.sql
Normal file
26
db/sqlite/0003-audit.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Audit log. Fabric's audit service flushes events here via a per-stage
|
||||
-- CF Queue. Add this table if you want audit capture; remove it (and skip
|
||||
-- this migration) if your project has no audit requirements.
|
||||
CREATE TABLE IF NOT EXISTS audit (
|
||||
audit_id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
type TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'auto',
|
||||
outcome TEXT,
|
||||
function_id TEXT,
|
||||
wire_type TEXT,
|
||||
trace_id TEXT,
|
||||
transaction_id TEXT,
|
||||
query_id TEXT,
|
||||
actor_user_id TEXT,
|
||||
actor_org_id TEXT,
|
||||
tables TEXT, -- JSON array of table names touched
|
||||
changed_cols TEXT, -- JSON array of changed column names
|
||||
event TEXT, -- custom event label
|
||||
old TEXT, -- JSON: previous values
|
||||
data TEXT -- JSON: new values / event payload
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_occurred_at ON audit (occurred_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit (actor_user_id) WHERE actor_user_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_function ON audit (function_id) WHERE function_id IS NOT NULL;
|
||||
14
emails/locales/en.json
Normal file
14
emails/locales/en.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"common": {
|
||||
"footer": "If you did not create this account, you can safely ignore this email."
|
||||
},
|
||||
"confirmEmail": {
|
||||
"subject": "Confirm your email for {{appName}}",
|
||||
"preview": "Confirm your email address to finish setting up your account.",
|
||||
"heading": "Confirm your email",
|
||||
"intro": "Thanks for signing up for {{appName}}! Confirm {{email}} to finish setting up your account.",
|
||||
"cta": "Confirm email",
|
||||
"fallback": "If the button does not work, copy and paste this URL into your browser:",
|
||||
"expiry": "This link expires in 24 hours."
|
||||
}
|
||||
}
|
||||
3
emails/partials/footer.html
Normal file
3
emails/partials/footer.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<p style="margin:32px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;">
|
||||
{{t.common.footer}}
|
||||
</p>
|
||||
13
emails/partials/layout.html
Normal file
13
emails/partials/layout.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="{{locale}}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{subject}}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:{{theme.colors.canvas}};font-family:{{theme.fonts.body}};">
|
||||
<div style="max-width:560px;margin:0 auto;padding:32px 16px;">
|
||||
{{content}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
emails/templates/confirm-email.html
Normal file
27
emails/templates/confirm-email.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<p style="margin:0 0 12px;color:{{theme.colors.accent}};font-size:12px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">
|
||||
{{appName}}
|
||||
</p>
|
||||
<h1 style="margin:0 0 16px;color:{{theme.colors.text}};font-size:28px;line-height:1.1;">
|
||||
{{t.confirmEmail.heading}}
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;color:{{theme.colors.muted}};font-size:16px;line-height:1.6;">
|
||||
{{t.confirmEmail.intro}}
|
||||
</p>
|
||||
<p style="margin:0 0 24px;">
|
||||
<a
|
||||
href="{{confirmUrl}}"
|
||||
style="display:inline-block;background:{{theme.colors.button}};color:{{theme.colors.buttonText}};text-decoration:none;padding:14px 22px;border-radius:999px;font-weight:700;"
|
||||
>
|
||||
{{t.confirmEmail.cta}}
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin:0 0 8px;color:{{theme.colors.text}};font-size:14px;line-height:1.6;">
|
||||
{{t.confirmEmail.fallback}}
|
||||
</p>
|
||||
<p style="margin:0;color:{{theme.colors.muted}};font-size:14px;line-height:1.6;word-break:break-all;">
|
||||
{{confirmUrl}}
|
||||
</p>
|
||||
<p style="margin:24px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;">
|
||||
{{t.confirmEmail.expiry}}
|
||||
</p>
|
||||
{{> footer}}
|
||||
1
emails/templates/confirm-email.subject.txt
Normal file
1
emails/templates/confirm-email.subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
{{t.confirmEmail.subject}}
|
||||
7
emails/templates/confirm-email.text.txt
Normal file
7
emails/templates/confirm-email.text.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
{{t.confirmEmail.heading}}
|
||||
|
||||
{{t.confirmEmail.intro}}
|
||||
|
||||
{{confirmUrl}}
|
||||
|
||||
{{t.confirmEmail.expiry}}
|
||||
16
emails/theme.json
Normal file
16
emails/theme.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"appName": "Pikku Starter",
|
||||
"fonts": {
|
||||
"body": "Inter, Arial, sans-serif"
|
||||
},
|
||||
"colors": {
|
||||
"canvas": "#0b1020",
|
||||
"surface": "#11182d",
|
||||
"border": "#263252",
|
||||
"text": "#f7f8fb",
|
||||
"muted": "#a8b0c5",
|
||||
"accent": "#7dd3fc",
|
||||
"button": "#f59e0b",
|
||||
"buttonText": "#111827"
|
||||
}
|
||||
}
|
||||
15
knowledge/README.md
Normal file
15
knowledge/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Knowledge base
|
||||
|
||||
This folder is the builder agent's long-term memory of **this company and product** —
|
||||
short markdown notes, not code. The agent reads these at the start of every session and
|
||||
updates them as it learns durable facts: domain concepts, decisions and the reasons
|
||||
behind them, preferences, and anything that makes the agent more personalised over time.
|
||||
|
||||
Suggested notes (create as needed):
|
||||
|
||||
- `product.md` — what the product is, who it's for, the core problem it solves
|
||||
- `glossary.md` — the company's own terms and what they mean
|
||||
- `decisions.md` — choices made together and why
|
||||
- `preferences.md` — style, scope, and things to avoid
|
||||
|
||||
Keep notes concise and current. Do not store secrets or credentials here.
|
||||
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@project/root",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*",
|
||||
"packages/functions/tests",
|
||||
"design/*",
|
||||
"!apps/_app_template_"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "yarn workspace @project/functions pikku all",
|
||||
"dev": "yarn workspace @project/app dev",
|
||||
"tsc": "yarn workspaces foreach --all run tsc",
|
||||
"lint": "oxlint --config ../.oxlintrc.json apps packages",
|
||||
"format": "oxfmt --config ../.oxfmtrc.json apps packages",
|
||||
"format:check": "oxfmt --check --config ../.oxfmtrc.json apps packages"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pikku/addon-console": "^0.12.20",
|
||||
"@pikku/core": "^0.12.38",
|
||||
"@pikku/kysely": "^0.12.17",
|
||||
"@pikku/kysely-sqlite": "^0.12.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pikku/cli": "^0.12.54",
|
||||
"oxfmt": "^0.53.0",
|
||||
"oxlint": "^1.68.0"
|
||||
}
|
||||
}
|
||||
8
packages/components/index.ts
Normal file
8
packages/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { UserCard } from './src/UserCard'
|
||||
export type { User, UserCardProps } from './src/UserCard'
|
||||
export { stubQuery } from './src/fixtures/stubQuery'
|
||||
export { stubMutation } from './src/fixtures/stubMutation'
|
||||
export { EmptyState } from './src/EmptyState'
|
||||
export { NotFoundState } from './src/NotFoundState'
|
||||
export { PageLoader } from './src/PageLoader'
|
||||
export { ServerErrorState } from './src/ServerErrorState'
|
||||
31
packages/components/package.json
Normal file
31
packages/components/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@project/components",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mantine/core": "^9.2.1",
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"lucide-react": "^0.456.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"typescript": "^5.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "^9.2.1",
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
}
|
||||
}
|
||||
58
packages/components/src/EmptyState.stories.tsx
Normal file
58
packages/components/src/EmptyState.stories.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { AlertCircle, Boxes, FolderOpen } from 'lucide-react'
|
||||
import type { Story, StoryMeta } from './csf.types'
|
||||
import { EmptyState } from './EmptyState'
|
||||
|
||||
const meta: StoryMeta = {
|
||||
title: 'EmptyState',
|
||||
component: EmptyState,
|
||||
tags: ['feedback'],
|
||||
argTypes: {
|
||||
icon: { description: 'Lucide icon component displayed above the title', control: false },
|
||||
title: { description: 'Primary heading text', control: 'text' },
|
||||
description: { description: 'Supporting text below the title', control: 'text' },
|
||||
action: { description: 'Label for the primary action button', control: 'text' },
|
||||
onAction: { description: 'Callback fired when the action button is clicked', control: false },
|
||||
compact: {
|
||||
description: 'Reduces padding and icon size for inline contexts',
|
||||
control: 'boolean',
|
||||
},
|
||||
docsHref: { description: 'Optional link shown as a Docs button', control: 'text' },
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
function DefaultStory() {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Boxes}
|
||||
title="No items yet"
|
||||
description="Create your first item to get started."
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const Default: Story = { render: DefaultStory }
|
||||
|
||||
function WithActionStory() {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={FolderOpen}
|
||||
title="No projects"
|
||||
description="You don't have any projects yet."
|
||||
action="New project"
|
||||
onAction={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const WithAction: Story = { render: WithActionStory }
|
||||
|
||||
function CompactStory() {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={AlertCircle}
|
||||
title="Nothing here"
|
||||
description="This section is currently empty."
|
||||
compact
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const Compact: Story = { render: CompactStory }
|
||||
77
packages/components/src/EmptyState.tsx
Normal file
77
packages/components/src/EmptyState.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import { Button, Center, Group, Stack, Text } from '@mantine/core'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon: React.ComponentType<{ size?: number; strokeWidth?: number }>
|
||||
title: string
|
||||
description: React.ReactNode
|
||||
action?: string
|
||||
onAction?: () => void
|
||||
actionIcon?: React.ReactNode
|
||||
actionLoading?: boolean
|
||||
secondaryText?: React.ReactNode
|
||||
docsHref?: string
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
onAction,
|
||||
actionIcon,
|
||||
actionLoading,
|
||||
secondaryText,
|
||||
docsHref,
|
||||
compact = false,
|
||||
}: EmptyStateProps) {
|
||||
const Icon = icon
|
||||
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
gap={compact ? 'sm' : 'md'}
|
||||
py={compact ? 'lg' : 'xl'}
|
||||
style={{ minHeight: compact ? undefined : '60vh', width: '100%' }}
|
||||
>
|
||||
<Icon size={compact ? 36 : 48} strokeWidth={1} />
|
||||
<Text size={compact ? 'lg' : 'xl'} fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text c="dimmed" ta="center" maw={500}>
|
||||
{description}
|
||||
</Text>
|
||||
{docsHref || (action && onAction) ? (
|
||||
<Group justify="center">
|
||||
{docsHref ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={docsHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
leftSection={<ExternalLink size={16} />}
|
||||
>
|
||||
Docs
|
||||
</Button>
|
||||
) : null}
|
||||
{action && onAction ? (
|
||||
<Button onClick={onAction} loading={actionLoading} leftSection={actionIcon}>
|
||||
{action}
|
||||
</Button>
|
||||
) : null}
|
||||
</Group>
|
||||
) : null}
|
||||
{secondaryText ? (
|
||||
<Text c="dimmed" size="sm" ta="center" maw={500}>
|
||||
{secondaryText}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
25
packages/components/src/NotFoundState.stories.tsx
Normal file
25
packages/components/src/NotFoundState.stories.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Story, StoryMeta } from './csf.types'
|
||||
import { NotFoundState } from './NotFoundState'
|
||||
|
||||
const meta: StoryMeta = {
|
||||
title: 'NotFoundState',
|
||||
component: NotFoundState,
|
||||
tags: ['feedback'],
|
||||
argTypes: {
|
||||
title: { description: 'Override the "Page not found" heading', control: 'text' },
|
||||
description: { description: 'Override the default description text', control: 'text' },
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
function CustomStory() {
|
||||
return (
|
||||
<NotFoundState
|
||||
title="Board not found"
|
||||
description="The board you were looking for has been removed or never existed."
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const Custom: Story = { render: CustomStory }
|
||||
40
packages/components/src/NotFoundState.tsx
Normal file
40
packages/components/src/NotFoundState.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Center, Stack, Text } from '@mantine/core'
|
||||
|
||||
interface NotFoundStateProps {
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function NotFoundState({
|
||||
title = 'Page not found',
|
||||
description = "The resource you're looking for doesn't exist or has been removed.",
|
||||
}: NotFoundStateProps) {
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="xs"
|
||||
py="xl"
|
||||
style={{ minHeight: '60vh', width: '100%' }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 72,
|
||||
lineHeight: 1,
|
||||
letterSpacing: '-0.06em',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
404
|
||||
</Text>
|
||||
<Text size="xl" fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text ta="center" maw={520} c="dimmed">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
11
packages/components/src/PageLoader.stories.tsx
Normal file
11
packages/components/src/PageLoader.stories.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Story, StoryMeta } from './csf.types'
|
||||
import { PageLoader } from './PageLoader'
|
||||
|
||||
const meta: StoryMeta = {
|
||||
title: 'PageLoader',
|
||||
component: PageLoader,
|
||||
tags: ['loading'],
|
||||
}
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {}
|
||||
16
packages/components/src/PageLoader.tsx
Normal file
16
packages/components/src/PageLoader.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Center, Loader } from '@mantine/core'
|
||||
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<Center
|
||||
style={{
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<Loader size="md" />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
47
packages/components/src/ServerErrorState.stories.tsx
Normal file
47
packages/components/src/ServerErrorState.stories.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useState } from 'react'
|
||||
import type { Story, StoryMeta } from './csf.types'
|
||||
import { ServerErrorState } from './ServerErrorState'
|
||||
|
||||
const meta: StoryMeta = {
|
||||
title: 'ServerErrorState',
|
||||
component: ServerErrorState,
|
||||
tags: ['feedback'],
|
||||
argTypes: {
|
||||
title: { description: 'Override the "Something went wrong" heading', control: 'text' },
|
||||
description: { description: 'Override the default error description', control: 'text' },
|
||||
onRetry: { description: 'Callback fired when the retry button is clicked', control: false },
|
||||
retrying: {
|
||||
description: 'Shows a loading spinner on the retry button',
|
||||
control: 'boolean',
|
||||
},
|
||||
retryLabel: { description: 'Label for the retry button', control: 'text' },
|
||||
glyph: {
|
||||
description: 'Custom element displayed above the title instead of "500"',
|
||||
control: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
function WithRetryStory() {
|
||||
const [retrying, setRetrying] = useState(false)
|
||||
const handleRetry = () => {
|
||||
setRetrying(true)
|
||||
setTimeout(() => setRetrying(false), 1500)
|
||||
}
|
||||
return <ServerErrorState onRetry={handleRetry} retrying={retrying} />
|
||||
}
|
||||
export const WithRetry: Story = { render: WithRetryStory }
|
||||
|
||||
function CustomGlyphStory() {
|
||||
return (
|
||||
<ServerErrorState
|
||||
glyph={<span style={{ fontSize: 64, lineHeight: 1 }}>⚠️</span>}
|
||||
title="Unexpected error"
|
||||
description="Please try again or contact support if the problem persists."
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const CustomGlyph: Story = { render: CustomGlyphStory }
|
||||
70
packages/components/src/ServerErrorState.tsx
Normal file
70
packages/components/src/ServerErrorState.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button, Center, Stack, Text } from '@mantine/core'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
|
||||
interface ServerErrorStateProps {
|
||||
title?: string
|
||||
description?: string
|
||||
onRetry?: () => void
|
||||
retrying?: boolean
|
||||
retryLabel?: string
|
||||
glyph?: ReactNode
|
||||
}
|
||||
|
||||
export function ServerErrorState({
|
||||
title = 'Something went wrong',
|
||||
description = 'The server returned an error while loading this page.',
|
||||
onRetry,
|
||||
retrying = false,
|
||||
retryLabel = 'Retry',
|
||||
glyph,
|
||||
}: ServerErrorStateProps) {
|
||||
const normalizedDescription = typeof description === 'string' ? description.trim() : ''
|
||||
const showDescription =
|
||||
normalizedDescription.length > 0 &&
|
||||
normalizedDescription !== 'HTTP 500' &&
|
||||
normalizedDescription !== 'HTTP 500.'
|
||||
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="xs"
|
||||
py="xl"
|
||||
style={{ minHeight: '60vh', width: '100%' }}
|
||||
>
|
||||
{glyph ?? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 72,
|
||||
lineHeight: 1,
|
||||
letterSpacing: '-0.06em',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
500
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xl" fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
{showDescription ? (
|
||||
<Text ta="center" maw={520} c="dimmed">
|
||||
{normalizedDescription}
|
||||
</Text>
|
||||
) : null}
|
||||
{onRetry ? (
|
||||
<Button
|
||||
onClick={onRetry}
|
||||
loading={retrying}
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
variant="default"
|
||||
>
|
||||
{retryLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
83
packages/components/src/UserCard.tsx
Normal file
83
packages/components/src/UserCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Avatar, Badge, Card, Group, Loader, Stack, Text } from '@mantine/core'
|
||||
import type { UseQueryResult } from '@tanstack/react-query'
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role: string
|
||||
status: 'active' | 'away' | 'offline'
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<User['status'], string> = {
|
||||
active: 'green',
|
||||
away: 'yellow',
|
||||
offline: 'gray',
|
||||
}
|
||||
|
||||
export type UserCardProps = {
|
||||
query: UseQueryResult<User | null>
|
||||
}
|
||||
|
||||
export const UserCard: React.FC<UserCardProps> = ({ query }) => {
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group>
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading user…
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (query.isError) {
|
||||
return (
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Stack gap={4}>
|
||||
<Text fw={600} c="red">
|
||||
Couldn’t load user
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{query.error instanceof Error ? query.error.message : 'Unknown error'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const user = query.data
|
||||
if (!user) {
|
||||
return (
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
No user to show.
|
||||
</Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Avatar color="initials" name={user.name} radius="xl" />
|
||||
<Stack gap={0}>
|
||||
<Text fw={600}>{user.name}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge color={STATUS_COLOR[user.status]} variant="light">
|
||||
{user.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" mt="md">
|
||||
{user.role}
|
||||
</Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
21
packages/components/src/csf.types.ts
Normal file
21
packages/components/src/csf.types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ComponentType } from 'react'
|
||||
|
||||
export interface ArgType {
|
||||
description?: string
|
||||
control?: string | false
|
||||
defaultValue?: unknown
|
||||
}
|
||||
|
||||
export interface StoryMeta {
|
||||
title: string
|
||||
component: ComponentType<any>
|
||||
description?: string
|
||||
tags?: string[]
|
||||
argTypes?: Record<string, ArgType>
|
||||
}
|
||||
|
||||
export interface Story {
|
||||
args?: Record<string, unknown>
|
||||
render?: ComponentType<any>
|
||||
name?: string
|
||||
}
|
||||
69
packages/components/src/fixtures/stubMutation.ts
Normal file
69
packages/components/src/fixtures/stubMutation.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { UseMutationResult } from '@tanstack/react-query'
|
||||
|
||||
function build<TData, TVariables>(
|
||||
partial: Record<string, unknown>,
|
||||
): UseMutationResult<TData, Error, TVariables> {
|
||||
return {
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
isPaused: false,
|
||||
submittedAt: 0,
|
||||
context: undefined,
|
||||
variables: undefined,
|
||||
mutate: () => undefined,
|
||||
mutateAsync: () => Promise.resolve(undefined as never),
|
||||
reset: () => undefined,
|
||||
...partial,
|
||||
} as unknown as UseMutationResult<TData, Error, TVariables>
|
||||
}
|
||||
|
||||
export const stubMutation = {
|
||||
idle<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
|
||||
return build<TData, TVariables>({
|
||||
status: 'idle',
|
||||
isIdle: true,
|
||||
isPending: false,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
pending<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
|
||||
return build<TData, TVariables>({
|
||||
status: 'pending',
|
||||
isIdle: false,
|
||||
isPending: true,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
error<TData = unknown, TVariables = unknown>(
|
||||
error: Error,
|
||||
): UseMutationResult<TData, Error, TVariables> {
|
||||
return build<TData, TVariables>({
|
||||
status: 'error',
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
isError: true,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error,
|
||||
})
|
||||
},
|
||||
success<TData = unknown, TVariables = unknown>(
|
||||
data: TData,
|
||||
): UseMutationResult<TData, Error, TVariables> {
|
||||
return build<TData, TVariables>({
|
||||
status: 'success',
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
data,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
}
|
||||
66
packages/components/src/fixtures/stubQuery.ts
Normal file
66
packages/components/src/fixtures/stubQuery.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { UseQueryResult } from '@tanstack/react-query'
|
||||
|
||||
function build<T>(partial: Record<string, unknown>): UseQueryResult<T> {
|
||||
return {
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: partial.fetchStatus === 'fetching',
|
||||
isPaused: false,
|
||||
isPlaceholderData: false,
|
||||
isRefetching: false,
|
||||
isStale: false,
|
||||
isInitialLoading: partial.isLoading === true,
|
||||
isLoadingError: false,
|
||||
isRefetchError: false,
|
||||
dataUpdatedAt: 0,
|
||||
errorUpdatedAt: 0,
|
||||
refetch: () => Promise.resolve(undefined as never),
|
||||
promise: Promise.resolve(undefined as never),
|
||||
...partial,
|
||||
} as unknown as UseQueryResult<T>
|
||||
}
|
||||
|
||||
export const stubQuery = {
|
||||
loading<T>(): UseQueryResult<T> {
|
||||
return build<T>({
|
||||
status: 'pending',
|
||||
fetchStatus: 'fetching',
|
||||
isPending: true,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
error<T>(error: Error): UseQueryResult<T> {
|
||||
return build<T>({
|
||||
status: 'error',
|
||||
fetchStatus: 'idle',
|
||||
isPending: false,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error,
|
||||
})
|
||||
},
|
||||
success<T>(data: T): UseQueryResult<T> {
|
||||
return build<T>({
|
||||
status: 'success',
|
||||
fetchStatus: 'idle',
|
||||
isPending: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
data,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
empty<T>(): UseQueryResult<T> {
|
||||
return stubQuery.success<T>(null as T)
|
||||
},
|
||||
}
|
||||
14
packages/components/tsconfig.json
Normal file
14
packages/components/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["index.ts", "src"]
|
||||
}
|
||||
18
packages/functions-sdk/package.json
Normal file
18
packages/functions-sdk/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@project/functions-sdk",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./pikku/api.gen": "./src/pikku/api.gen.ts",
|
||||
"./pikku/pikku-fetch.gen": "./src/pikku/pikku-fetch.gen.ts",
|
||||
"./pikku/pikku-rpc.gen": "./src/pikku/pikku-rpc.gen.ts",
|
||||
"./pikku/rpc-map.gen": "./src/pikku/rpc-map.gen.d.ts",
|
||||
"./pikku/http-map.gen": "./src/pikku/http-map.gen.d.ts",
|
||||
"./pikku/*": "./src/pikku/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pikku/fetch": "^0.12.5",
|
||||
"@pikku/react": "^0.12.4"
|
||||
}
|
||||
}
|
||||
56
packages/functions-sdk/src/pikku/api.gen.ts
Normal file
56
packages/functions-sdk/src/pikku/api.gen.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import { useQuery, useInfiniteQuery, useMutation, type UseQueryOptions, type UseInfiniteQueryOptions, type UseMutationOptions, type InfiniteData } from '@tanstack/react-query'
|
||||
import { usePikkuRPC } from '@pikku/react'
|
||||
import type { FlattenedRPCMap } from '../../../functions/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
|
||||
|
||||
type RPCInvoke = <Name extends keyof FlattenedRPCMap>(name: Name, data: FlattenedRPCMap[Name]['input']) => Promise<FlattenedRPCMap[Name]['output']>
|
||||
|
||||
export const usePikkuQuery = <Name extends keyof FlattenedRPCMap>(
|
||||
name: Name,
|
||||
data: FlattenedRPCMap[Name]['input'],
|
||||
options?: Omit<UseQueryOptions<FlattenedRPCMap[Name]['output'], Error>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
|
||||
return useQuery<FlattenedRPCMap[Name]['output'], Error>({
|
||||
queryKey: [name, data],
|
||||
queryFn: () => rpc.invoke(name, data),
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePikkuMutation = <Name extends keyof FlattenedRPCMap>(
|
||||
name: Name,
|
||||
options?: Omit<UseMutationOptions<FlattenedRPCMap[Name]['output'], Error, FlattenedRPCMap[Name]['input']>, 'mutationFn'>
|
||||
) => {
|
||||
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
|
||||
return useMutation<FlattenedRPCMap[Name]['output'], Error, FlattenedRPCMap[Name]['input']>({
|
||||
mutationFn: (data) => rpc.invoke(name, data),
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
type PaginatedKeys = {
|
||||
[K in keyof FlattenedRPCMap]: FlattenedRPCMap[K]['output'] extends { nextCursor?: string | null } ? K : never
|
||||
}[keyof FlattenedRPCMap]
|
||||
|
||||
type InfiniteOpts<Name extends PaginatedKeys> = Omit<
|
||||
UseInfiniteQueryOptions<FlattenedRPCMap[Name]['output'], Error, InfiniteData<FlattenedRPCMap[Name]['output'], string | undefined>, readonly unknown[], string | undefined>,
|
||||
'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'
|
||||
>
|
||||
|
||||
export const usePikkuInfiniteQuery = <Name extends PaginatedKeys>(
|
||||
name: Name,
|
||||
data: Omit<FlattenedRPCMap[Name]['input'], 'cursor'>,
|
||||
options?: InfiniteOpts<Name>
|
||||
) => {
|
||||
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
|
||||
return useInfiniteQuery({
|
||||
queryKey: [name, data] as const,
|
||||
queryFn: ({ pageParam }: { pageParam: string | undefined }) => rpc.invoke(name, { ...data, cursor: pageParam } as unknown as FlattenedRPCMap[Name]['input']),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage: FlattenedRPCMap[Name]['output']) => (lastPage as { nextCursor?: string | null }).nextCursor ?? undefined,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
67
packages/functions-sdk/src/pikku/pikku-fetch.gen.ts
Normal file
67
packages/functions-sdk/src/pikku/pikku-fetch.gen.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
|
||||
import { CorePikkuFetch, HTTPMethod } from '@pikku/fetch'
|
||||
import type { HTTPWiringsMap, HTTPWiringHandlerOf, HTTPWiringsWithMethod } from '../../../functions/.pikku/http/pikku-http-wirings-map.gen.d.js'
|
||||
|
||||
export class PikkuFetch extends CorePikkuFetch {
|
||||
public async post<Route extends HTTPWiringsWithMethod<'POST'>>(
|
||||
route: Route,
|
||||
...args: null extends HTTPWiringHandlerOf<Route, 'POST'>['input']
|
||||
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'POST'>['input'], null>, options?: Omit<RequestInit, 'body'>]
|
||||
: [data: HTTPWiringHandlerOf<Route, 'POST'>['input'], options?: Omit<RequestInit, 'body'>]
|
||||
): Promise<HTTPWiringHandlerOf<Route, 'POST'>['output']> {
|
||||
const [data, options] = args;
|
||||
return super.api(route, 'POST', data, options);
|
||||
}
|
||||
|
||||
public async get<Route extends HTTPWiringsWithMethod<'GET'>>(
|
||||
route: Route,
|
||||
...args: null extends HTTPWiringHandlerOf<Route, 'GET'>['input']
|
||||
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'GET'>['input'], null>, options?: Omit<RequestInit, 'body'>]
|
||||
: [data: HTTPWiringHandlerOf<Route, 'GET'>['input'], options?: Omit<RequestInit, 'body'>]
|
||||
): Promise<HTTPWiringHandlerOf<Route, 'GET'>['output']> {
|
||||
const [data, options] = args;
|
||||
return super.api(route, 'GET', data, options);
|
||||
}
|
||||
|
||||
public async patch<Route extends HTTPWiringsWithMethod<'PATCH'>>(
|
||||
route: Route,
|
||||
...args: null extends HTTPWiringHandlerOf<Route, 'PATCH'>['input']
|
||||
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'PATCH'>['input'], null>, options?: Omit<RequestInit, 'body'>]
|
||||
: [data: HTTPWiringHandlerOf<Route, 'PATCH'>['input'], options?: Omit<RequestInit, 'body'>]
|
||||
): Promise<HTTPWiringHandlerOf<Route, 'PATCH'>['output']> {
|
||||
const [data, options] = args;
|
||||
return super.api(route, 'PATCH', data, options);
|
||||
}
|
||||
|
||||
public async head<Route extends HTTPWiringsWithMethod<'HEAD'>>(
|
||||
route: Route,
|
||||
...args: null extends HTTPWiringHandlerOf<Route, 'HEAD'>['input']
|
||||
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'HEAD'>['input'], null>, options?: Omit<RequestInit, 'body'>]
|
||||
: [data: HTTPWiringHandlerOf<Route, 'HEAD'>['input'], options?: Omit<RequestInit, 'body'>]
|
||||
): Promise<HTTPWiringHandlerOf<Route, 'HEAD'>['output']> {
|
||||
const [data, options] = args;
|
||||
return super.api(route, 'HEAD', data, options);
|
||||
}
|
||||
|
||||
public async delete<Route extends HTTPWiringsWithMethod<'DELETE'>>(
|
||||
route: Route,
|
||||
...args: null extends HTTPWiringHandlerOf<Route, 'DELETE'>['input']
|
||||
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'DELETE'>['input'], null>, options?: Omit<RequestInit, 'body'>]
|
||||
: [data: HTTPWiringHandlerOf<Route, 'DELETE'>['input'], options?: Omit<RequestInit, 'body'>]
|
||||
): Promise<HTTPWiringHandlerOf<Route, 'DELETE'>['output']> {
|
||||
const [data, options] = args;
|
||||
return super.api(route, 'DELETE', data, options);
|
||||
}
|
||||
|
||||
public async fetch<
|
||||
Route extends keyof HTTPWiringsMap,
|
||||
Method extends keyof HTTPWiringsMap[Route]
|
||||
>(route: Route, method: Method, data: HTTPWiringHandlerOf<Route, Method>['input'], options?: Omit<RequestInit, 'body'>): Promise<Response> {
|
||||
return await super.fetch(route, method as HTTPMethod, data, options);
|
||||
}
|
||||
}
|
||||
|
||||
export const pikkuFetch = new PikkuFetch();
|
||||
148
packages/functions-sdk/src/pikku/pikku-rpc.gen.ts
Normal file
148
packages/functions-sdk/src/pikku/pikku-rpc.gen.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
|
||||
import { PikkuFetch } from "./pikku-fetch.gen.js"
|
||||
import type { RPCInvoke, TypedAgentRun, TypedStartWorkflow, TypedRunWorkflow, TypedWorkflowStatus } from '../../../functions/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
|
||||
import type { WorkflowRunStatus } from '@pikku/core/workflow'
|
||||
|
||||
/**
|
||||
* PikkuRPC provides a type-safe client for making Remote Procedure Calls (RPC)
|
||||
* to your Pikku server. It wraps the underlying HTTP client and provides a
|
||||
* simple interface for invoking server-side functions.
|
||||
*/
|
||||
export class PikkuRPC {
|
||||
/** The underlying HTTP client used for making RPC calls */
|
||||
pikkuFetch = new PikkuFetch()
|
||||
|
||||
/**
|
||||
* Sets a custom PikkuFetch instance to use for RPC calls.
|
||||
* This allows you to configure custom settings or use a shared client instance.
|
||||
*
|
||||
* @param pikkuFetch - The PikkuFetch instance to use
|
||||
*/
|
||||
setPikkuFetch(pikkuFetch: PikkuFetch): void {
|
||||
this.pikkuFetch = pikkuFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base server URL for all RPC calls.
|
||||
*
|
||||
* @param serverUrl - The base URL of your Pikku server (e.g., 'https://api.example.com')
|
||||
*/
|
||||
setServerUrl(serverUrl: string): void {
|
||||
this.pikkuFetch.setServerUrl(serverUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWT token for authorization on all RPC calls.
|
||||
*
|
||||
* @param jwt - The JWT token to use for authorization, or null to remove authorization
|
||||
*/
|
||||
setAuthorizationJWT(jwt: string | null): void {
|
||||
this.pikkuFetch.setAuthorizationJWT(jwt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API key for authorization on all RPC calls.
|
||||
*
|
||||
* @param apiKey - The API key to use for authorization, or null to remove the API key
|
||||
*/
|
||||
setAPIKey(apiKey: string | null): void {
|
||||
this.pikkuFetch.setAPIKey(apiKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a remote procedure call on the server.
|
||||
* This is a generic method that routes to the appropriate server function
|
||||
* based on the function name and passes the provided data.
|
||||
*
|
||||
* @param rpcName - The name of the server function to invoke
|
||||
* @param data - The data to pass to the server function
|
||||
* @returns A promise that resolves with the function's return value
|
||||
*/
|
||||
invoke = ((rpcName: string, data?: unknown) => {
|
||||
return this.pikkuFetch.post(`/rpc/${String(rpcName)}` as never, { rpcName: String(rpcName), data }) as any
|
||||
}) as RPCInvoke
|
||||
|
||||
/**
|
||||
* Starts a workflow by name with the given input.
|
||||
* Posts to \`/workflow/:workflowName/start\`.
|
||||
*
|
||||
* @param workflowName - The registered workflow name
|
||||
* @param input - The workflow input data
|
||||
* @returns A promise that resolves with the new run ID
|
||||
*/
|
||||
startWorkflow: TypedStartWorkflow = async (workflowName, input) => {
|
||||
return await this.pikkuFetch.post(`/workflow/${String(workflowName)}/start` as never, { data: input }) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a workflow to completion and returns the output.
|
||||
* Posts to \`/workflow/:workflowName/run\`.
|
||||
*
|
||||
* @param workflowName - The registered workflow name
|
||||
* @param input - The workflow input data
|
||||
* @returns A promise that resolves with the workflow output
|
||||
*/
|
||||
runWorkflow: TypedRunWorkflow = async (workflowName, input) => {
|
||||
return await this.pikkuFetch.post(`/workflow/${String(workflowName)}/run` as never, { data: input }) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current status of a workflow run.
|
||||
* GET \`/workflow/:workflowName/status/:runId\`.
|
||||
*
|
||||
* @param workflowName - The registered workflow name
|
||||
* @param runId - The workflow run ID
|
||||
* @returns A promise with the minimal run status
|
||||
*/
|
||||
workflowStatus: TypedWorkflowStatus = async (workflowName, runId) => {
|
||||
return await this.pikkuFetch.get(`/workflow/${String(workflowName)}/status/${runId}` as never) as unknown as WorkflowRunStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent namespace — methods for running, streaming, and approving AI agents.
|
||||
* All methods post to \`/rpc/agent/:agentName/*\`.
|
||||
*/
|
||||
agent = {
|
||||
/**
|
||||
* Runs an agent to completion and returns the result.
|
||||
*
|
||||
* @param agentName - The registered agent name
|
||||
* @param input - The agent input (message, threadId, resourceId)
|
||||
* @returns A promise with runId, result, and token usage
|
||||
*/
|
||||
run: (async (agentName, input) => {
|
||||
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}` as never, input) as any
|
||||
}) as TypedAgentRun,
|
||||
/**
|
||||
* Streams agent responses via SSE. Used for real-time chat interfaces.
|
||||
*
|
||||
* @param agentName - The registered agent name
|
||||
* @param input - The agent input (message, threadId, resourceId)
|
||||
*/
|
||||
stream: async (agentName: string, input: Record<string, unknown>) => {
|
||||
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}/stream` as never, input) as any
|
||||
},
|
||||
/**
|
||||
* Approves or denies pending tool calls for an agent run.
|
||||
*
|
||||
* @param agentName - The registered agent name
|
||||
* @param input - The approval payload (runId, approvals array)
|
||||
*/
|
||||
approve: async (agentName: string, input: Record<string, unknown>) => {
|
||||
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}/approve` as never, input) as any
|
||||
},
|
||||
}
|
||||
|
||||
subscribeToSSE<T = unknown>(
|
||||
path: string,
|
||||
handler: (event: T) => void,
|
||||
onError?: (err: unknown) => void
|
||||
): { close: () => void } {
|
||||
return this.pikkuFetch.subscribeToSSE(path, handler, onError)
|
||||
}
|
||||
}
|
||||
|
||||
export const pikkuRPC = new PikkuRPC();
|
||||
4
packages/functions/.pikku/agent/pikku-agent-map.gen.d.ts
vendored
Normal file
4
packages/functions/.pikku/agent/pikku-agent-map.gen.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
export type AgentMap = {}
|
||||
82
packages/functions/.pikku/agent/pikku-agent-types.gen.ts
Normal file
82
packages/functions/.pikku/agent/pikku-agent-types.gen.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import {
|
||||
CoreAIAgent,
|
||||
PikkuAIMiddlewareHooks,
|
||||
} from '@pikku/core/ai-agent'
|
||||
import {
|
||||
agent as coreAgent,
|
||||
agentStream as coreAgentStream,
|
||||
agentResume as coreAgentResume,
|
||||
agentApprove as coreAgentApprove,
|
||||
} from '@pikku/core/ai-agent'
|
||||
import type { PikkuPermission, PikkuMiddleware, Services, PikkuFunctionConfig } from '../function/pikku-function-types.gen.js'
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
import type { AIAgentMemoryConfig, AIAgentInput } from '@pikku/core/ai-agent'
|
||||
import type { AgentMap } from './pikku-agent-map.gen.d.js'
|
||||
|
||||
type AIAgentConfig<
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
> = Omit<CoreAIAgent<PikkuPermission, PikkuMiddleware>, 'tools' | 'agents' | 'memory' | 'input' | 'output'> & {
|
||||
input?: InputSchema
|
||||
output?: OutputSchema
|
||||
memory?: Omit<AIAgentMemoryConfig, 'workingMemory'> & { workingMemory?: StandardSchemaV1 }
|
||||
tools?: object[]
|
||||
agents?: AIAgentConfig<StandardSchemaV1 | undefined, StandardSchemaV1 | undefined>[]
|
||||
}
|
||||
|
||||
export const pikkuAIAgent = <
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
>(
|
||||
agent: AIAgentConfig<InputSchema, OutputSchema>
|
||||
) => {
|
||||
return agent
|
||||
}
|
||||
|
||||
export const pikkuAIMiddleware = <
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
RequiredServices extends Services = Services,
|
||||
>(
|
||||
hooks: PikkuAIMiddlewareHooks<State, RequiredServices>
|
||||
): PikkuAIMiddlewareHooks<State, RequiredServices> => hooks
|
||||
|
||||
export const agent = <Name extends keyof AgentMap>(
|
||||
agentName: Name
|
||||
) => {
|
||||
return coreAgent<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
|
||||
AIAgentInput,
|
||||
{ runId: string; result: AgentMap[Name]['output']; usage: { inputTokens: number; outputTokens: number } },
|
||||
'session' | 'rpc'
|
||||
>
|
||||
}
|
||||
|
||||
export const agentStream = <Name extends keyof AgentMap>(
|
||||
agentName?: Name
|
||||
) => {
|
||||
return coreAgentStream<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
|
||||
{ agentName?: string; message: string; threadId: string; resourceId: string },
|
||||
void,
|
||||
'session' | 'rpc'
|
||||
>
|
||||
}
|
||||
|
||||
export const agentResume = () => {
|
||||
return coreAgentResume() as PikkuFunctionConfig<
|
||||
{ runId: string; toolCallId: string; approved: boolean },
|
||||
void,
|
||||
'session' | 'rpc'
|
||||
>
|
||||
}
|
||||
|
||||
export const agentApprove = <Name extends keyof AgentMap>(
|
||||
agentName: Name
|
||||
) => {
|
||||
return coreAgentApprove<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
|
||||
{ runId: string; approvals: { toolCallId: string; approved: boolean }[] },
|
||||
unknown,
|
||||
'session' | 'rpc'
|
||||
>
|
||||
}
|
||||
26
packages/functions/.pikku/auth/auth.types.ts
Normal file
26
packages/functions/.pikku/auth/auth.types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
// AUTO-GENERATED by pikku CLI — do not edit
|
||||
|
||||
import { pikkuBetterAuth as _pikkuBetterAuth } from '@pikku/better-auth'
|
||||
import type { BetterAuthInstance, PikkuBetterAuthFactory } from '@pikku/better-auth'
|
||||
import type { SingletonServices } from '../function/pikku-function-types.gen.js'
|
||||
import { TypedSecretService } from '../secrets/pikku-secrets.gen.js'
|
||||
import { TypedVariablesService } from '../variables/pikku-variables.gen.js'
|
||||
|
||||
type AuthSingletonServices = Omit<SingletonServices, 'secrets' | 'variables'> & {
|
||||
secrets: TypedSecretService
|
||||
variables: TypedVariablesService
|
||||
}
|
||||
|
||||
export const pikkuBetterAuth = <I extends BetterAuthInstance>(
|
||||
factory: (services: AuthSingletonServices) => I | Promise<I>
|
||||
): PikkuBetterAuthFactory<I> =>
|
||||
_pikkuBetterAuth((services) =>
|
||||
factory({
|
||||
...services,
|
||||
secrets: new TypedSecretService(services.secrets),
|
||||
variables: new TypedVariablesService(services.variables),
|
||||
} as AuthSingletonServices)
|
||||
)
|
||||
6
packages/functions/.pikku/auth/pikku-auth-meta.gen.json
Normal file
6
packages/functions/.pikku/auth/pikku-auth-meta.gen.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"basePath": "/api/auth",
|
||||
"hasCredentials": true,
|
||||
"providers": [],
|
||||
"plugins": []
|
||||
}
|
||||
177
packages/functions/.pikku/channel/pikku-channel-types.gen.ts
Normal file
177
packages/functions/.pikku/channel/pikku-channel-types.gen.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
* Channel-specific type definitions for tree-shaking optimization
|
||||
*/
|
||||
|
||||
import { CoreChannel, CorePikkuChannelMiddleware, CorePikkuChannelMiddlewareFactory, wireChannel as wireChannelCore, defineChannelRoutes as defineChannelRoutesCore, addChannelMiddleware as addChannelMiddlewareCore } from '@pikku/core/channel'
|
||||
import { AssertHTTPWiringParams } from '@pikku/core/http'
|
||||
import type { PikkuFunctionConfig, PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware, Services } from '../function/pikku-function-types.gen.js'
|
||||
import type { CorePermissionGroup } from '@pikku/core'
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
/**
|
||||
* Helper type to infer the output type from a Standard Schema
|
||||
*/
|
||||
type InferSchemaOutput<T> = T extends StandardSchemaV1<any, infer Output> ? Output : never
|
||||
|
||||
/**
|
||||
* Type definition for WebSocket channels with typed data exchange.
|
||||
* Supports connection, disconnection, and message handling.
|
||||
* Accepts both session-based (PikkuFunction) and sessionless (PikkuFunctionSessionless) functions.
|
||||
*
|
||||
* @template ChannelData - Type of data exchanged through the channel
|
||||
* @template Channel - String literal type for the channel name
|
||||
*/
|
||||
type ChannelWiring<ChannelData, Channel extends string> = CoreChannel<
|
||||
ChannelData,
|
||||
Channel,
|
||||
PikkuFunctionConfig<void, any, 'channel' | 'session' | 'rpc'>,
|
||||
PikkuFunctionConfig<void, void, 'channel' | 'session' | 'rpc'>,
|
||||
PikkuFunctionConfig<any, any, 'channel' | 'session' | 'rpc'>,
|
||||
PikkuPermission,
|
||||
PikkuMiddleware
|
||||
>
|
||||
|
||||
/**
|
||||
* Creates a function that handles WebSocket channel connections.
|
||||
* Called when a client connects to a channel.
|
||||
*
|
||||
* @template Out - Output type for connection response
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*/
|
||||
export const pikkuChannelConnectionFunc = <Out = unknown>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<void, Out, 'channel' | 'session' | 'rpc'>
|
||||
| PikkuFunctionConfig<void, Out, 'channel' | 'session' | 'rpc'>
|
||||
) => {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that handles WebSocket channel disconnections.
|
||||
* Called when a client disconnects from a channel.
|
||||
*
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*/
|
||||
export const pikkuChannelDisconnectionFunc = (
|
||||
func:
|
||||
| PikkuFunctionSessionless<void, void, 'channel'>
|
||||
| PikkuFunctionConfig<void, void, 'channel' | 'session' | 'rpc'>
|
||||
) => {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for channel functions with Zod schema validation.
|
||||
*/
|
||||
type PikkuChannelFuncConfigWithSchema<
|
||||
InputSchema extends StandardSchemaV1,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
> = {
|
||||
name?: string
|
||||
tags?: string[]
|
||||
expose?: boolean
|
||||
remote?: boolean
|
||||
func: PikkuFunctionSessionless<
|
||||
InferSchemaOutput<InputSchema>,
|
||||
OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
|
||||
'channel' | 'session' | 'rpc'
|
||||
>
|
||||
auth?: boolean
|
||||
permissions?: CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
|
||||
middleware?: PikkuMiddleware[]
|
||||
input: InputSchema
|
||||
output?: OutputSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that handles WebSocket channel messages.
|
||||
* Called when a message is received on a channel.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuChannelFunc<Input, Output>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuChannelFunc({ input: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for channel messages (inferred from schema if provided)
|
||||
* @template Out - Output type for channel responses (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Pattern 1: Using generic types
|
||||
* const handleMessage = pikkuChannelFunc<{text: string}, {received: boolean}>({
|
||||
* func: async (_services, { text }) => ({ received: true })
|
||||
* })
|
||||
*
|
||||
* // Pattern 2: Using Zod schemas
|
||||
* const messageInput = z.object({ text: z.string() })
|
||||
* const messageOutput = z.object({ received: z.boolean() })
|
||||
*
|
||||
* const handleMessage = pikkuChannelFunc({
|
||||
* input: messageInput,
|
||||
* output: messageOutput,
|
||||
* func: async (_services, { text }) => ({ received: true })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function pikkuChannelFunc<
|
||||
InputSchema extends StandardSchemaV1,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
>(
|
||||
config: PikkuChannelFuncConfigWithSchema<InputSchema, OutputSchema>
|
||||
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'channel' | 'session' | 'rpc'>
|
||||
export function pikkuChannelFunc<In, Out = unknown>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<In, Out, 'channel' | 'session' | 'rpc'>
|
||||
| PikkuFunctionConfig<In, Out, 'channel' | 'session' | 'rpc'>
|
||||
): PikkuFunctionConfig<In, Out, 'channel' | 'session' | 'rpc'>
|
||||
export function pikkuChannelFunc(func: any) {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a WebSocket channel with the Pikku framework.
|
||||
*
|
||||
* @template ChannelData - Type of data associated with the channel
|
||||
* @template Channel - String literal type for the channel name
|
||||
* @param channel - Channel definition with connection, disconnection, and message handlers
|
||||
*/
|
||||
export const wireChannel = <ChannelData, Channel extends string>(
|
||||
channel: ChannelWiring<ChannelData, Channel> & AssertHTTPWiringParams<ChannelData, Channel>
|
||||
) => {
|
||||
wireChannelCore(channel as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe helper for defining channel message routes that can be composed.
|
||||
* Returns the routes record as-is for use with wireChannel's onMessageWiring.
|
||||
*
|
||||
* @template T - Record of channel route handlers
|
||||
* @param routes - The channel routes record
|
||||
* @returns The same routes record (identity function for type safety)
|
||||
*/
|
||||
export function defineChannelRoutes<T extends Record<string, any>>(routes: T): T {
|
||||
return defineChannelRoutesCore(routes)
|
||||
}
|
||||
|
||||
export type PikkuChannelMiddleware<RequiredServices extends Services = Services, Event = unknown> = CorePikkuChannelMiddleware<RequiredServices, Event>
|
||||
|
||||
export const pikkuChannelMiddleware = <RequiredServices extends Services = Services, Event = unknown>(
|
||||
middleware: PikkuChannelMiddleware<RequiredServices, Event>
|
||||
): PikkuChannelMiddleware<RequiredServices, Event> => {
|
||||
return middleware
|
||||
}
|
||||
|
||||
export const pikkuChannelMiddlewareFactory = <In = any>(
|
||||
factory: CorePikkuChannelMiddlewareFactory<In>
|
||||
): CorePikkuChannelMiddlewareFactory<In> => {
|
||||
return factory
|
||||
}
|
||||
|
||||
export const addChannelMiddleware = (tag: string, middleware: PikkuChannelMiddleware[]) =>
|
||||
addChannelMiddlewareCore(tag, middleware, null)
|
||||
105
packages/functions/.pikku/cli/pikku-cli-types.gen.ts
Normal file
105
packages/functions/.pikku/cli/pikku-cli-types.gen.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
|
||||
* CLI-specific type definitions for tree-shaking optimization
|
||||
*/
|
||||
|
||||
import { CoreCLI, wireCLI as wireCLICore, CorePikkuCLIRender, CoreCLICommandConfig, defineCLICommands as defineCLICommandsCore } from '@pikku/core/cli'
|
||||
import type { PikkuFunctionConfig, PikkuMiddleware } from '../function/pikku-function-types.gen.js'
|
||||
import type { UserSession } from '../../src/application-types.d.js'
|
||||
import type { SingletonServices } from '../../src/application-types.d.js'
|
||||
|
||||
|
||||
type Session = UserSession
|
||||
|
||||
/**
|
||||
* Type-safe CLI renderer definition that can access your application's services.
|
||||
* Use this to define custom renderers for CLI command output.
|
||||
*
|
||||
* @template Data - The output data type from the CLI command
|
||||
* @template RequiredServices - The services required for this renderer
|
||||
*/
|
||||
type PikkuCLIRender<Data, RequiredServices extends SingletonServices = SingletonServices> = CorePikkuCLIRender<Data, RequiredServices, Session>
|
||||
|
||||
/**
|
||||
* Creates a type-safe CLI renderer with access to your application's singleton services.
|
||||
* The renderer receives the full singleton services and output data to format and display results.
|
||||
*
|
||||
* @template Data - The output data type from the CLI command
|
||||
* @template RequiredServices - The minimum services required for type checking (defaults to SingletonServices)
|
||||
* @param render - Function that receives singleton services and data to render output
|
||||
* @returns A CLI renderer configuration
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const myRenderer = pikkuCLIRender<MyData>(({ logger }, data) => {
|
||||
* logger.info(data.message)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuCLIRender = <Data, RequiredServices extends SingletonServices = SingletonServices>(
|
||||
render: (services: SingletonServices, data: Data) => void | Promise<void>
|
||||
): PikkuCLIRender<Data, RequiredServices> => {
|
||||
return render as any
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI command configuration with project-specific types.
|
||||
* Uses CoreCLICommandConfig from @pikku/core with local middleware and render types.
|
||||
*/
|
||||
type CLICommandConfig<Func extends PikkuFunctionConfig<In, Out, 'cli' | 'rpc' | 'session'>, In = any, Out = any, Params extends string = string> = CoreCLICommandConfig<Func, PikkuMiddleware, PikkuCLIRender<any>, Params>
|
||||
|
||||
/**
|
||||
* Type definition for CLI applications with commands and global options.
|
||||
*
|
||||
* @template Commands - Type describing the command structure
|
||||
* @template GlobalOptions - Type for global CLI options
|
||||
*/
|
||||
type CLIWiring<Commands extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>, GlobalOptions> = CoreCLI<Commands, GlobalOptions, PikkuMiddleware, PikkuCLIRender<any>>
|
||||
|
||||
/**
|
||||
* Registers a CLI application with the Pikku framework.
|
||||
* Creates command-line interfaces with type-safe commands and options.
|
||||
*
|
||||
* @template Commands - Type describing the command structure
|
||||
* @template GlobalOptions - Type for global CLI options
|
||||
* @param cli - CLI definition with program name, commands, and global options
|
||||
*/
|
||||
export const wireCLI = <Commands extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>, GlobalOptions>(
|
||||
cli: CLIWiring<Commands, GlobalOptions>
|
||||
) => {
|
||||
wireCLICore(cli as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CLI command definition with automatic option inference from the function's input type.
|
||||
* This allows TypeScript to automatically derive CLI options from the function signature.
|
||||
*
|
||||
* @template FuncConfig - The Pikku function config type
|
||||
* @template Params - The parameters string literal type
|
||||
* @param config - CLI command configuration
|
||||
* @returns CLI command configuration with inferred types
|
||||
*/
|
||||
export const pikkuCLICommand = <
|
||||
FuncConfig extends PikkuFunctionConfig<any, any, 'cli' | 'rpc' | 'session'>,
|
||||
Params extends string
|
||||
>(
|
||||
config: CLICommandConfig<FuncConfig, any, any, Params>
|
||||
): CoreCLICommandConfig<FuncConfig, PikkuMiddleware, PikkuCLIRender<any>, string> => {
|
||||
return config as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe helper for defining CLI commands that can be composed and spread into wireCLI.
|
||||
*
|
||||
* @template T - Record of CLI command configurations
|
||||
* @param commands - The CLI commands record
|
||||
* @returns The same commands record (identity function for type safety)
|
||||
*/
|
||||
export const defineCLICommands = <T extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>>(
|
||||
commands: T
|
||||
): T => {
|
||||
return defineCLICommandsCore(commands as any) as T
|
||||
}
|
||||
14
packages/functions/.pikku/console/pikku-addon-meta.gen.json
Normal file
14
packages/functions/.pikku/console/pikku-addon-meta.gen.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"nodes": {},
|
||||
"secrets": {
|
||||
"betterAuthSecret": {
|
||||
"name": "betterAuthSecret",
|
||||
"displayName": "Better Auth Secret",
|
||||
"description": "Signing secret for better-auth sessions",
|
||||
"secretId": "BETTER_AUTH_SECRET",
|
||||
"schema": "SecretSchema_betterAuthSecret",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth-secrets.gen.ts"
|
||||
}
|
||||
},
|
||||
"package": {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import type { FlattenedRPCMap } from '../rpc/pikku-rpc-wirings-map.internal.gen.js'
|
||||
|
||||
export type NodeCategory = never
|
||||
|
||||
export type NodeRPCName = keyof FlattenedRPCMap
|
||||
89
packages/functions/.pikku/db/classification-map.gen.d.ts
vendored
Normal file
89
packages/functions/.pikku/db/classification-map.gen.d.ts
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
// Generated by @pikku/cli — do not edit by hand.
|
||||
// Run `pikku db migrate` to refresh.
|
||||
// Use this type in db/classifications.ts:
|
||||
// import type { DbClassificationMap } from './.pikku/db/classification-map.gen.d.ts'
|
||||
// export const classifications = { ... } satisfies DbClassificationMap
|
||||
|
||||
export type ColumnEntry = {
|
||||
/** Privacy level. Defaults to 'private' when omitted. */
|
||||
security?: 'public' | 'private' | 'pii' | 'secret' | 'encrypted'
|
||||
/** Anonymize strategy used by `pikku db anonymize`. */
|
||||
classification?: 'fake:email' | 'fake:name' | 'hash' | 'keep'
|
||||
/** Column kind override for codegen coercion + typing. */
|
||||
kind?: 'date' | 'bool' | 'json' | 'uuid'
|
||||
/** TypeScript type override, e.g. `string[]` or `MyJson`. Wins over `kind`. */
|
||||
tsType?: string
|
||||
/** Zod string-format validator (keeps the TS type as `string`). */
|
||||
format?: 'email' | 'url' | 'emoji' | 'e164' | 'jwt' | 'cuid' | 'cuid2' | 'ulid' | 'nanoid' | 'base64' | 'base64url' | 'ipv4' | 'ipv6' | 'cidrv4' | 'cidrv6' | 'isoDate' | 'isoTime' | 'isoDatetime' | 'isoDuration'
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type DbClassificationMap = {
|
||||
"account": {
|
||||
"id": ColumnEntry
|
||||
"account_id": ColumnEntry
|
||||
"provider_id": ColumnEntry
|
||||
"user_id": ColumnEntry
|
||||
"access_token": ColumnEntry
|
||||
"refresh_token": ColumnEntry
|
||||
"id_token": ColumnEntry
|
||||
"access_token_expires_at": ColumnEntry
|
||||
"refresh_token_expires_at": ColumnEntry
|
||||
"scope": ColumnEntry
|
||||
"password": ColumnEntry
|
||||
"created_at": ColumnEntry
|
||||
"updated_at": ColumnEntry
|
||||
}
|
||||
"audit": {
|
||||
"audit_id": ColumnEntry
|
||||
"occurred_at": ColumnEntry
|
||||
"type": ColumnEntry
|
||||
"source": ColumnEntry
|
||||
"outcome": ColumnEntry
|
||||
"function_id": ColumnEntry
|
||||
"wire_type": ColumnEntry
|
||||
"trace_id": ColumnEntry
|
||||
"transaction_id": ColumnEntry
|
||||
"query_id": ColumnEntry
|
||||
"actor_user_id": ColumnEntry
|
||||
"actor_org_id": ColumnEntry
|
||||
"tables": ColumnEntry
|
||||
"changed_cols": ColumnEntry
|
||||
"event": ColumnEntry
|
||||
"old": ColumnEntry
|
||||
"data": ColumnEntry
|
||||
}
|
||||
"message_state": {
|
||||
"id": ColumnEntry
|
||||
"message": ColumnEntry
|
||||
"updated_at": ColumnEntry
|
||||
"updated_by_user_id": ColumnEntry
|
||||
}
|
||||
"session": {
|
||||
"id": ColumnEntry
|
||||
"expires_at": ColumnEntry
|
||||
"token": ColumnEntry
|
||||
"created_at": ColumnEntry
|
||||
"updated_at": ColumnEntry
|
||||
"ip_address": ColumnEntry
|
||||
"user_agent": ColumnEntry
|
||||
"user_id": ColumnEntry
|
||||
}
|
||||
"user": {
|
||||
"id": ColumnEntry
|
||||
"name": ColumnEntry
|
||||
"email": ColumnEntry
|
||||
"email_verified": ColumnEntry
|
||||
"image": ColumnEntry
|
||||
"created_at": ColumnEntry
|
||||
"updated_at": ColumnEntry
|
||||
}
|
||||
"verification": {
|
||||
"id": ColumnEntry
|
||||
"identifier": ColumnEntry
|
||||
"value": ColumnEntry
|
||||
"expires_at": ColumnEntry
|
||||
"created_at": ColumnEntry
|
||||
"updated_at": ColumnEntry
|
||||
}
|
||||
}
|
||||
75
packages/functions/.pikku/db/classification.gen.ts
Normal file
75
packages/functions/.pikku/db/classification.gen.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// Generated by @pikku/cli — do not edit by hand.
|
||||
// Run `pikku db migrate` to refresh.
|
||||
|
||||
export const classificationManifest = {
|
||||
version: 1 as const,
|
||||
tables: {
|
||||
"account": {
|
||||
"id": { classification: 'private', anonymize_strategy: null },
|
||||
"account_id": { classification: 'private', anonymize_strategy: null },
|
||||
"provider_id": { classification: 'private', anonymize_strategy: null },
|
||||
"user_id": { classification: 'private', anonymize_strategy: null },
|
||||
"access_token": { classification: 'private', anonymize_strategy: null },
|
||||
"refresh_token": { classification: 'private', anonymize_strategy: null },
|
||||
"id_token": { classification: 'private', anonymize_strategy: null },
|
||||
"access_token_expires_at": { classification: 'private', anonymize_strategy: null },
|
||||
"refresh_token_expires_at": { classification: 'private', anonymize_strategy: null },
|
||||
"scope": { classification: 'private', anonymize_strategy: null },
|
||||
"password": { classification: 'private', anonymize_strategy: null },
|
||||
"created_at": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_at": { classification: 'private', anonymize_strategy: null }
|
||||
},
|
||||
"audit": {
|
||||
"audit_id": { classification: 'private', anonymize_strategy: null },
|
||||
"occurred_at": { classification: 'private', anonymize_strategy: null },
|
||||
"type": { classification: 'private', anonymize_strategy: null },
|
||||
"source": { classification: 'private', anonymize_strategy: null },
|
||||
"outcome": { classification: 'private', anonymize_strategy: null },
|
||||
"function_id": { classification: 'private', anonymize_strategy: null },
|
||||
"wire_type": { classification: 'private', anonymize_strategy: null },
|
||||
"trace_id": { classification: 'private', anonymize_strategy: null },
|
||||
"transaction_id": { classification: 'private', anonymize_strategy: null },
|
||||
"query_id": { classification: 'private', anonymize_strategy: null },
|
||||
"actor_user_id": { classification: 'private', anonymize_strategy: null },
|
||||
"actor_org_id": { classification: 'private', anonymize_strategy: null },
|
||||
"tables": { classification: 'private', anonymize_strategy: null },
|
||||
"changed_cols": { classification: 'private', anonymize_strategy: null },
|
||||
"event": { classification: 'private', anonymize_strategy: null },
|
||||
"old": { classification: 'private', anonymize_strategy: null },
|
||||
"data": { classification: 'private', anonymize_strategy: null }
|
||||
},
|
||||
"message_state": {
|
||||
"id": { classification: 'private', anonymize_strategy: null },
|
||||
"message": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_at": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_by_user_id": { classification: 'private', anonymize_strategy: null }
|
||||
},
|
||||
"session": {
|
||||
"id": { classification: 'private', anonymize_strategy: null },
|
||||
"expires_at": { classification: 'private', anonymize_strategy: null },
|
||||
"token": { classification: 'private', anonymize_strategy: null },
|
||||
"created_at": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_at": { classification: 'private', anonymize_strategy: null },
|
||||
"ip_address": { classification: 'private', anonymize_strategy: null },
|
||||
"user_agent": { classification: 'private', anonymize_strategy: null },
|
||||
"user_id": { classification: 'private', anonymize_strategy: null }
|
||||
},
|
||||
"user": {
|
||||
"id": { classification: 'private', anonymize_strategy: null },
|
||||
"name": { classification: 'private', anonymize_strategy: null },
|
||||
"email": { classification: 'private', anonymize_strategy: null },
|
||||
"email_verified": { classification: 'private', anonymize_strategy: null },
|
||||
"image": { classification: 'private', anonymize_strategy: null },
|
||||
"created_at": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_at": { classification: 'private', anonymize_strategy: null }
|
||||
},
|
||||
"verification": {
|
||||
"id": { classification: 'private', anonymize_strategy: null },
|
||||
"identifier": { classification: 'private', anonymize_strategy: null },
|
||||
"value": { classification: 'private', anonymize_strategy: null },
|
||||
"expires_at": { classification: 'private', anonymize_strategy: null },
|
||||
"created_at": { classification: 'private', anonymize_strategy: null },
|
||||
"updated_at": { classification: 'private', anonymize_strategy: null }
|
||||
}
|
||||
},
|
||||
} as const
|
||||
6
packages/functions/.pikku/db/coercion.gen.ts
Normal file
6
packages/functions/.pikku/db/coercion.gen.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Generated by @pikku/cli — do not edit by hand.
|
||||
// Run `pikku db migrate` to refresh.
|
||||
|
||||
export const coercionMap = {
|
||||
|
||||
} as const
|
||||
377
packages/functions/.pikku/db/pikku-db-schema.gen.json
Normal file
377
packages/functions/.pikku/db/pikku-db-schema.gen.json
Normal file
@@ -0,0 +1,377 @@
|
||||
{
|
||||
"tables": [
|
||||
{
|
||||
"name": "account",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "account_id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "provider_id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false,
|
||||
"foreignKey": {
|
||||
"table": "user",
|
||||
"column": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "id_token",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "access_token_expires_at",
|
||||
"type": "date",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "date",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "scope",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "audit",
|
||||
"columns": [
|
||||
{
|
||||
"name": "audit_id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "occurred_at",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "outcome",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "function_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "wire_type",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "trace_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "transaction_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "query_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "actor_user_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "actor_org_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "tables",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "changed_cols",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "old",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "message_state",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "INTEGER",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_by_user_id",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false,
|
||||
"foreignKey": {
|
||||
"table": "user",
|
||||
"column": "id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "expires_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "ip_address",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "user_agent",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false,
|
||||
"foreignKey": {
|
||||
"table": "user",
|
||||
"column": "id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "email_verified",
|
||||
"type": "INTEGER",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "TEXT",
|
||||
"nullable": true,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "verification",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": true
|
||||
},
|
||||
{
|
||||
"name": "identifier",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "TEXT",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "expires_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"isPrimaryKey": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"enums": []
|
||||
}
|
||||
95
packages/functions/.pikku/db/schema.d.ts
vendored
Normal file
95
packages/functions/.pikku/db/schema.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
// Generated by @pikku/cli — do not edit by hand.
|
||||
// Run `pikku db migrate` to refresh.
|
||||
|
||||
import type { ColumnType } from 'kysely'
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>
|
||||
|
||||
export type Private<T> = T & { readonly __classification__?: 'private' }
|
||||
export type Pii<T> = T & { readonly __classification__?: 'pii' }
|
||||
export type Secret<T> = T & { readonly __classification__?: 'secret' }
|
||||
export type Uuid = string
|
||||
|
||||
export interface Account {
|
||||
id: ColumnType<Private<string>, string, string>
|
||||
accountId: ColumnType<Private<string>, string, string>
|
||||
providerId: ColumnType<Private<string>, string, string>
|
||||
userId: ColumnType<Private<string>, string, string>
|
||||
accessToken: ColumnType<Private<string> | null, string | null, string | null>
|
||||
refreshToken: ColumnType<Private<string> | null, string | null, string | null>
|
||||
idToken: ColumnType<Private<string> | null, string | null, string | null>
|
||||
accessTokenExpiresAt: ColumnType<Private<string> | null, string | null, string | null>
|
||||
refreshTokenExpiresAt: ColumnType<Private<string> | null, string | null, string | null>
|
||||
scope: ColumnType<Private<string> | null, string | null, string | null>
|
||||
password: ColumnType<Private<string> | null, string | null, string | null>
|
||||
createdAt: ColumnType<Private<string>, string, string>
|
||||
updatedAt: ColumnType<Private<string>, string, string>
|
||||
}
|
||||
|
||||
export interface Audit {
|
||||
auditId: ColumnType<Private<string>, string | undefined, string>
|
||||
occurredAt: ColumnType<Private<string>, string | undefined, string>
|
||||
type: ColumnType<Private<string>, string, string>
|
||||
source: ColumnType<Private<string>, string | undefined, string>
|
||||
outcome: ColumnType<Private<string> | null, string | null, string | null>
|
||||
functionId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
wireType: ColumnType<Private<string> | null, string | null, string | null>
|
||||
traceId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
transactionId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
queryId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
actorUserId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
actorOrgId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
tables: ColumnType<Private<string> | null, string | null, string | null>
|
||||
changedCols: ColumnType<Private<string> | null, string | null, string | null>
|
||||
event: ColumnType<Private<string> | null, string | null, string | null>
|
||||
old: ColumnType<Private<string> | null, string | null, string | null>
|
||||
data: ColumnType<Private<string> | null, string | null, string | null>
|
||||
}
|
||||
|
||||
export interface MessageState {
|
||||
id: ColumnType<Private<number>, number | undefined, number>
|
||||
message: ColumnType<Private<string>, string, string>
|
||||
updatedAt: ColumnType<Private<string>, string | undefined, string>
|
||||
updatedByUserId: ColumnType<Private<string> | null, string | null, string | null>
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: ColumnType<Private<string>, string, string>
|
||||
expiresAt: ColumnType<Private<string>, string, string>
|
||||
token: ColumnType<Private<string>, string, string>
|
||||
createdAt: ColumnType<Private<string>, string, string>
|
||||
updatedAt: ColumnType<Private<string>, string, string>
|
||||
ipAddress: ColumnType<Private<string> | null, string | null, string | null>
|
||||
userAgent: ColumnType<Private<string> | null, string | null, string | null>
|
||||
userId: ColumnType<Private<string>, string, string>
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: ColumnType<Private<string>, string, string>
|
||||
name: ColumnType<Private<string>, string, string>
|
||||
email: ColumnType<Private<string>, string, string>
|
||||
emailVerified: ColumnType<Private<number>, number, number>
|
||||
image: ColumnType<Private<string> | null, string | null, string | null>
|
||||
createdAt: ColumnType<Private<string>, string, string>
|
||||
updatedAt: ColumnType<Private<string>, string, string>
|
||||
}
|
||||
|
||||
export interface Verification {
|
||||
id: ColumnType<Private<string>, string, string>
|
||||
identifier: ColumnType<Private<string>, string, string>
|
||||
value: ColumnType<Private<string>, string, string>
|
||||
expiresAt: ColumnType<Private<string>, string, string>
|
||||
createdAt: ColumnType<Private<string>, string, string>
|
||||
updatedAt: ColumnType<Private<string>, string, string>
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
account: Account
|
||||
audit: Audit
|
||||
messageState: MessageState
|
||||
session: Session
|
||||
user: User
|
||||
verification: Verification
|
||||
}
|
||||
163
packages/functions/.pikku/db/zod.gen.ts
Normal file
163
packages/functions/.pikku/db/zod.gen.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// Generated by @pikku/cli — do not edit by hand.
|
||||
// Run `pikku db migrate` to refresh.
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
export const AccountZ = z.object({
|
||||
id: z.string(),
|
||||
accountId: z.string(),
|
||||
providerId: z.string(),
|
||||
userId: z.string(),
|
||||
accessToken: z.string().nullable(),
|
||||
refreshToken: z.string().nullable(),
|
||||
idToken: z.string().nullable(),
|
||||
accessTokenExpiresAt: z.string().nullable(),
|
||||
refreshTokenExpiresAt: z.string().nullable(),
|
||||
scope: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const AccountInsertZ = z.object({
|
||||
id: z.string(),
|
||||
accountId: z.string(),
|
||||
providerId: z.string(),
|
||||
userId: z.string(),
|
||||
accessToken: z.string().nullable(),
|
||||
refreshToken: z.string().nullable(),
|
||||
idToken: z.string().nullable(),
|
||||
accessTokenExpiresAt: z.string().nullable(),
|
||||
refreshTokenExpiresAt: z.string().nullable(),
|
||||
scope: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const AccountPatchZ = AccountZ.partial()
|
||||
|
||||
export const AuditZ = z.object({
|
||||
auditId: z.string(),
|
||||
occurredAt: z.string(),
|
||||
type: z.string(),
|
||||
source: z.string(),
|
||||
outcome: z.string().nullable(),
|
||||
functionId: z.string().nullable(),
|
||||
wireType: z.string().nullable(),
|
||||
traceId: z.string().nullable(),
|
||||
transactionId: z.string().nullable(),
|
||||
queryId: z.string().nullable(),
|
||||
actorUserId: z.string().nullable(),
|
||||
actorOrgId: z.string().nullable(),
|
||||
tables: z.string().nullable(),
|
||||
changedCols: z.string().nullable(),
|
||||
event: z.string().nullable(),
|
||||
old: z.string().nullable(),
|
||||
data: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const AuditInsertZ = z.object({
|
||||
auditId: z.string().optional(),
|
||||
occurredAt: z.string().optional(),
|
||||
type: z.string(),
|
||||
source: z.string().optional(),
|
||||
outcome: z.string().nullable(),
|
||||
functionId: z.string().nullable(),
|
||||
wireType: z.string().nullable(),
|
||||
traceId: z.string().nullable(),
|
||||
transactionId: z.string().nullable(),
|
||||
queryId: z.string().nullable(),
|
||||
actorUserId: z.string().nullable(),
|
||||
actorOrgId: z.string().nullable(),
|
||||
tables: z.string().nullable(),
|
||||
changedCols: z.string().nullable(),
|
||||
event: z.string().nullable(),
|
||||
old: z.string().nullable(),
|
||||
data: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const AuditPatchZ = AuditZ.partial()
|
||||
|
||||
export const MessageStateZ = z.object({
|
||||
id: z.number(),
|
||||
message: z.string(),
|
||||
updatedAt: z.string(),
|
||||
updatedByUserId: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const MessageStateInsertZ = z.object({
|
||||
id: z.number().optional(),
|
||||
message: z.string(),
|
||||
updatedAt: z.string().optional(),
|
||||
updatedByUserId: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const MessageStatePatchZ = MessageStateZ.partial()
|
||||
|
||||
export const SessionZ = z.object({
|
||||
id: z.string(),
|
||||
expiresAt: z.string(),
|
||||
token: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
ipAddress: z.string().nullable(),
|
||||
userAgent: z.string().nullable(),
|
||||
userId: z.string(),
|
||||
})
|
||||
|
||||
export const SessionInsertZ = z.object({
|
||||
id: z.string(),
|
||||
expiresAt: z.string(),
|
||||
token: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
ipAddress: z.string().nullable(),
|
||||
userAgent: z.string().nullable(),
|
||||
userId: z.string(),
|
||||
})
|
||||
|
||||
export const SessionPatchZ = SessionZ.partial()
|
||||
|
||||
export const UserZ = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
emailVerified: z.number(),
|
||||
image: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const UserInsertZ = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
emailVerified: z.number(),
|
||||
image: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const UserPatchZ = UserZ.partial()
|
||||
|
||||
export const VerificationZ = z.object({
|
||||
id: z.string(),
|
||||
identifier: z.string(),
|
||||
value: z.string(),
|
||||
expiresAt: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const VerificationInsertZ = z.object({
|
||||
id: z.string(),
|
||||
identifier: z.string(),
|
||||
value: z.string(),
|
||||
expiresAt: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const VerificationPatchZ = VerificationZ.partial()
|
||||
|
||||
24
packages/functions/.pikku/email/pikku-emails-meta.gen.json
Normal file
24
packages/functions/.pikku/email/pikku-emails-meta.gen.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"src": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/emails",
|
||||
"themeHash": "1f7772ad7fb7b18f109bd67f2459cc5ea173b0f66929474bebc4522a2d24213c",
|
||||
"templates": {
|
||||
"confirm-email": {
|
||||
"variables": [
|
||||
"appName",
|
||||
"confirmUrl",
|
||||
"email"
|
||||
],
|
||||
"hasHtml": true,
|
||||
"hasSubject": true,
|
||||
"hasText": true,
|
||||
"locales": {
|
||||
"en": {
|
||||
"contentHash": "fa3ef0d0ff652703fc0586c5c78b4c4820206a09bd47651c56a9f1c26a5d2cfd",
|
||||
"htmlHash": "c4b9403e096b1f6c439e120f701bf8cbc6bdba9fdb8e4f21fbad524961343a39",
|
||||
"subjectHash": "209d4ac15c8f7e70e14de04c81724beb00dac7f4ea450a6e8ce527faf4a0ad75",
|
||||
"textHash": "4b494839407f2817290b9b08103cd84103e53a6339ff4e4f24e71dd89ebb7d13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
packages/functions/.pikku/email/pikku-emails.gen.ts
Normal file
211
packages/functions/.pikku/email/pikku-emails.gen.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
type EmailPrimitive = string | number | boolean | null | undefined
|
||||
type EmailTemplateValue = EmailPrimitive | Record<string, unknown> | Array<unknown>
|
||||
|
||||
const EMAIL_THEME = {
|
||||
"appName": "Pikku Starter",
|
||||
"fonts": {
|
||||
"body": "Inter, Arial, sans-serif"
|
||||
},
|
||||
"colors": {
|
||||
"canvas": "#0b1020",
|
||||
"surface": "#11182d",
|
||||
"border": "#263252",
|
||||
"text": "#f7f8fb",
|
||||
"muted": "#a8b0c5",
|
||||
"accent": "#7dd3fc",
|
||||
"button": "#f59e0b",
|
||||
"buttonText": "#111827"
|
||||
}
|
||||
} as const
|
||||
const EMAIL_LOCALES = {
|
||||
"en": {
|
||||
"common": {
|
||||
"footer": "If you did not create this account, you can safely ignore this email."
|
||||
},
|
||||
"confirmEmail": {
|
||||
"subject": "Confirm your email for {{appName}}",
|
||||
"preview": "Confirm your email address to finish setting up your account.",
|
||||
"heading": "Confirm your email",
|
||||
"intro": "Thanks for signing up for {{appName}}! Confirm {{email}} to finish setting up your account.",
|
||||
"cta": "Confirm email",
|
||||
"fallback": "If the button does not work, copy and paste this URL into your browser:",
|
||||
"expiry": "This link expires in 24 hours."
|
||||
}
|
||||
}
|
||||
} as const
|
||||
const EMAIL_PARTIALS = {
|
||||
"footer": "<p style=\"margin:32px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;\">\n {{t.common.footer}}\n</p>\n",
|
||||
"layout": "<!doctype html>\n<html lang=\"{{locale}}\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>{{subject}}</title>\n </head>\n <body style=\"margin:0;padding:0;background:{{theme.colors.canvas}};font-family:{{theme.fonts.body}};\">\n <div style=\"max-width:560px;margin:0 auto;padding:32px 16px;\">\n {{content}}\n </div>\n </body>\n</html>\n"
|
||||
} as const
|
||||
const EMAIL_TEMPLATES = {
|
||||
"confirm-email": {
|
||||
"html": "<p style=\"margin:0 0 12px;color:{{theme.colors.accent}};font-size:12px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;\">\n {{appName}}\n</p>\n<h1 style=\"margin:0 0 16px;color:{{theme.colors.text}};font-size:28px;line-height:1.1;\">\n {{t.confirmEmail.heading}}\n</h1>\n<p style=\"margin:0 0 24px;color:{{theme.colors.muted}};font-size:16px;line-height:1.6;\">\n {{t.confirmEmail.intro}}\n</p>\n<p style=\"margin:0 0 24px;\">\n <a\n href=\"{{confirmUrl}}\"\n style=\"display:inline-block;background:{{theme.colors.button}};color:{{theme.colors.buttonText}};text-decoration:none;padding:14px 22px;border-radius:999px;font-weight:700;\"\n >\n {{t.confirmEmail.cta}}\n </a>\n</p>\n<p style=\"margin:0 0 8px;color:{{theme.colors.text}};font-size:14px;line-height:1.6;\">\n {{t.confirmEmail.fallback}}\n</p>\n<p style=\"margin:0;color:{{theme.colors.muted}};font-size:14px;line-height:1.6;word-break:break-all;\">\n {{confirmUrl}}\n</p>\n<p style=\"margin:24px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;\">\n {{t.confirmEmail.expiry}}\n</p>\n{{> footer}}\n",
|
||||
"subject": "{{t.confirmEmail.subject}}\n",
|
||||
"text": "{{t.confirmEmail.heading}}\n\n{{t.confirmEmail.intro}}\n\n{{confirmUrl}}\n\n{{t.confirmEmail.expiry}}\n",
|
||||
"variables": [
|
||||
"appName",
|
||||
"confirmUrl",
|
||||
"email"
|
||||
],
|
||||
"hashes": {
|
||||
"en": {
|
||||
"contentHash": "fa3ef0d0ff652703fc0586c5c78b4c4820206a09bd47651c56a9f1c26a5d2cfd",
|
||||
"htmlHash": "c4b9403e096b1f6c439e120f701bf8cbc6bdba9fdb8e4f21fbad524961343a39",
|
||||
"subjectHash": "209d4ac15c8f7e70e14de04c81724beb00dac7f4ea450a6e8ce527faf4a0ad75",
|
||||
"textHash": "4b494839407f2817290b9b08103cd84103e53a6339ff4e4f24e71dd89ebb7d13"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
export type EmailTemplateName = keyof typeof EMAIL_TEMPLATES
|
||||
export type EmailLocale = keyof typeof EMAIL_LOCALES
|
||||
|
||||
type TemplateVariableMap = {
|
||||
"confirm-email": {
|
||||
"appName"?: EmailTemplateValue
|
||||
"confirmUrl"?: EmailTemplateValue
|
||||
"email"?: EmailTemplateValue
|
||||
}
|
||||
}
|
||||
|
||||
export type EmailTemplateVariables<TName extends EmailTemplateName> =
|
||||
TemplateVariableMap[TName]
|
||||
|
||||
export type RenderEmailInput<TName extends EmailTemplateName> = {
|
||||
name: TName
|
||||
locale?: EmailLocale
|
||||
data: EmailTemplateVariables<TName>
|
||||
}
|
||||
|
||||
export type RenderedEmail<TName extends EmailTemplateName> = {
|
||||
name: TName
|
||||
locale: EmailLocale
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
variables: ReadonlyArray<string>
|
||||
hash: (typeof EMAIL_TEMPLATES)[TName]['hashes'][EmailLocale]['contentHash']
|
||||
}
|
||||
|
||||
function getNestedValue(source: Record<string, unknown>, path: string): string {
|
||||
const segments = path.split('.')
|
||||
let current: unknown = source
|
||||
for (const segment of segments) {
|
||||
if (!current || typeof current !== 'object' || !(segment in current)) {
|
||||
return ''
|
||||
}
|
||||
current = (current as Record<string, unknown>)[segment]
|
||||
}
|
||||
return typeof current === 'string' || typeof current === 'number'
|
||||
? String(current)
|
||||
: ''
|
||||
}
|
||||
|
||||
function applyTemplate(source: string, context: Record<string, unknown>): string {
|
||||
return source.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, rawKey) => {
|
||||
const key = String(rawKey).trim()
|
||||
if (key === 'content') {
|
||||
return typeof context.content === 'string' ? context.content : ''
|
||||
}
|
||||
if (key.startsWith('>')) {
|
||||
return ''
|
||||
}
|
||||
return getNestedValue(context, key)
|
||||
})
|
||||
}
|
||||
|
||||
function renderTemplate(source: string, context: Record<string, unknown>): string {
|
||||
let rendered = source
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const next = applyTemplate(rendered, context)
|
||||
if (next === rendered) break
|
||||
rendered = next
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
function renderPartial(name: string, context: Record<string, unknown>): string {
|
||||
const partial = EMAIL_PARTIALS[name as keyof typeof EMAIL_PARTIALS]
|
||||
return partial ? renderTemplate(partial, context) : ''
|
||||
}
|
||||
|
||||
export const EMAILS = EMAIL_TEMPLATES
|
||||
|
||||
export function renderEmailTemplate<TName extends EmailTemplateName>(
|
||||
input: RenderEmailInput<TName>
|
||||
): RenderedEmail<TName> {
|
||||
const locale = (input.locale ?? 'en') as EmailLocale
|
||||
const template = EMAIL_TEMPLATES[input.name]
|
||||
if (!template) {
|
||||
throw new Error(`Unknown email template: ${String(input.name)}`)
|
||||
}
|
||||
|
||||
const strings = EMAIL_LOCALES[locale]
|
||||
if (!strings) {
|
||||
throw new Error(`Unknown email locale: ${String(locale)}`)
|
||||
}
|
||||
|
||||
const data = (input.data ?? {}) as Record<string, unknown>
|
||||
const appName =
|
||||
(typeof data.appName === 'string' && data.appName) ||
|
||||
getNestedValue(EMAIL_THEME as Record<string, unknown>, 'appName')
|
||||
|
||||
const baseContext = {
|
||||
...data,
|
||||
locale,
|
||||
theme: EMAIL_THEME,
|
||||
t: strings,
|
||||
appName,
|
||||
}
|
||||
|
||||
const subject = renderTemplate(template.subject, baseContext).trim()
|
||||
const htmlWithPartials = template.html.replace(
|
||||
/\{\{\s*>\s*([a-zA-Z0-9-_/.]+)\s*\}\}/g,
|
||||
(_match, partialName) => `@@PARTIAL:${String(partialName).trim()}@@`
|
||||
)
|
||||
|
||||
let htmlBody = renderTemplate(htmlWithPartials, {
|
||||
...baseContext,
|
||||
subject,
|
||||
})
|
||||
|
||||
const partialMatches = [...htmlBody.matchAll(/@@PARTIAL:([^@]+)@@/g)]
|
||||
for (const match of partialMatches) {
|
||||
const rendered = renderPartial(match[1], {
|
||||
...baseContext,
|
||||
subject,
|
||||
})
|
||||
htmlBody = htmlBody.replace(match[0], rendered)
|
||||
}
|
||||
|
||||
const html = EMAIL_PARTIALS.layout
|
||||
? renderTemplate(EMAIL_PARTIALS.layout, {
|
||||
...baseContext,
|
||||
subject,
|
||||
content: htmlBody,
|
||||
})
|
||||
: htmlBody
|
||||
|
||||
const text = template.text
|
||||
? renderTemplate(template.text, {
|
||||
...baseContext,
|
||||
subject,
|
||||
}).trim()
|
||||
: undefined
|
||||
const hash = (template.hashes[locale]?.contentHash ??
|
||||
'') as RenderedEmail<TName>['hash']
|
||||
|
||||
return {
|
||||
name: input.name,
|
||||
locale,
|
||||
subject,
|
||||
html,
|
||||
...(text ? { text } : {}),
|
||||
variables: template.variables,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
754
packages/functions/.pikku/function/pikku-function-types.gen.ts
Normal file
754
packages/functions/.pikku/function/pikku-function-types.gen.ts
Normal file
@@ -0,0 +1,754 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
* Core function, middleware, and permission types for all wirings
|
||||
*/
|
||||
|
||||
import type { CorePikkuMiddleware, CorePermissionGroup, ListInput, ListOutput, PikkuWire, PickRequired } from '@pikku/core'
|
||||
import type { CorePikkuFunctionConfig, CorePikkuAuth, CorePikkuAuthConfig, CorePikkuPermission } from '@pikku/core/function'
|
||||
import { pikkuAuth as pikkuAuthCore } from '@pikku/core/function'
|
||||
import {
|
||||
addTagMiddleware as addTagMiddlewareCore,
|
||||
addGlobalMiddleware as addGlobalMiddlewareCore,
|
||||
addTagPermission as addTagPermissionCore,
|
||||
addGlobalPermission as addGlobalPermissionCore,
|
||||
} from '@pikku/core/middleware'
|
||||
import { pikkuState as __pikkuState, CreateWireServices } from '@pikku/core/internal'
|
||||
import type { NodeType } from '@pikku/core/node'
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
import { CorePikkuFunction, CorePikkuFunctionSessionless } from '@pikku/core/function'
|
||||
|
||||
import type { UserSession } from '../../src/application-types.d.js'
|
||||
import type { SingletonServices } from '../../src/application-types.d.js'
|
||||
import type { Services } from '../../src/application-types.d.js'
|
||||
import type { Config } from '../../src/application-types.d.js'
|
||||
import type { TypedPikkuRPC, FlattenedRPCMap } from '../rpc/pikku-rpc-wirings-map.internal.gen.d.js'
|
||||
import type { RequiredSingletonServices, RequiredWireServices } from '../pikku-services.gen.js'
|
||||
import type { TypedWorkflow } from '../workflow/pikku-workflow-types.gen.js'
|
||||
|
||||
export type { SingletonServices as SingletonServices }
|
||||
export type { Services as Services }
|
||||
export type Session = UserSession
|
||||
|
||||
|
||||
/**
|
||||
* Inline node configuration for function definitions.
|
||||
*/
|
||||
export type NodeConfig = {
|
||||
displayName: string
|
||||
category: string
|
||||
type: NodeType
|
||||
errorOutput?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe API permission definition that integrates with your application's session type.
|
||||
* Use this to define authorization logic for your API endpoints.
|
||||
*
|
||||
* @template In - The input type that the permission check will receive
|
||||
* @template RequiredServices - The services required for this permission check
|
||||
*/
|
||||
export type PikkuPermission<In = unknown, RequiredServices extends Services = Services> = CorePikkuPermission<In, RequiredServices, PikkuWire<In, never, false, Session>>
|
||||
|
||||
/**
|
||||
* Type-safe middleware definition that can access your application's services and session.
|
||||
* Use this to define reusable middleware that can be applied to multiple wirings.
|
||||
*
|
||||
* @template RequiredServices - The services required for this middleware
|
||||
*/
|
||||
export type PikkuMiddleware<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuMiddleware<RequiredServices>
|
||||
|
||||
/**
|
||||
* Configuration object for creating a permission with metadata
|
||||
*/
|
||||
export type PikkuPermissionConfig<In = unknown, RequiredServices extends Services = Services> = {
|
||||
/** The permission function */
|
||||
func: PikkuPermission<In, RequiredServices>
|
||||
/** Optional human-readable name for the permission */
|
||||
name?: string
|
||||
/** Optional description of what the permission checks */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function for creating permissions with tree-shaking support.
|
||||
* Supports both direct function and configuration object syntax.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Direct function syntax
|
||||
* const permission = pikkuPermission(async ({ logger }, data, { session }) => {
|
||||
* const session = await session?.get()
|
||||
* return session?.role === 'admin'
|
||||
* })
|
||||
*
|
||||
* // Configuration object syntax with metadata
|
||||
* const adminPermission = pikkuPermission({
|
||||
* name: 'Admin Permission',
|
||||
* description: 'Checks if user has admin role',
|
||||
* func: async ({ logger }, data, { session }) => {
|
||||
* const session = await session?.get()
|
||||
* return session?.role === 'admin'
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuPermission = <In>(
|
||||
permission: PikkuPermission<In> | PikkuPermissionConfig<In>
|
||||
): PikkuPermission<In> => {
|
||||
return typeof permission === 'function' ? permission : permission.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe auth-only permission that only needs services and session.
|
||||
* Use this for upfront authorization gates (MCP tools, AI agents, workflows)
|
||||
* where request data isn't available yet.
|
||||
*
|
||||
* @template RequiredServices - The services required for this auth check
|
||||
*/
|
||||
export type PikkuAuth<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuAuth<RequiredServices, Session>
|
||||
|
||||
/**
|
||||
* Configuration object for creating an auth permission with metadata
|
||||
*/
|
||||
export type PikkuAuthConfig<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuAuthConfig<RequiredServices, Session>
|
||||
|
||||
/**
|
||||
* Factory function for creating auth-only permissions with tree-shaking support.
|
||||
* Auth permissions only receive services and session (no request data),
|
||||
* making them evaluable before request data is available.
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* const isAuthenticated = pikkuAuth(async ({ logger }, session) => {
|
||||
* return !!session
|
||||
* })
|
||||
*
|
||||
* const isAdmin = pikkuAuth({
|
||||
* name: 'Admin Auth',
|
||||
* description: 'Checks if user is an admin',
|
||||
* func: async ({ logger }, session) => {
|
||||
* return session?.role === 'admin'
|
||||
* }
|
||||
* })
|
||||
* \`\`\`
|
||||
*/
|
||||
export const pikkuAuth = <RequiredServices extends SingletonServices = SingletonServices>(
|
||||
auth: PikkuAuth<RequiredServices> | PikkuAuthConfig<RequiredServices>
|
||||
): PikkuPermission<any, any> => {
|
||||
return pikkuAuthCore(auth as any) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for creating middleware with metadata
|
||||
*/
|
||||
export type PikkuMiddlewareConfig<RequiredServices extends SingletonServices = SingletonServices> = {
|
||||
/** The middleware function */
|
||||
func: PikkuMiddleware<RequiredServices>
|
||||
/** Optional human-readable name for the middleware */
|
||||
name?: string
|
||||
/** Optional description of what the middleware does */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function for creating middleware with tree-shaking support.
|
||||
* Supports both direct function and configuration object syntax.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Direct function syntax
|
||||
* const middleware = pikkuMiddleware(({ logger }, wires, next) => {
|
||||
* logger.info('Middleware executed')
|
||||
* await next()
|
||||
* })
|
||||
*
|
||||
* // Configuration object syntax with metadata
|
||||
* const logMiddleware = pikkuMiddleware({
|
||||
* name: 'Request Logger',
|
||||
* description: 'Logs all incoming requests',
|
||||
* func: async ({ logger }, wires, next) => {
|
||||
* logger.info('Request started')
|
||||
* await next()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuMiddleware = <RequiredServices extends SingletonServices = SingletonServices>(
|
||||
middleware: PikkuMiddleware<RequiredServices> | PikkuMiddlewareConfig<RequiredServices>
|
||||
): PikkuMiddleware<RequiredServices> => {
|
||||
return typeof middleware === 'function' ? middleware : middleware.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function for creating middleware factories
|
||||
* Use this when your middleware needs configuration/input parameters
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const logMiddleware = pikkuMiddlewareFactory<LogOptions>(({
|
||||
* message,
|
||||
* level = 'info'
|
||||
* }) => {
|
||||
* return pikkuMiddleware(async ({ logger }, next) => {
|
||||
* logger[level](message)
|
||||
* await next()
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuMiddlewareFactory = <In = any>(
|
||||
factory: (input: In) => PikkuMiddleware
|
||||
): ((input: In) => PikkuMiddleware) => {
|
||||
return factory
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function for creating permission factories
|
||||
* Use this when your permission needs configuration/input parameters
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const requireRole = pikkuPermissionFactory<{ role: string }>(({
|
||||
* role
|
||||
* }) => {
|
||||
* return pikkuPermission(async ({ logger }, data, { session }) => {
|
||||
* if (!session || session.role !== role) {
|
||||
* logger.warn(`Permission denied: required role '${role}'`)
|
||||
* return false
|
||||
* }
|
||||
* return true
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuPermissionFactory = <In = any>(
|
||||
factory: (input: In) => PikkuPermission<any>
|
||||
): ((input: In) => PikkuPermission<any>) => {
|
||||
return factory
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that generates a human-readable description of a pending approval action.
|
||||
* Used by AI agents to show meaningful approval prompts instead of raw tool arguments.
|
||||
*
|
||||
* @template In - The input type (same as the function it describes)
|
||||
* @template RequiredServices - The services required for this description function
|
||||
*/
|
||||
export type PikkuApprovalDescription<In = unknown, RequiredServices extends Services = Services> = (
|
||||
services: RequiredServices,
|
||||
data: In
|
||||
) => Promise<string>
|
||||
|
||||
/**
|
||||
* Factory function for creating approval description functions with tree-shaking support.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const deleteTodoApproval = pikkuApprovalDescription(
|
||||
* async ({ todoStore }, { id }) => {
|
||||
* const todo = await todoStore.get(id)
|
||||
* return \`Delete todo: "${todo.title}"\`
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export const pikkuApprovalDescription = <In = unknown, RequiredServices extends Services = Services>(
|
||||
fn: PikkuApprovalDescription<In, RequiredServices>
|
||||
): PikkuApprovalDescription<In, RequiredServices> => {
|
||||
return fn
|
||||
}
|
||||
|
||||
/**
|
||||
* A sessionless API function that doesn't require user authentication.
|
||||
* Use this for public endpoints, health checks, or operations that don't need user context.
|
||||
*
|
||||
* @template In - The input type
|
||||
* @template Out - The output type that the function returns
|
||||
* @template RequiredServices - Services required by this function
|
||||
*/
|
||||
export type PikkuFunctionSessionless<
|
||||
In = unknown,
|
||||
Out = never,
|
||||
RequiredWires extends keyof PikkuWire = never,
|
||||
RequiredServices extends Services = Services
|
||||
> = CorePikkuFunctionSessionless<
|
||||
In,
|
||||
Out,
|
||||
RequiredServices,
|
||||
Session,
|
||||
PickRequired<PikkuWire<In, Out, false, Session, TypedPikkuRPC, null, any, TypedWorkflow>, RequiredWires>
|
||||
>
|
||||
|
||||
/**
|
||||
* A session-aware API function that requires user authentication.
|
||||
* Use this for protected endpoints that need access to user session data.
|
||||
*
|
||||
* @template In - The input type
|
||||
* @template Out - The output type that the function returns
|
||||
* @template RequiredServices - Services required by this function
|
||||
*/
|
||||
export type PikkuFunction<
|
||||
In = unknown,
|
||||
Out = never,
|
||||
RequiredWires extends keyof PikkuWire = 'session',
|
||||
RequiredServices extends Services = Services
|
||||
> = CorePikkuFunction<
|
||||
In,
|
||||
Out,
|
||||
RequiredServices,
|
||||
Session,
|
||||
PickRequired<PikkuWire<In, Out, true, Session, TypedPikkuRPC, null, any, TypedWorkflow>, RequiredWires>
|
||||
>
|
||||
|
||||
/**
|
||||
* Helper type to infer the output type from a Standard Schema
|
||||
*/
|
||||
export type InferSchemaOutput<T> = T extends StandardSchemaV1<any, infer Output> ? Output : never
|
||||
|
||||
/**
|
||||
* Configuration object for Pikku functions with optional middleware, permissions, tags, and documentation.
|
||||
* This type wraps CorePikkuFunctionConfig with the user's custom types.
|
||||
*
|
||||
* @template In - The input type
|
||||
* @template Out - The output type
|
||||
* @template PikkuFunc - The function type (can be narrowed to PikkuFunction or PikkuFunctionSessionless)
|
||||
*/
|
||||
export type PikkuFunctionConfig<
|
||||
In = unknown,
|
||||
Out = unknown,
|
||||
RequiredWires extends keyof PikkuWire = never,
|
||||
PikkuFunc extends PikkuFunction<In, Out, RequiredWires> | PikkuFunctionSessionless<In, Out, RequiredWires> = PikkuFunction<In, Out, RequiredWires> | PikkuFunctionSessionless<In, Out, RequiredWires>,
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
> = CorePikkuFunctionConfig<PikkuFunc, PikkuPermission<In>, PikkuMiddleware, InputSchema, OutputSchema>
|
||||
|
||||
/**
|
||||
* Configuration object for Pikku functions with Zod schema validation.
|
||||
* Use this when you want to define input/output schemas using Zod.
|
||||
* Types are automatically inferred from the schemas.
|
||||
*/
|
||||
type SchemaInferred<S, Fallback = unknown> = S extends StandardSchemaV1
|
||||
? InferSchemaOutput<S>
|
||||
: Fallback
|
||||
|
||||
/**
|
||||
* Schema-overload variant for pikkuFunc. Derived from CorePikkuFunctionConfig
|
||||
* so adding a field on the core type automatically propagates here.
|
||||
*/
|
||||
export type PikkuFunctionConfigWithSchema<
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
RequiredWires extends keyof PikkuWire = never
|
||||
> = Omit<
|
||||
CorePikkuFunctionConfig<
|
||||
| PikkuFunction<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
|
||||
| PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
|
||||
>,
|
||||
'func' | 'input' | 'output' | 'permissions' | 'approvalDescription'
|
||||
> & {
|
||||
func:
|
||||
| PikkuFunction<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
|
||||
| PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
|
||||
input?: InputSchema
|
||||
output?: OutputSchema
|
||||
permissions?: InputSchema extends StandardSchemaV1
|
||||
? CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
|
||||
: undefined
|
||||
approvalDescription?: InputSchema extends StandardSchemaV1
|
||||
? PikkuApprovalDescription<InferSchemaOutput<InputSchema>>
|
||||
: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Pikku function that can be either session-aware or sessionless.
|
||||
* This is the main function wrapper for creating API endpoints.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuFunc<Input, Output>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuFunc({ input: z.object(...), output: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for the function (inferred from schema if provided)
|
||||
* @template Out - Output type for the function (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Pattern 1: Using generic types
|
||||
* const createUser = pikkuFunc<{name: string, email: string}, {id: number}>({
|
||||
* func: async ({db}, input) => {
|
||||
* const user = await db.users.create(input)
|
||||
* return { id: user.id }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // Pattern 2: Using Zod schemas (types inferred automatically)
|
||||
* const createUserInput = z.object({ name: z.string(), email: z.string() })
|
||||
* const createUserOutput = z.object({ id: z.number() })
|
||||
*
|
||||
* const createUser = pikkuFunc({
|
||||
* input: createUserInput,
|
||||
* output: createUserOutput,
|
||||
* func: async ({db}, input) => {
|
||||
* // input is typed as { name: string, email: string }
|
||||
* const user = await db.users.create(input)
|
||||
* return { id: user.id } // must match output schema
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function pikkuFunc<
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
>(
|
||||
config: PikkuFunctionConfigWithSchema<InputSchema, OutputSchema, 'session' | 'rpc'>
|
||||
): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'session' | 'rpc'>
|
||||
export function pikkuFunc<In, Out = unknown>(
|
||||
func:
|
||||
| PikkuFunction<In, Out, 'session' | 'rpc'>
|
||||
| PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
|
||||
): PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
|
||||
export function pikkuFunc(func: any) {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
export type PikkuListFunction<
|
||||
F extends Record<string, unknown> = {},
|
||||
Row = unknown,
|
||||
S extends string = never
|
||||
> =
|
||||
| PikkuFunction<ListInput<F, S>, ListOutput<Row>, 'session' | 'rpc'>
|
||||
| PikkuFunctionSessionless<
|
||||
ListInput<F, S>,
|
||||
ListOutput<Row>,
|
||||
'session' | 'rpc'
|
||||
>
|
||||
|
||||
export const pikkuListFunc = <
|
||||
F extends Record<string, unknown> = {},
|
||||
Row = unknown,
|
||||
S extends string = never
|
||||
>(
|
||||
config: PikkuFunctionConfig<
|
||||
ListInput<F, S>,
|
||||
ListOutput<Row>,
|
||||
'session' | 'rpc'
|
||||
>
|
||||
): PikkuFunctionConfig<ListInput<F, S>, ListOutput<Row>, 'session' | 'rpc'> => {
|
||||
return pikkuFunc(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for sessionless Pikku functions with Zod schema validation.
|
||||
*/
|
||||
/**
|
||||
* Schema-overload variant for pikkuSessionlessFunc. Derived from
|
||||
* CorePikkuFunctionConfig to stay in sync with the generic-typed config.
|
||||
*/
|
||||
export type PikkuFunctionSessionlessConfigWithSchema<
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
RequiredWires extends keyof PikkuWire = never
|
||||
> = Omit<
|
||||
CorePikkuFunctionConfig<
|
||||
PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
|
||||
>,
|
||||
'func' | 'input' | 'output' | 'permissions' | 'approvalDescription'
|
||||
> & {
|
||||
func: PikkuFunctionSessionless<
|
||||
SchemaInferred<InputSchema>,
|
||||
SchemaInferred<OutputSchema>,
|
||||
RequiredWires
|
||||
>
|
||||
input?: InputSchema
|
||||
output?: OutputSchema
|
||||
permissions?: InputSchema extends StandardSchemaV1
|
||||
? CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
|
||||
: undefined
|
||||
approvalDescription?: InputSchema extends StandardSchemaV1
|
||||
? PikkuApprovalDescription<InferSchemaOutput<InputSchema>>
|
||||
: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sessionless Pikku function that doesn't require user authentication.
|
||||
* Use this for public endpoints, webhooks, or background tasks.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuSessionlessFunc<Input, Output>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuSessionlessFunc({ input: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for the function (inferred from schema if provided)
|
||||
* @template Out - Output type for the function (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Pattern 1: Using generic types
|
||||
* const healthCheck = pikkuSessionlessFunc<void, {status: string}>({
|
||||
* func: async ({logger}) => {
|
||||
* return { status: 'healthy' }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // Pattern 2: Using Zod schemas
|
||||
* const greetInput = z.object({ name: z.string() })
|
||||
* const greetOutput = z.object({ message: z.string() })
|
||||
*
|
||||
* const greet = pikkuSessionlessFunc({
|
||||
* input: greetInput,
|
||||
* output: greetOutput,
|
||||
* func: async (_services, { name }) => {
|
||||
* return { message: `Hello, ${name}!` }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function pikkuSessionlessFunc<
|
||||
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
||||
OutputSchema extends StandardSchemaV1 | undefined = undefined
|
||||
>(
|
||||
config: PikkuFunctionSessionlessConfigWithSchema<InputSchema, OutputSchema, 'session' | 'rpc'>
|
||||
): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'session' | 'rpc'>
|
||||
export function pikkuSessionlessFunc<In, Out = unknown>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<In, Out, 'session' | 'rpc'>
|
||||
| PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
|
||||
): PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
|
||||
export function pikkuSessionlessFunc(func: any) {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that takes no input and returns no output.
|
||||
* Useful for health checks, triggers, or cleanup operations.
|
||||
*
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The normalized configuration object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cleanupTempFiles = pikkuVoidFunc(async ({fileSystem, logger}) => {
|
||||
* logger.info('Starting cleanup of temporary files')
|
||||
* await fileSystem.deleteDirectory('/tmp/uploads')
|
||||
* logger.info('Cleanup completed')
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuVoidFunc = (
|
||||
func:
|
||||
| PikkuFunctionSessionless<void, void, 'session' | 'rpc'>
|
||||
| PikkuFunctionConfig<void, void, 'session' | 'rpc'>
|
||||
) => {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* References a registered function by name for use in any wiring.
|
||||
* Works for both local and addon functions — resolves via RPC at runtime.
|
||||
*
|
||||
* @template Name - The function name (must be a key in FlattenedRPCMap)
|
||||
* @param rpcName - The name of the function to reference
|
||||
* @returns A Pikku function config that proxies calls via RPC
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Use in agent tools
|
||||
* tools: [ref('todos:listTodos'), ref('myLocalFunc')]
|
||||
*
|
||||
* // Use in HTTP wiring
|
||||
* wireHTTP({ route: '/greet', method: 'post', func: ref('greet') })
|
||||
* ```
|
||||
*/
|
||||
export const ref = <Name extends keyof FlattenedRPCMap>(
|
||||
rpcName: Name
|
||||
): PikkuFunctionConfig<
|
||||
FlattenedRPCMap[Name]['input'],
|
||||
FlattenedRPCMap[Name]['output'],
|
||||
'session' | 'rpc'
|
||||
> => {
|
||||
return {
|
||||
func: async (_services: any, data: FlattenedRPCMap[Name]['input'], { rpc }: any) => {
|
||||
return rpc.invoke(rpcName, data)
|
||||
}
|
||||
} as PikkuFunctionConfig<
|
||||
FlattenedRPCMap[Name]['input'],
|
||||
FlattenedRPCMap[Name]['output'],
|
||||
'session' | 'rpc'
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Pikku config factory.
|
||||
* Use this to define your application's configuration factory.
|
||||
*
|
||||
* @param func - Config factory function that returns your application's config
|
||||
* @returns The config factory function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const createConfig = pikkuConfig(async () => {
|
||||
* return {
|
||||
* apiUrl: process.env.API_URL || 'http://localhost:3000',
|
||||
* dbUrl: process.env.DATABASE_URL
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuConfig = (
|
||||
func: (variables?: any, ...args: any[]) => Promise<Config>
|
||||
) => func
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Pikku singleton services factory.
|
||||
* Use this to define services that are created once and shared across all requests.
|
||||
*
|
||||
* @param func - Singleton services factory function
|
||||
* @returns The singleton services factory function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const createSingletonServices = pikkuServices(async (config, existingServices) => {
|
||||
* return {
|
||||
* config,
|
||||
* logger: new CustomLogger(),
|
||||
* db: await createDatabaseConnection(config.dbUrl)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuServices = (
|
||||
func: (config: Config, existingServices: Partial<SingletonServices>) => Promise<Partial<Omit<RequiredSingletonServices, 'auth'>>>
|
||||
) => {
|
||||
return async (config: Config, existingServices: Partial<SingletonServices> = {}) => {
|
||||
const createdServices = await func(config, existingServices)
|
||||
const services = { ...existingServices, ...createdServices }
|
||||
const authFactory = __pikkuState(null, 'package', 'authFactory')
|
||||
if (authFactory) {
|
||||
let authInstance: Promise<unknown> | undefined
|
||||
;(services as any).auth = () => {
|
||||
authInstance ??= Promise.resolve()
|
||||
.then(() => authFactory(services as any))
|
||||
.catch((error) => {
|
||||
authInstance = undefined
|
||||
throw error
|
||||
})
|
||||
return authInstance
|
||||
}
|
||||
}
|
||||
const resolved = services as RequiredSingletonServices
|
||||
__pikkuState(null, 'package', 'singletonServices', resolved as any)
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Pikku wire services factory.
|
||||
* Use this to define services that are created per-request/session.
|
||||
*
|
||||
* @param func - Wire services factory function
|
||||
* @returns The wire services factory function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const createWireServices = pikkuWireServices(async (services, wire) => {
|
||||
* const session = await wire.session?.get()
|
||||
* return {
|
||||
* userCache: new UserCache(session?.userId)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const pikkuWireServices = (
|
||||
func: (
|
||||
services: SingletonServices,
|
||||
wire: any
|
||||
) => Promise<RequiredWireServices>
|
||||
): CreateWireServices => {
|
||||
const factories = __pikkuState(null, 'package', 'factories')
|
||||
__pikkuState(null, 'package', 'factories', { ...factories, createWireServices: func as any })
|
||||
return func as unknown as CreateWireServices
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag-scoped middleware. Applies to any wiring that carries the matching tag.
|
||||
*
|
||||
* @example
|
||||
* addTagMiddleware('admin', [adminMiddleware])
|
||||
*/
|
||||
export const addTagMiddleware = (tag: string, middleware: PikkuMiddleware[]) => {
|
||||
addTagMiddlewareCore(tag, middleware as any, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire-agnostic global middleware. Runs at the top of every wiring's
|
||||
* middleware chain — before wire-, tag-, and function-level entries.
|
||||
*
|
||||
* Resolution order: global -> wire -> tag -> function.
|
||||
*
|
||||
* @example
|
||||
* addGlobalMiddleware([telemetryMiddleware])
|
||||
*/
|
||||
export const addGlobalMiddleware = (middleware: PikkuMiddleware[]) => {
|
||||
addGlobalMiddlewareCore(middleware as any, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag-scoped permissions. Applies to any wiring that carries the matching tag.
|
||||
*
|
||||
* @example
|
||||
* addTagPermission('admin', [adminPermission])
|
||||
*/
|
||||
export const addTagPermission = <In = unknown>(tag: string, permissions: CorePermissionGroup<PikkuPermission<In>> | PikkuPermission<In>[]) => {
|
||||
addTagPermissionCore(tag, permissions as any, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire-agnostic global permissions. Runs at the top of every wiring's
|
||||
* permission resolution — before wire-, tag-, and function-level entries.
|
||||
*
|
||||
* Resolution order: global -> wire -> tag -> function.
|
||||
*
|
||||
* @example
|
||||
* addGlobalPermission([signedInUser])
|
||||
*/
|
||||
export const addGlobalPermission = <In = unknown>(permissions: CorePermissionGroup<PikkuPermission<In>> | PikkuPermission<In>[]) => {
|
||||
addGlobalPermissionCore(permissions as any, null)
|
||||
}
|
||||
|
||||
export { wireAddon } from '@pikku/core/rpc'
|
||||
export type { WireAddonConfig } from '@pikku/core/rpc'
|
||||
|
||||
|
||||
/**
|
||||
* Addon contract references. Generated from each wired addon's published
|
||||
* contract metadata — no addon source is imported. Functions are proxied via
|
||||
* ref() (RPC) exactly like ref('namespace:fn').
|
||||
*/
|
||||
const __addonHttp = {} as const
|
||||
const __addonChannel = {} as const
|
||||
const __addonCli = {} as const
|
||||
|
||||
export const refHTTP = <Name extends keyof typeof __addonHttp>(
|
||||
name: Name,
|
||||
options?: { basePath?: string }
|
||||
): (typeof __addonHttp)[Name] => {
|
||||
const contract = __addonHttp[name] as any
|
||||
return (
|
||||
options?.basePath !== undefined
|
||||
? { ...contract, basePath: options.basePath }
|
||||
: contract
|
||||
) as (typeof __addonHttp)[Name]
|
||||
}
|
||||
|
||||
export const refChannel = <Name extends keyof typeof __addonChannel>(
|
||||
name: Name
|
||||
): (typeof __addonChannel)[Name] => __addonChannel[name]
|
||||
|
||||
export const refCLI = <Name extends keyof typeof __addonCli>(
|
||||
name: Name
|
||||
): (typeof __addonCli)[Name] => __addonCli[name]
|
||||
@@ -0,0 +1,763 @@
|
||||
{
|
||||
"edgeEcho": {
|
||||
"pikkuFuncId": "edgeEcho",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "edgeEcho",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"inputSchemaName": "EdgeEchoInput",
|
||||
"outputSchemaName": "EdgeEchoOutput",
|
||||
"inputs": [
|
||||
"EdgeEchoInput"
|
||||
],
|
||||
"outputs": [
|
||||
"EdgeEchoOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "e1dd1c485212e3ea",
|
||||
"description": "Echoes a message back from a serverless edge Worker.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/edge-echo.function.ts",
|
||||
"exportedName": "edgeEcho",
|
||||
"contractHash": "91dda37cd8231346",
|
||||
"inputHash": "3bb62634",
|
||||
"outputHash": "10641e1c"
|
||||
},
|
||||
"getMessage": {
|
||||
"pikkuFuncId": "getMessage",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "getMessage",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"kysely"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "GetMessageInput",
|
||||
"outputSchemaName": "GetMessageOutput",
|
||||
"inputs": [
|
||||
"GetMessageInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetMessageOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"implementationHash": "90fc43e8778db110",
|
||||
"description": "Returns the current starter message for the signed-in user.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/get-message.function.ts",
|
||||
"exportedName": "getMessage",
|
||||
"contractHash": "0b953679c3613f47",
|
||||
"inputHash": "cd075fe2",
|
||||
"outputHash": "d188ad31"
|
||||
},
|
||||
"getSession": {
|
||||
"pikkuFuncId": "getSession",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "getSession",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"kysely"
|
||||
]
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"session"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "GetSessionInput",
|
||||
"outputSchemaName": "GetSessionOutput",
|
||||
"inputs": [
|
||||
"GetSessionInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetSessionOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"implementationHash": "2ed4f21ea016c7e3",
|
||||
"description": "Returns the current signed-in user.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/get-session.function.ts",
|
||||
"exportedName": "getSession",
|
||||
"contractHash": "493320ce1e00cb69",
|
||||
"inputHash": "ba4ab2e7",
|
||||
"outputHash": "386fe409"
|
||||
},
|
||||
"serverCounter": {
|
||||
"pikkuFuncId": "serverCounter",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "serverCounter",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"inputSchemaName": "ServerCounterInput",
|
||||
"outputSchemaName": "ServerCounterOutput",
|
||||
"inputs": [
|
||||
"ServerCounterInput"
|
||||
],
|
||||
"outputs": [
|
||||
"ServerCounterOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"deploy": "server",
|
||||
"implementationHash": "8a956f633eb3a4a4",
|
||||
"description": "Increments an in-memory counter held by the container instance.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/server-counter.function.ts",
|
||||
"exportedName": "serverCounter",
|
||||
"contractHash": "a891b2fe0608177d",
|
||||
"inputHash": "d8945f99",
|
||||
"outputHash": "56e3de15"
|
||||
},
|
||||
"serverUptime": {
|
||||
"pikkuFuncId": "serverUptime",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "serverUptime",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"inputSchemaName": "ServerUptimeInput",
|
||||
"outputSchemaName": "ServerUptimeOutput",
|
||||
"inputs": [
|
||||
"ServerUptimeInput"
|
||||
],
|
||||
"outputs": [
|
||||
"ServerUptimeOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"deploy": "server",
|
||||
"implementationHash": "046d656adb63b531",
|
||||
"description": "Reports the serving container process uptime and runtime.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/server-uptime.function.ts",
|
||||
"exportedName": "serverUptime",
|
||||
"contractHash": "d62f18b9b2739cd6",
|
||||
"inputHash": "5425fd1f",
|
||||
"outputHash": "4c1b3905"
|
||||
},
|
||||
"updateMessage": {
|
||||
"pikkuFuncId": "updateMessage",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "updateMessage",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"kysely"
|
||||
]
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"session"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "UpdateMessageInput",
|
||||
"outputSchemaName": "UpdateMessageOutput",
|
||||
"inputs": [
|
||||
"UpdateMessageInput"
|
||||
],
|
||||
"outputs": [
|
||||
"UpdateMessageOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "bfb74857a2ee5d16",
|
||||
"description": "Updates the current starter message.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/functions/update-message.function.ts",
|
||||
"exportedName": "updateMessage",
|
||||
"contractHash": "16e17af7cea46c7a",
|
||||
"inputHash": "77b14595",
|
||||
"outputHash": "bca77943"
|
||||
},
|
||||
"agentCaller": {
|
||||
"pikkuFuncId": "agentCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentCaller",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "AgentCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "4af198bf5bbd9bd8",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "agentCaller",
|
||||
"contractHash": "76619abd99f23189",
|
||||
"inputHash": "4d1eddbc",
|
||||
"outputHash": "9a3af204"
|
||||
},
|
||||
"agentStreamCaller": {
|
||||
"pikkuFuncId": "agentStreamCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentStreamCaller",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "AgentStreamCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentStreamCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "5d475a264b62cc63",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "agentStreamCaller",
|
||||
"contractHash": "440ca8bea8919cc6",
|
||||
"inputHash": "6b434982",
|
||||
"outputHash": "641a263e"
|
||||
},
|
||||
"agentApproveCaller": {
|
||||
"pikkuFuncId": "agentApproveCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentApproveCaller",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "AgentApproveCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentApproveCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "ae5d4bfb0293119a",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "agentApproveCaller",
|
||||
"contractHash": "83a9293c4b51d65c",
|
||||
"inputHash": "f1628b7c",
|
||||
"outputHash": "ef71f08a"
|
||||
},
|
||||
"agentResumeCaller": {
|
||||
"pikkuFuncId": "agentResumeCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentResumeCaller",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "AgentResumeCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentResumeCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "d17ccf1259ba4a60",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "agentResumeCaller",
|
||||
"contractHash": "0648ffb2cfa9d861",
|
||||
"inputHash": "f25b50dd",
|
||||
"outputHash": "ee89e454"
|
||||
},
|
||||
"getAgentThreads": {
|
||||
"pikkuFuncId": "getAgentThreads",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreads",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"agentRunService"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "GetAgentThreadsInput",
|
||||
"outputSchemaName": "GetAgentThreadsOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadsInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadsOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "3823f8f3911c48e2",
|
||||
"title": "Get Agent Threads",
|
||||
"tags": [
|
||||
"pikku",
|
||||
"pikku:agent"
|
||||
],
|
||||
"description": "Returns a list of AI agent threads from storage. Accepts optional filters: agentName, resourceId, limit, and offset for pagination.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "getAgentThreads",
|
||||
"contractHash": "d629208347702d27",
|
||||
"inputHash": "41f5350c",
|
||||
"outputHash": "6598bdff"
|
||||
},
|
||||
"getAgentThreadMessages": {
|
||||
"pikkuFuncId": "getAgentThreadMessages",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreadMessages",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"agentRunService"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "GetAgentThreadMessagesInput",
|
||||
"outputSchemaName": "GetAgentThreadMessagesOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadMessagesInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadMessagesOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "dffe581287205c6e",
|
||||
"title": "Get Agent Thread Messages",
|
||||
"tags": [
|
||||
"pikku",
|
||||
"pikku:agent"
|
||||
],
|
||||
"description": "Returns all messages for a given AI agent thread, ordered by creation time.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "getAgentThreadMessages",
|
||||
"contractHash": "44e43e96024680f3",
|
||||
"inputHash": "a158e652",
|
||||
"outputHash": "04960780"
|
||||
},
|
||||
"getAgentThreadRuns": {
|
||||
"pikkuFuncId": "getAgentThreadRuns",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreadRuns",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"agentRunService"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "GetAgentThreadRunsInput",
|
||||
"outputSchemaName": "GetAgentThreadRunsOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadRunsInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadRunsOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "2e83ec1d0e08772d",
|
||||
"title": "Get Agent Thread Runs",
|
||||
"tags": [
|
||||
"pikku",
|
||||
"pikku:agent"
|
||||
],
|
||||
"description": "Returns the run history for a given AI agent thread, ordered by creation time.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "getAgentThreadRuns",
|
||||
"contractHash": "c51892c37c245537",
|
||||
"inputHash": "fb544997",
|
||||
"outputHash": "6352aca1"
|
||||
},
|
||||
"deleteAgentThread": {
|
||||
"pikkuFuncId": "deleteAgentThread",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "deleteAgentThread",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"agentRunService"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "DeleteAgentThreadInput",
|
||||
"outputSchemaName": "DeleteAgentThreadOutput",
|
||||
"inputs": [
|
||||
"DeleteAgentThreadInput"
|
||||
],
|
||||
"outputs": [
|
||||
"DeleteAgentThreadOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "7c0d0fb40a42fc20",
|
||||
"title": "Delete Agent Thread",
|
||||
"tags": [
|
||||
"pikku",
|
||||
"pikku:agent"
|
||||
],
|
||||
"description": "Deletes an AI agent thread and all of its persisted state.",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"exportedName": "deleteAgentThread",
|
||||
"contractHash": "b970011db1e0230e",
|
||||
"inputHash": "3ed36ee5",
|
||||
"outputHash": "a2094cc1"
|
||||
},
|
||||
"authHandler": {
|
||||
"pikkuFuncId": "authHandler",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "authHandler",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"kysely",
|
||||
"secrets"
|
||||
]
|
||||
},
|
||||
"wires": {
|
||||
"optimized": false,
|
||||
"wires": []
|
||||
},
|
||||
"inputSchemaName": null,
|
||||
"outputSchemaName": "AuthHandlerOutput",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
"AuthHandlerOutput"
|
||||
],
|
||||
"implementationHash": "7b1988fee5ec048e",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth.gen.ts",
|
||||
"exportedName": "authHandler",
|
||||
"contractHash": "56ca81afb5965d88",
|
||||
"inputHash": "3db46710",
|
||||
"outputHash": "4a0c6eec"
|
||||
},
|
||||
"pikkuConsoleSetSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleSetSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleSetSecret",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"secrets"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "PikkuConsoleSetSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleSetSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleSetSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleSetSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "403f267c2ff0edad",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"description": "Set the value of a secret",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"exportedName": "pikkuConsoleSetSecret",
|
||||
"contractHash": "fcb625d86fe88b8f",
|
||||
"inputHash": "a0c8a043",
|
||||
"outputHash": "57702b41"
|
||||
},
|
||||
"pikkuConsoleGetVariable": {
|
||||
"pikkuFuncId": "pikkuConsoleGetVariable",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleGetVariable",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"variables"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "PikkuConsoleGetVariableInput",
|
||||
"outputSchemaName": "PikkuConsoleGetVariableOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleGetVariableInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleGetVariableOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "a2637db8df0daa91",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"description": "Get the current value of a variable",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"exportedName": "pikkuConsoleGetVariable",
|
||||
"contractHash": "3b66dda4d82fa48a",
|
||||
"inputHash": "ce4b2e06",
|
||||
"outputHash": "f54f1059"
|
||||
},
|
||||
"pikkuConsoleSetVariable": {
|
||||
"pikkuFuncId": "pikkuConsoleSetVariable",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleSetVariable",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"variables"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "PikkuConsoleSetVariableInput",
|
||||
"outputSchemaName": "PikkuConsoleSetVariableOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleSetVariableInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleSetVariableOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "4fac80de849be6c9",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"description": "Set the value of a variable",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"exportedName": "pikkuConsoleSetVariable",
|
||||
"contractHash": "9dda40633b173a08",
|
||||
"inputHash": "d91a4af5",
|
||||
"outputHash": "91fe907b"
|
||||
},
|
||||
"pikkuConsoleHasSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleHasSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleHasSecret",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"secrets"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "PikkuConsoleHasSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleHasSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleHasSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleHasSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "b8c818fb2291b032",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"description": "Check if a secret exists without reading its value",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"exportedName": "pikkuConsoleHasSecret",
|
||||
"contractHash": "eeff751c00461679",
|
||||
"inputHash": "a67adc7c",
|
||||
"outputHash": "1da66eac"
|
||||
},
|
||||
"pikkuConsoleGetSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleGetSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleGetSecret",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": [
|
||||
"secrets"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "PikkuConsoleGetSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleGetSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleGetSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleGetSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "6808aead5cd1896a",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"description": "Get the current value of a secret",
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"exportedName": "pikkuConsoleGetSecret",
|
||||
"contractHash": "d651ad93642eaeed",
|
||||
"inputHash": "329251da",
|
||||
"outputHash": "fb675a26"
|
||||
},
|
||||
"http:get:/workflow-run/:runId/stream": {
|
||||
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
|
||||
"functionType": "inline",
|
||||
"name": "/workflow-run/:runId/stream",
|
||||
"services": {
|
||||
"optimized": false,
|
||||
"services": []
|
||||
},
|
||||
"inputSchemaName": "StreamWorkflowRunInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"StreamWorkflowRunInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"sessionless": true,
|
||||
"contractHash": "b693c43c3c0f10d5",
|
||||
"inputHash": "e0a18a17",
|
||||
"outputHash": "fc2fd4a0"
|
||||
},
|
||||
"http:get:/function-tests/stream": {
|
||||
"pikkuFuncId": "http:get:/function-tests/stream",
|
||||
"functionType": "inline",
|
||||
"name": "/function-tests/stream",
|
||||
"services": {
|
||||
"optimized": false,
|
||||
"services": []
|
||||
},
|
||||
"inputSchemaName": null,
|
||||
"outputSchemaName": "StreamFunctionTestsOutput",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
"StreamFunctionTestsOutput"
|
||||
],
|
||||
"sessionless": true,
|
||||
"contractHash": "de6cfac52a493763",
|
||||
"inputHash": "748d80cf",
|
||||
"outputHash": "2090bce5"
|
||||
},
|
||||
"rpcCaller": {
|
||||
"pikkuFuncId": "rpcCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "rpcCaller",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "RpcCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"RpcCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "f92b60dda357f483",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/rpc-public.gen.ts",
|
||||
"exportedName": "rpcCaller",
|
||||
"contractHash": "f6901fd233740bf4",
|
||||
"inputHash": "a69cacc4",
|
||||
"outputHash": "629e0b5a"
|
||||
},
|
||||
"remoteRPCHandler": {
|
||||
"pikkuFuncId": "remoteRPCHandler",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "remoteRPCHandler",
|
||||
"services": {
|
||||
"optimized": true,
|
||||
"services": []
|
||||
},
|
||||
"wires": {
|
||||
"optimized": true,
|
||||
"wires": [
|
||||
"rpc"
|
||||
]
|
||||
},
|
||||
"inputSchemaName": "RemoteRPCHandlerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"RemoteRPCHandlerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"remote": true,
|
||||
"implementationHash": "105018a823bdb551",
|
||||
"tags": [
|
||||
"pikku"
|
||||
],
|
||||
"isDirectFunction": false,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/rpc-remote.gen.ts",
|
||||
"exportedName": "remoteRPCHandler"
|
||||
}
|
||||
}
|
||||
454
packages/functions/.pikku/function/pikku-functions-meta.gen.json
Normal file
454
packages/functions/.pikku/function/pikku-functions-meta.gen.json
Normal file
@@ -0,0 +1,454 @@
|
||||
{
|
||||
"edgeEcho": {
|
||||
"pikkuFuncId": "edgeEcho",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "edgeEcho",
|
||||
"inputSchemaName": "EdgeEchoInput",
|
||||
"outputSchemaName": "EdgeEchoOutput",
|
||||
"inputs": [
|
||||
"EdgeEchoInput"
|
||||
],
|
||||
"outputs": [
|
||||
"EdgeEchoOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "e1dd1c485212e3ea",
|
||||
"contractHash": "91dda37cd8231346",
|
||||
"inputHash": "3bb62634",
|
||||
"outputHash": "10641e1c"
|
||||
},
|
||||
"getMessage": {
|
||||
"pikkuFuncId": "getMessage",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "getMessage",
|
||||
"inputSchemaName": "GetMessageInput",
|
||||
"outputSchemaName": "GetMessageOutput",
|
||||
"inputs": [
|
||||
"GetMessageInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetMessageOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"implementationHash": "90fc43e8778db110",
|
||||
"contractHash": "0b953679c3613f47",
|
||||
"inputHash": "cd075fe2",
|
||||
"outputHash": "d188ad31"
|
||||
},
|
||||
"getSession": {
|
||||
"pikkuFuncId": "getSession",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "getSession",
|
||||
"inputSchemaName": "GetSessionInput",
|
||||
"outputSchemaName": "GetSessionOutput",
|
||||
"inputs": [
|
||||
"GetSessionInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetSessionOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"implementationHash": "2ed4f21ea016c7e3",
|
||||
"contractHash": "493320ce1e00cb69",
|
||||
"inputHash": "ba4ab2e7",
|
||||
"outputHash": "386fe409"
|
||||
},
|
||||
"serverCounter": {
|
||||
"pikkuFuncId": "serverCounter",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "serverCounter",
|
||||
"inputSchemaName": "ServerCounterInput",
|
||||
"outputSchemaName": "ServerCounterOutput",
|
||||
"inputs": [
|
||||
"ServerCounterInput"
|
||||
],
|
||||
"outputs": [
|
||||
"ServerCounterOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"deploy": "server",
|
||||
"implementationHash": "8a956f633eb3a4a4",
|
||||
"contractHash": "a891b2fe0608177d",
|
||||
"inputHash": "d8945f99",
|
||||
"outputHash": "56e3de15"
|
||||
},
|
||||
"serverUptime": {
|
||||
"pikkuFuncId": "serverUptime",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "serverUptime",
|
||||
"inputSchemaName": "ServerUptimeInput",
|
||||
"outputSchemaName": "ServerUptimeOutput",
|
||||
"inputs": [
|
||||
"ServerUptimeInput"
|
||||
],
|
||||
"outputs": [
|
||||
"ServerUptimeOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"readonly": true,
|
||||
"deploy": "server",
|
||||
"implementationHash": "046d656adb63b531",
|
||||
"contractHash": "d62f18b9b2739cd6",
|
||||
"inputHash": "5425fd1f",
|
||||
"outputHash": "4c1b3905"
|
||||
},
|
||||
"updateMessage": {
|
||||
"pikkuFuncId": "updateMessage",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuFunc",
|
||||
"sessionless": false,
|
||||
"name": "updateMessage",
|
||||
"inputSchemaName": "UpdateMessageInput",
|
||||
"outputSchemaName": "UpdateMessageOutput",
|
||||
"inputs": [
|
||||
"UpdateMessageInput"
|
||||
],
|
||||
"outputs": [
|
||||
"UpdateMessageOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "bfb74857a2ee5d16",
|
||||
"contractHash": "16e17af7cea46c7a",
|
||||
"inputHash": "77b14595",
|
||||
"outputHash": "bca77943"
|
||||
},
|
||||
"agentCaller": {
|
||||
"pikkuFuncId": "agentCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentCaller",
|
||||
"inputSchemaName": "AgentCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "4af198bf5bbd9bd8",
|
||||
"contractHash": "76619abd99f23189",
|
||||
"inputHash": "4d1eddbc",
|
||||
"outputHash": "9a3af204"
|
||||
},
|
||||
"agentStreamCaller": {
|
||||
"pikkuFuncId": "agentStreamCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentStreamCaller",
|
||||
"inputSchemaName": "AgentStreamCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentStreamCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "5d475a264b62cc63",
|
||||
"contractHash": "440ca8bea8919cc6",
|
||||
"inputHash": "6b434982",
|
||||
"outputHash": "641a263e"
|
||||
},
|
||||
"agentApproveCaller": {
|
||||
"pikkuFuncId": "agentApproveCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentApproveCaller",
|
||||
"inputSchemaName": "AgentApproveCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentApproveCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "ae5d4bfb0293119a",
|
||||
"contractHash": "83a9293c4b51d65c",
|
||||
"inputHash": "f1628b7c",
|
||||
"outputHash": "ef71f08a"
|
||||
},
|
||||
"agentResumeCaller": {
|
||||
"pikkuFuncId": "agentResumeCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "agentResumeCaller",
|
||||
"inputSchemaName": "AgentResumeCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"AgentResumeCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "d17ccf1259ba4a60",
|
||||
"contractHash": "0648ffb2cfa9d861",
|
||||
"inputHash": "f25b50dd",
|
||||
"outputHash": "ee89e454"
|
||||
},
|
||||
"getAgentThreads": {
|
||||
"pikkuFuncId": "getAgentThreads",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreads",
|
||||
"inputSchemaName": "GetAgentThreadsInput",
|
||||
"outputSchemaName": "GetAgentThreadsOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadsInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadsOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "3823f8f3911c48e2",
|
||||
"contractHash": "d629208347702d27",
|
||||
"inputHash": "41f5350c",
|
||||
"outputHash": "6598bdff"
|
||||
},
|
||||
"getAgentThreadMessages": {
|
||||
"pikkuFuncId": "getAgentThreadMessages",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreadMessages",
|
||||
"inputSchemaName": "GetAgentThreadMessagesInput",
|
||||
"outputSchemaName": "GetAgentThreadMessagesOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadMessagesInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadMessagesOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "dffe581287205c6e",
|
||||
"contractHash": "44e43e96024680f3",
|
||||
"inputHash": "a158e652",
|
||||
"outputHash": "04960780"
|
||||
},
|
||||
"getAgentThreadRuns": {
|
||||
"pikkuFuncId": "getAgentThreadRuns",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "getAgentThreadRuns",
|
||||
"inputSchemaName": "GetAgentThreadRunsInput",
|
||||
"outputSchemaName": "GetAgentThreadRunsOutput",
|
||||
"inputs": [
|
||||
"GetAgentThreadRunsInput"
|
||||
],
|
||||
"outputs": [
|
||||
"GetAgentThreadRunsOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "2e83ec1d0e08772d",
|
||||
"contractHash": "c51892c37c245537",
|
||||
"inputHash": "fb544997",
|
||||
"outputHash": "6352aca1"
|
||||
},
|
||||
"deleteAgentThread": {
|
||||
"pikkuFuncId": "deleteAgentThread",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "deleteAgentThread",
|
||||
"inputSchemaName": "DeleteAgentThreadInput",
|
||||
"outputSchemaName": "DeleteAgentThreadOutput",
|
||||
"inputs": [
|
||||
"DeleteAgentThreadInput"
|
||||
],
|
||||
"outputs": [
|
||||
"DeleteAgentThreadOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "7c0d0fb40a42fc20",
|
||||
"contractHash": "b970011db1e0230e",
|
||||
"inputHash": "3ed36ee5",
|
||||
"outputHash": "a2094cc1"
|
||||
},
|
||||
"authHandler": {
|
||||
"pikkuFuncId": "authHandler",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "authHandler",
|
||||
"inputSchemaName": null,
|
||||
"outputSchemaName": "AuthHandlerOutput",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
"AuthHandlerOutput"
|
||||
],
|
||||
"implementationHash": "7b1988fee5ec048e",
|
||||
"contractHash": "56ca81afb5965d88",
|
||||
"inputHash": "3db46710",
|
||||
"outputHash": "4a0c6eec"
|
||||
},
|
||||
"pikkuConsoleSetSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleSetSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleSetSecret",
|
||||
"inputSchemaName": "PikkuConsoleSetSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleSetSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleSetSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleSetSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "403f267c2ff0edad",
|
||||
"contractHash": "fcb625d86fe88b8f",
|
||||
"inputHash": "a0c8a043",
|
||||
"outputHash": "57702b41"
|
||||
},
|
||||
"pikkuConsoleGetVariable": {
|
||||
"pikkuFuncId": "pikkuConsoleGetVariable",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleGetVariable",
|
||||
"inputSchemaName": "PikkuConsoleGetVariableInput",
|
||||
"outputSchemaName": "PikkuConsoleGetVariableOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleGetVariableInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleGetVariableOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "a2637db8df0daa91",
|
||||
"contractHash": "3b66dda4d82fa48a",
|
||||
"inputHash": "ce4b2e06",
|
||||
"outputHash": "f54f1059"
|
||||
},
|
||||
"pikkuConsoleSetVariable": {
|
||||
"pikkuFuncId": "pikkuConsoleSetVariable",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleSetVariable",
|
||||
"inputSchemaName": "PikkuConsoleSetVariableInput",
|
||||
"outputSchemaName": "PikkuConsoleSetVariableOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleSetVariableInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleSetVariableOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "4fac80de849be6c9",
|
||||
"contractHash": "9dda40633b173a08",
|
||||
"inputHash": "d91a4af5",
|
||||
"outputHash": "91fe907b"
|
||||
},
|
||||
"pikkuConsoleHasSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleHasSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleHasSecret",
|
||||
"inputSchemaName": "PikkuConsoleHasSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleHasSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleHasSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleHasSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "b8c818fb2291b032",
|
||||
"contractHash": "eeff751c00461679",
|
||||
"inputHash": "a67adc7c",
|
||||
"outputHash": "1da66eac"
|
||||
},
|
||||
"pikkuConsoleGetSecret": {
|
||||
"pikkuFuncId": "pikkuConsoleGetSecret",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "pikkuConsoleGetSecret",
|
||||
"inputSchemaName": "PikkuConsoleGetSecretInput",
|
||||
"outputSchemaName": "PikkuConsoleGetSecretOutput",
|
||||
"inputs": [
|
||||
"PikkuConsoleGetSecretInput"
|
||||
],
|
||||
"outputs": [
|
||||
"PikkuConsoleGetSecretOutput"
|
||||
],
|
||||
"expose": true,
|
||||
"implementationHash": "6808aead5cd1896a",
|
||||
"contractHash": "d651ad93642eaeed",
|
||||
"inputHash": "329251da",
|
||||
"outputHash": "fb675a26"
|
||||
},
|
||||
"http:get:/workflow-run/:runId/stream": {
|
||||
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
|
||||
"functionType": "inline",
|
||||
"name": "/workflow-run/:runId/stream",
|
||||
"inputSchemaName": "StreamWorkflowRunInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"StreamWorkflowRunInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"sessionless": true,
|
||||
"contractHash": "b693c43c3c0f10d5",
|
||||
"inputHash": "e0a18a17",
|
||||
"outputHash": "fc2fd4a0"
|
||||
},
|
||||
"http:get:/function-tests/stream": {
|
||||
"pikkuFuncId": "http:get:/function-tests/stream",
|
||||
"functionType": "inline",
|
||||
"name": "/function-tests/stream",
|
||||
"inputSchemaName": null,
|
||||
"outputSchemaName": "StreamFunctionTestsOutput",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
"StreamFunctionTestsOutput"
|
||||
],
|
||||
"sessionless": true,
|
||||
"contractHash": "de6cfac52a493763",
|
||||
"inputHash": "748d80cf",
|
||||
"outputHash": "2090bce5"
|
||||
},
|
||||
"rpcCaller": {
|
||||
"pikkuFuncId": "rpcCaller",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "rpcCaller",
|
||||
"inputSchemaName": "RpcCallerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"RpcCallerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"implementationHash": "f92b60dda357f483",
|
||||
"contractHash": "f6901fd233740bf4",
|
||||
"inputHash": "a69cacc4",
|
||||
"outputHash": "629e0b5a"
|
||||
},
|
||||
"remoteRPCHandler": {
|
||||
"pikkuFuncId": "remoteRPCHandler",
|
||||
"functionType": "user",
|
||||
"funcWrapper": "pikkuSessionlessFunc",
|
||||
"sessionless": true,
|
||||
"name": "remoteRPCHandler",
|
||||
"inputSchemaName": "RemoteRPCHandlerInput",
|
||||
"outputSchemaName": null,
|
||||
"inputs": [
|
||||
"RemoteRPCHandlerInput"
|
||||
],
|
||||
"outputs": [],
|
||||
"remote": true,
|
||||
"implementationHash": "105018a823bdb551"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import { pikkuState } from '@pikku/core/internal'
|
||||
import type { FunctionsMeta } from '@pikku/core'
|
||||
import metaData from './pikku-functions-meta.gen.json' with { type: 'json' }
|
||||
pikkuState(null, 'function', 'meta', metaData as FunctionsMeta)
|
||||
38
packages/functions/.pikku/function/pikku-functions.gen.ts
Normal file
38
packages/functions/.pikku/function/pikku-functions.gen.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/* Import and register functions used by RPCs */
|
||||
import { addFunction } from '@pikku/core/function'
|
||||
import { deleteAgentThread } from '../../src/scaffold/agent.gen.js'
|
||||
import { edgeEcho } from '../../src/functions/edge-echo.function.js'
|
||||
import { getAgentThreadMessages } from '../../src/scaffold/agent.gen.js'
|
||||
import { getAgentThreadRuns } from '../../src/scaffold/agent.gen.js'
|
||||
import { getAgentThreads } from '../../src/scaffold/agent.gen.js'
|
||||
import { getMessage } from '../../src/functions/get-message.function.js'
|
||||
import { getSession } from '../../src/functions/get-session.function.js'
|
||||
import { pikkuConsoleGetSecret } from '../../src/scaffold/console.gen.js'
|
||||
import { pikkuConsoleGetVariable } from '../../src/scaffold/console.gen.js'
|
||||
import { pikkuConsoleHasSecret } from '../../src/scaffold/console.gen.js'
|
||||
import { pikkuConsoleSetSecret } from '../../src/scaffold/console.gen.js'
|
||||
import { pikkuConsoleSetVariable } from '../../src/scaffold/console.gen.js'
|
||||
import { remoteRPCHandler } from '../../src/scaffold/rpc-remote.gen.js'
|
||||
import { serverCounter } from '../../src/functions/server-counter.function.js'
|
||||
import { serverUptime } from '../../src/functions/server-uptime.function.js'
|
||||
import { updateMessage } from '../../src/functions/update-message.function.js'
|
||||
|
||||
addFunction('deleteAgentThread', deleteAgentThread)
|
||||
addFunction('edgeEcho', edgeEcho)
|
||||
addFunction('getAgentThreadMessages', getAgentThreadMessages)
|
||||
addFunction('getAgentThreadRuns', getAgentThreadRuns)
|
||||
addFunction('getAgentThreads', getAgentThreads)
|
||||
addFunction('getMessage', getMessage)
|
||||
addFunction('getSession', getSession)
|
||||
addFunction('pikkuConsoleGetSecret', pikkuConsoleGetSecret)
|
||||
addFunction('pikkuConsoleGetVariable', pikkuConsoleGetVariable)
|
||||
addFunction('pikkuConsoleHasSecret', pikkuConsoleHasSecret)
|
||||
addFunction('pikkuConsoleSetSecret', pikkuConsoleSetSecret)
|
||||
addFunction('pikkuConsoleSetVariable', pikkuConsoleSetVariable)
|
||||
addFunction('remoteRPCHandler', remoteRPCHandler)
|
||||
addFunction('serverCounter', serverCounter)
|
||||
addFunction('serverUptime', serverUptime)
|
||||
addFunction('updateMessage', updateMessage)
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"agentRoutes": {
|
||||
"basePath": "",
|
||||
"tags": [
|
||||
"pikku:public"
|
||||
],
|
||||
"auth": false,
|
||||
"routes": {
|
||||
"agentRun": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "post",
|
||||
"route": "/rpc/agent/:agentName",
|
||||
"sse": null,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "agentCaller"
|
||||
}
|
||||
},
|
||||
"agentStream": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "post",
|
||||
"route": "/rpc/agent/:agentName/stream",
|
||||
"sse": true,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "agentStreamCaller"
|
||||
}
|
||||
},
|
||||
"agentApprove": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "post",
|
||||
"route": "/rpc/agent/:agentName/approve",
|
||||
"sse": null,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "agentApproveCaller"
|
||||
}
|
||||
},
|
||||
"agentResume": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "post",
|
||||
"route": "/rpc/agent/:agentName/resume",
|
||||
"sse": true,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "agentResumeCaller"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"consoleRoutes": {
|
||||
"basePath": "",
|
||||
"tags": [],
|
||||
"auth": false,
|
||||
"routes": {
|
||||
"workflowRunStream": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "get",
|
||||
"route": "/workflow-run/:runId/stream",
|
||||
"sse": true,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "console:streamWorkflowRun",
|
||||
"packageName": "@pikku/addon-console"
|
||||
}
|
||||
},
|
||||
"functionTestsStream": {
|
||||
"auth": null,
|
||||
"contentType": null,
|
||||
"method": "get",
|
||||
"route": "/function-tests/stream",
|
||||
"sse": true,
|
||||
"timeout": null,
|
||||
"func": {
|
||||
"pikkuFuncId": "console:streamFunctionTests",
|
||||
"packageName": "@pikku/addon-console"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import contractsMeta from './pikku-http-contracts-meta.gen.json' with { type: 'json' }
|
||||
export default contractsMeta
|
||||
147
packages/functions/.pikku/http/pikku-http-types.gen.ts
Normal file
147
packages/functions/.pikku/http/pikku-http-types.gen.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
* HTTP-specific type definitions for tree-shaking optimization
|
||||
*/
|
||||
|
||||
import { AssertHTTPWiringParams, wireHTTP as wireHTTPCore, addHTTPMiddleware as addHTTPMiddlewareCore, addHTTPPermission as addHTTPPermissionCore, wireHTTPRoutes as wireHTTPRoutesCore, defineHTTPRoutes as defineHTTPRoutesCore } from '@pikku/core/http'
|
||||
import type { PikkuFunction, PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware, PikkuFunctionConfig } from '../function/pikku-function-types.gen.js'
|
||||
import type { CoreHTTPFunctionWiring, HTTPMethod, HTTPRouteBaseConfig } from '@pikku/core/http'
|
||||
|
||||
/**
|
||||
* Type definition for HTTP API wirings with type-safe path parameters.
|
||||
* Supports both authenticated and unauthenticated functions.
|
||||
*
|
||||
* @template In - Input type for the HTTP wiring
|
||||
* @template Out - Output type for the HTTP wiring
|
||||
* @template Route - String literal type for the HTTP path (e.g., "/users/:id")
|
||||
*/
|
||||
type HTTPWiring<In, Out, Route extends string> = CoreHTTPFunctionWiring<In, Out, Route, PikkuFunction<In, Out, 'rpc' | 'session'>, PikkuFunctionSessionless<In, Out, 'rpc' | 'session'>, PikkuPermission<In>, PikkuMiddleware>
|
||||
|
||||
/**
|
||||
* Registers an HTTP wiring with the Pikku framework.
|
||||
*
|
||||
* @template In - Input type for the HTTP wiring
|
||||
* @template Out - Output type for the HTTP wiring
|
||||
* @template Route - String literal type for the HTTP path (e.g., "/users/:id")
|
||||
* @param httpWiring - HTTP wiring definition with handler, method, and optional middleware
|
||||
*/
|
||||
export const wireHTTP = <In, Out, Route extends string>(
|
||||
httpWiring: HTTPWiring<In, Out, Route> & AssertHTTPWiringParams<In, Route>
|
||||
) => {
|
||||
wireHTTPCore(httpWiring as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers HTTP middleware either globally or for a specific route pattern.
|
||||
*
|
||||
* When a string route pattern is provided along with middleware, the middleware
|
||||
* is applied only to that route. Otherwise, if an array is provided, it is treated
|
||||
* as global middleware (applied to all routes).
|
||||
*
|
||||
* @param routeOrMiddleware - Either a global middleware array or a route pattern string
|
||||
* @param middleware - The middleware array to apply when a route pattern is specified
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add global HTTP middleware
|
||||
* addHTTPMiddleware([authMiddleware, loggingMiddleware])
|
||||
*
|
||||
* // Add route-specific middleware
|
||||
* addHTTPMiddleware('/api/admin/*', [adminAuthMiddleware])
|
||||
* ```
|
||||
*/
|
||||
export const addHTTPMiddleware = (
|
||||
routeOrMiddleware: PikkuMiddleware[] | string,
|
||||
middleware?: PikkuMiddleware[]
|
||||
) => {
|
||||
addHTTPMiddlewareCore(routeOrMiddleware as any, middleware as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers HTTP permissions either globally or for a specific route pattern.
|
||||
*
|
||||
* When a string route pattern is provided along with permissions, the permissions
|
||||
* are applied only to that route. Permissions can be passed as an array or as a
|
||||
* permission group object.
|
||||
*
|
||||
* @param pattern - Route pattern string (e.g., '*' for all routes, '/api/*' for specific routes)
|
||||
* @param permissions - The permissions to apply for the specified route pattern
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add global HTTP permissions
|
||||
* addHTTPPermission('*', { global: globalPermission })
|
||||
*
|
||||
* // Add route-specific permissions
|
||||
* addHTTPPermission('/api/admin/*', { admin: adminPermission })
|
||||
* ```
|
||||
*/
|
||||
export const addHTTPPermission = <In = unknown>(
|
||||
pattern: string,
|
||||
permissions: Record<string, PikkuPermission<In>> | PikkuPermission<In>[]
|
||||
) => {
|
||||
addHTTPPermissionCore(pattern, permissions as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Route configuration for wireHTTPRoutes with proper typing
|
||||
*/
|
||||
type HTTPRouteConfig = HTTPRouteBaseConfig & {
|
||||
method: HTTPMethod
|
||||
route: string
|
||||
func: PikkuFunctionConfig<any, any, any, any, any, any>
|
||||
auth?: boolean
|
||||
permissions?: Record<string, PikkuPermission | PikkuPermission[]>
|
||||
middleware?: PikkuMiddleware[]
|
||||
sse?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed route map for wireHTTPRoutes
|
||||
*/
|
||||
type TypedHTTPRouteMap = {
|
||||
[key: string]: HTTPRouteConfig | TypedHTTPRouteMap | TypedHTTPRouteContract
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed route contract for defineHTTPRoutes
|
||||
*/
|
||||
type TypedHTTPRouteContract<T extends TypedHTTPRouteMap = TypedHTTPRouteMap> = TypedHTTPRoutesGroupConfig & {
|
||||
routes: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Group config with typed middleware/permissions
|
||||
*/
|
||||
type TypedHTTPRoutesGroupConfig = {
|
||||
basePath?: string
|
||||
tags?: string[]
|
||||
auth?: boolean
|
||||
middleware?: PikkuMiddleware[]
|
||||
permissions?: Record<string, PikkuPermission | PikkuPermission[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Full config for wireHTTPRoutes
|
||||
*/
|
||||
type TypedWireHTTPRoutesConfig = TypedHTTPRoutesGroupConfig & {
|
||||
routes: TypedHTTPRouteMap | HTTPRouteConfig[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe helper for defining route contracts that can be composed.
|
||||
*/
|
||||
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(routes: T): TypedHTTPRouteContract<T>
|
||||
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(config: TypedHTTPRoutesGroupConfig & { routes: T }): TypedHTTPRouteContract<T>
|
||||
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(configOrRoutes: T | (TypedHTTPRoutesGroupConfig & { routes: T })): TypedHTTPRouteContract<T> {
|
||||
return defineHTTPRoutesCore(configOrRoutes as any) as unknown as TypedHTTPRouteContract<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires multiple HTTP routes from a nested map or array configuration.
|
||||
*/
|
||||
export const wireHTTPRoutes = (config: TypedWireHTTPRoutesConfig): void => {
|
||||
wireHTTPRoutesCore(config as any)
|
||||
}
|
||||
151
packages/functions/.pikku/http/pikku-http-wirings-map.gen.d.ts
vendored
Normal file
151
packages/functions/.pikku/http/pikku-http-wirings-map.gen.d.ts
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
* This provides the structure needed for typescript to be aware of routes and their return types
|
||||
*/
|
||||
|
||||
import type { StreamWorkflowRunInput, StreamFunctionTestsOutput } from '@pikku/addon-console/dist/.pikku/rpc/pikku-rpc-wirings-map.internal.gen'
|
||||
|
||||
|
||||
|
||||
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
|
||||
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
|
||||
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
|
||||
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string; }
|
||||
export type AuthHandlerOutput = Promise<any> | Promise<void>
|
||||
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string; }
|
||||
export type DeleteAgentThreadOutput = { deleted: boolean; }
|
||||
export type EdgeEchoInput = {
|
||||
message: string;
|
||||
}
|
||||
export type EdgeEchoOutput = {
|
||||
echoed: string;
|
||||
serverless: boolean;
|
||||
}
|
||||
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string; }
|
||||
export type GetAgentThreadMessagesOutput = any[]
|
||||
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string; }
|
||||
export type GetAgentThreadRunsOutput = any[]
|
||||
export type GetAgentThreadsInput = { agentName?: string; resourceId?: string; limit?: number; offset?: number; }
|
||||
export type GetAgentThreadsOutput = any[]
|
||||
export type GetMessageInput = {}
|
||||
export type GetMessageOutput = {
|
||||
message: string;
|
||||
updatedAt: string;
|
||||
updatedBy: {
|
||||
email: string;
|
||||
name: string | null;
|
||||
} | null;
|
||||
}
|
||||
export type GetSessionInput = {}
|
||||
export type GetSessionOutput = {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
}
|
||||
export type PikkuConsoleGetSecretInput = { secretId: string; }
|
||||
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
|
||||
export type PikkuConsoleGetVariableInput = { variableId: string; }
|
||||
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
|
||||
export type PikkuConsoleHasSecretInput = { secretId: string; }
|
||||
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
|
||||
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
|
||||
export type PikkuConsoleSetSecretOutput = { success: boolean; }
|
||||
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
|
||||
export type PikkuConsoleSetVariableOutput = { success: boolean; }
|
||||
export type RemoteRPCHandlerInput = { rpcName: string; data?: unknown; }
|
||||
export type RpcCallerInput = { rpcName: string; data?: unknown; }
|
||||
export type SecretSchema_betterAuthSecret = string
|
||||
export type ServerCounterInput = {}
|
||||
export type ServerCounterOutput = {
|
||||
count: number;
|
||||
pid: number;
|
||||
}
|
||||
export type ServerUptimeInput = {}
|
||||
export type ServerUptimeOutput = {
|
||||
uptimeSeconds: number;
|
||||
nodeVersion: string;
|
||||
rssMb: number;
|
||||
}
|
||||
export type UpdateMessageInput = {
|
||||
message: string;
|
||||
}
|
||||
export type UpdateMessageOutput = {
|
||||
message: string;
|
||||
updatedAt: string;
|
||||
updatedBy: {
|
||||
email: string;
|
||||
name: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// The '& {}' is a workaround for not directly refering to a type since it confuses typescript
|
||||
export type ServerUptimeInputBody = ServerUptimeInput & {}
|
||||
export type ServerCounterInputBody = ServerCounterInput & {}
|
||||
export type AgentCallerInputParams = Pick<AgentCallerInput, 'agentName'> & {}
|
||||
export type AgentCallerInputBody = Omit<AgentCallerInput, 'agentName'> & {}
|
||||
export type AgentStreamCallerInputParams = Pick<AgentStreamCallerInput, 'agentName'> & {}
|
||||
export type AgentStreamCallerInputBody = Omit<AgentStreamCallerInput, 'agentName'> & {}
|
||||
export type AgentApproveCallerInputParams = Pick<AgentApproveCallerInput, 'agentName'> & {}
|
||||
export type AgentApproveCallerInputBody = Omit<AgentApproveCallerInput, 'agentName'> & {}
|
||||
export type AgentResumeCallerInputParams = Pick<AgentResumeCallerInput, 'agentName'> & {}
|
||||
export type AgentResumeCallerInputBody = Omit<AgentResumeCallerInput, 'agentName'> & {}
|
||||
export type StreamWorkflowRunInputParams = Pick<StreamWorkflowRunInput, 'runId'> & {}
|
||||
export type StreamWorkflowRunInputBody = Omit<StreamWorkflowRunInput, 'runId'> & {}
|
||||
export type RpcCallerInputParams = Pick<RpcCallerInput, 'rpcName'> & {}
|
||||
export type RpcCallerInputBody = Omit<RpcCallerInput, 'rpcName'> & {}
|
||||
export type RemoteRPCHandlerInputParams = Pick<RemoteRPCHandlerInput, 'rpcName'> & {}
|
||||
export type RemoteRPCHandlerInputBody = Omit<RemoteRPCHandlerInput, 'rpcName'> & {}
|
||||
|
||||
interface HTTPWiringHandler<I, O> {
|
||||
input: I;
|
||||
output: O;
|
||||
}
|
||||
|
||||
export type HTTPWiringsMap = {
|
||||
readonly '/api/auth{/*splat}': {
|
||||
readonly GET: HTTPWiringHandler<null, AuthHandlerOutput>,
|
||||
readonly POST: HTTPWiringHandler<null, AuthHandlerOutput>,
|
||||
},
|
||||
readonly '/workflow-run/:runId/stream': {
|
||||
readonly GET: HTTPWiringHandler<StreamWorkflowRunInput, null>,
|
||||
},
|
||||
readonly '/function-tests/stream': {
|
||||
readonly GET: HTTPWiringHandler<null, StreamFunctionTestsOutput>,
|
||||
},
|
||||
readonly '/server/uptime': {
|
||||
readonly POST: HTTPWiringHandler<ServerUptimeInput, ServerUptimeOutput>,
|
||||
},
|
||||
readonly '/server/counter': {
|
||||
readonly POST: HTTPWiringHandler<ServerCounterInput, ServerCounterOutput>,
|
||||
},
|
||||
readonly '/rpc/agent/:agentName': {
|
||||
readonly POST: HTTPWiringHandler<AgentCallerInput, null>,
|
||||
},
|
||||
readonly '/rpc/agent/:agentName/stream': {
|
||||
readonly POST: HTTPWiringHandler<AgentStreamCallerInput, null>,
|
||||
},
|
||||
readonly '/rpc/agent/:agentName/approve': {
|
||||
readonly POST: HTTPWiringHandler<AgentApproveCallerInput, null>,
|
||||
},
|
||||
readonly '/rpc/agent/:agentName/resume': {
|
||||
readonly POST: HTTPWiringHandler<AgentResumeCallerInput, null>,
|
||||
},
|
||||
readonly '/rpc/:rpcName': {
|
||||
readonly POST: HTTPWiringHandler<RpcCallerInput, null>,
|
||||
},
|
||||
readonly '/remote/rpc/:rpcName': {
|
||||
readonly POST: HTTPWiringHandler<RemoteRPCHandlerInput, null>,
|
||||
},
|
||||
};
|
||||
|
||||
export type HTTPWiringHandlerOf<HTTPWiring extends keyof HTTPWiringsMap, Method extends keyof HTTPWiringsMap[HTTPWiring]> =
|
||||
HTTPWiringsMap[HTTPWiring][Method] extends { input: infer I; output: infer O }
|
||||
? HTTPWiringHandler<I, O>
|
||||
: never;
|
||||
|
||||
export type HTTPWiringsWithMethod<Method extends string> = {
|
||||
[HTTPWiring in keyof HTTPWiringsMap]: Method extends keyof HTTPWiringsMap[HTTPWiring] ? HTTPWiring : never;
|
||||
}[keyof HTTPWiringsMap];
|
||||
|
||||
197
packages/functions/.pikku/http/pikku-http-wirings-meta.gen.json
Normal file
197
packages/functions/.pikku/http/pikku-http-wirings-meta.gen.json
Normal file
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"get": {
|
||||
"/api/auth{/*splat}": {
|
||||
"pikkuFuncId": "authHandler",
|
||||
"route": "/api/auth{/*splat}",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth.gen.ts",
|
||||
"method": "get",
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/workflow-run/:runId/stream": {
|
||||
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
|
||||
"route": "/workflow-run/:runId/stream",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"method": "get",
|
||||
"params": [
|
||||
"runId"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
],
|
||||
"sse": true
|
||||
},
|
||||
"/function-tests/stream": {
|
||||
"pikkuFuncId": "http:get:/function-tests/stream",
|
||||
"route": "/function-tests/stream",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/console.gen.ts",
|
||||
"method": "get",
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
],
|
||||
"sse": true
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"/server/uptime": {
|
||||
"pikkuFuncId": "serverUptime",
|
||||
"route": "/server/uptime",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/wirings/server.http.ts",
|
||||
"method": "post",
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/server/counter": {
|
||||
"pikkuFuncId": "serverCounter",
|
||||
"route": "/server/counter",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/wirings/server.http.ts",
|
||||
"method": "post",
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/rpc/agent/:agentName": {
|
||||
"pikkuFuncId": "agentCaller",
|
||||
"route": "/rpc/agent/:agentName",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"agentName"
|
||||
],
|
||||
"tags": [
|
||||
"pikku:public"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/rpc/agent/:agentName/stream": {
|
||||
"pikkuFuncId": "agentStreamCaller",
|
||||
"route": "/rpc/agent/:agentName/stream",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"agentName"
|
||||
],
|
||||
"tags": [
|
||||
"pikku:public"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
],
|
||||
"sse": true
|
||||
},
|
||||
"/rpc/agent/:agentName/approve": {
|
||||
"pikkuFuncId": "agentApproveCaller",
|
||||
"route": "/rpc/agent/:agentName/approve",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"agentName"
|
||||
],
|
||||
"tags": [
|
||||
"pikku:public"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/rpc/agent/:agentName/resume": {
|
||||
"pikkuFuncId": "agentResumeCaller",
|
||||
"route": "/rpc/agent/:agentName/resume",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/agent.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"agentName"
|
||||
],
|
||||
"tags": [
|
||||
"pikku:public"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
],
|
||||
"sse": true
|
||||
},
|
||||
"/api/auth{/*splat}": {
|
||||
"pikkuFuncId": "authHandler",
|
||||
"route": "/api/auth{/*splat}",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth.gen.ts",
|
||||
"method": "post",
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/rpc/:rpcName": {
|
||||
"pikkuFuncId": "rpcCaller",
|
||||
"route": "/rpc/:rpcName",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/rpc-public.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"rpcName"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/remote/rpc/:rpcName": {
|
||||
"pikkuFuncId": "remoteRPCHandler",
|
||||
"route": "/remote/rpc/:rpcName",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/rpc-remote.gen.ts",
|
||||
"method": "post",
|
||||
"params": [
|
||||
"rpcName"
|
||||
],
|
||||
"middleware": [
|
||||
{
|
||||
"type": "http",
|
||||
"route": "*"
|
||||
},
|
||||
{
|
||||
"type": "wire",
|
||||
"name": "pikkuRemoteAuthMiddleware",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"put": {},
|
||||
"delete": {},
|
||||
"head": {},
|
||||
"patch": {},
|
||||
"options": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
import { pikkuState } from '@pikku/core/internal'
|
||||
import type { HTTPWiringsMeta } from '@pikku/core/http'
|
||||
import metaData from './pikku-http-wirings-meta.gen.json' with { type: 'json' }
|
||||
pikkuState(null, 'http', 'meta', metaData as HTTPWiringsMeta)
|
||||
12
packages/functions/.pikku/http/pikku-http-wirings.gen.ts
Normal file
12
packages/functions/.pikku/http/pikku-http-wirings.gen.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/* The files with an wireHTTP function call */
|
||||
import '../../src/middleware/cors.middleware.js'
|
||||
import '../../src/scaffold/agent.gen.js'
|
||||
import '../../src/scaffold/auth-middleware.gen.js'
|
||||
import '../../src/scaffold/auth.gen.js'
|
||||
import '../../src/scaffold/console.gen.js'
|
||||
import '../../src/scaffold/rpc-public.gen.js'
|
||||
import '../../src/scaffold/rpc-remote.gen.js'
|
||||
import '../../src/wirings/server.http.js'
|
||||
5
packages/functions/.pikku/mcp/mcp.gen.json
Normal file
5
packages/functions/.pikku/mcp/mcp.gen.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tools": [],
|
||||
"resources": [],
|
||||
"prompts": []
|
||||
}
|
||||
183
packages/functions/.pikku/mcp/pikku-mcp-types.gen.ts
Normal file
183
packages/functions/.pikku/mcp/pikku-mcp-types.gen.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
/**
|
||||
* MCP-specific type definitions for tree-shaking optimization
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreMCPResource,
|
||||
CoreMCPPrompt,
|
||||
wireMCPResource as wireMCPResourceCore,
|
||||
wireMCPPrompt as wireMCPPromptCore,
|
||||
MCPResourceResponse,
|
||||
MCPToolResponse,
|
||||
MCPPromptResponse,
|
||||
AssertMCPResourceURIParams
|
||||
} from '@pikku/core/mcp'
|
||||
|
||||
import type { PikkuFunctionConfig, PikkuFunctionSessionless, PikkuMiddleware, PikkuPermission, InferSchemaOutput } from '../function/pikku-function-types.gen.js'
|
||||
import type { CorePermissionGroup } from '@pikku/core'
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
/**
|
||||
* Type definition for MCP resources that provide data to AI models.
|
||||
*
|
||||
* @template In - Input type for the resource request
|
||||
* @template URI - URI template string type for compile-time parameter validation
|
||||
*/
|
||||
type MCPResourceWiring<In, URI extends string> = CoreMCPResource<PikkuFunctionConfig<In, MCPResourceResponse, 'rpc' | 'session' | 'mcp'>> & { uri: URI }
|
||||
|
||||
/**
|
||||
* Type definition for MCP prompts that provide templates to AI models.
|
||||
*
|
||||
* @template In - Input type for the prompt parameters
|
||||
*/
|
||||
type MCPPromptWiring<In> = CoreMCPPrompt<PikkuFunctionConfig<In, MCPPromptResponse, 'rpc' | 'session' | 'mcp'>>
|
||||
|
||||
/**
|
||||
* Registers an MCP resource with the Pikku framework.
|
||||
* Resources provide data that AI models can access.
|
||||
*
|
||||
* @template In - Input type for the resource request
|
||||
* @template URI - URI template string for compile-time parameter validation
|
||||
* @param mcpResource - MCP resource definition with data provider function
|
||||
*/
|
||||
export const wireMCPResource = <In, URI extends string>(
|
||||
mcpResource: MCPResourceWiring<In, URI> & AssertMCPResourceURIParams<In, URI>
|
||||
) => {
|
||||
wireMCPResourceCore(mcpResource as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an MCP prompt with the Pikku framework.
|
||||
* Prompts provide templates that AI models can use.
|
||||
*
|
||||
* @template In - Input type for the prompt parameters
|
||||
* @param mcpPrompt - MCP prompt definition with template function
|
||||
*/
|
||||
export const wireMCPPrompt = <In>(
|
||||
mcpPrompt: MCPPromptWiring<In>
|
||||
) => {
|
||||
wireMCPPromptCore(mcpPrompt as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MCP prompt with Zod schema input validation.
|
||||
*/
|
||||
type MCPPromptFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
|
||||
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPPromptResponse, 'mcp' | 'rpc'>
|
||||
name?: string
|
||||
input: InputSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function for handling MCP prompt requests.
|
||||
* These functions generate prompt templates for AI models.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuMCPPromptFunc<Input>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuMCPPromptFunc({ input: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for the prompt parameters (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The unwrapped function for internal use
|
||||
*/
|
||||
export function pikkuMCPPromptFunc<InputSchema extends StandardSchemaV1>(
|
||||
config: MCPPromptFuncConfigWithSchema<InputSchema>
|
||||
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPPromptResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPPromptFunc<In>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<In, MCPPromptResponse, 'mcp' | 'rpc'>
|
||||
| {
|
||||
func: PikkuFunctionSessionless<In, MCPPromptResponse, 'mcp' | 'rpc'>
|
||||
name?: string
|
||||
}
|
||||
): PikkuFunctionConfig<In, MCPPromptResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPPromptFunc(func: any): any {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MCP tool with Zod schema input validation.
|
||||
*/
|
||||
type MCPToolFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
|
||||
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPToolResponse, 'mcp' | 'rpc'>
|
||||
description?: string
|
||||
tags?: string[]
|
||||
title?: string
|
||||
summary?: string
|
||||
name?: string
|
||||
middleware?: PikkuMiddleware[]
|
||||
permissions?: CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
|
||||
input: InputSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function for handling MCP tool invocations.
|
||||
* These functions perform actions that AI models can request.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuMCPToolFunc<Input>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuMCPToolFunc({ input: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for the tool invocation (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The unwrapped function for internal use
|
||||
*/
|
||||
export function pikkuMCPToolFunc<InputSchema extends StandardSchemaV1>(
|
||||
config: MCPToolFuncConfigWithSchema<InputSchema>
|
||||
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPToolResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPToolFunc<In>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<In, MCPToolResponse, 'mcp' | 'rpc'>
|
||||
| {
|
||||
func: PikkuFunctionSessionless<In, MCPToolResponse, 'mcp' | 'rpc'>
|
||||
description?: string
|
||||
tags?: string[]
|
||||
title?: string
|
||||
summary?: string
|
||||
name?: string
|
||||
middleware?: PikkuMiddleware[]
|
||||
permissions?: CorePermissionGroup<PikkuPermission<In>>
|
||||
}
|
||||
): PikkuFunctionConfig<In, MCPToolResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPToolFunc(func: any): any {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MCP resource with Zod schema input validation.
|
||||
*/
|
||||
type MCPResourceFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
|
||||
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPResourceResponse, 'mcp' | 'rpc'>
|
||||
name?: string
|
||||
input: InputSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function for handling MCP resource requests.
|
||||
* These functions provide data that AI models can access.
|
||||
*
|
||||
* Supports two patterns:
|
||||
* 1. Generic types: `pikkuMCPResourceFunc<Input>({ func: ... })`
|
||||
* 2. Zod schemas: `pikkuMCPResourceFunc({ input: z.object(...), func: ... })`
|
||||
*
|
||||
* @template In - Input type for the resource request (inferred from schema if provided)
|
||||
* @param func - Function definition, either direct function or configuration object
|
||||
* @returns The unwrapped function for internal use
|
||||
*/
|
||||
export function pikkuMCPResourceFunc<InputSchema extends StandardSchemaV1>(
|
||||
config: MCPResourceFuncConfigWithSchema<InputSchema>
|
||||
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPResourceResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPResourceFunc<In>(
|
||||
func:
|
||||
| PikkuFunctionSessionless<In, MCPResourceResponse, 'mcp' | 'rpc'>
|
||||
| {
|
||||
func: PikkuFunctionSessionless<In, MCPResourceResponse, 'mcp' | 'rpc'>
|
||||
name?: string
|
||||
}
|
||||
): PikkuFunctionConfig<In, MCPResourceResponse, 'mcp' | 'rpc'>
|
||||
export function pikkuMCPResourceFunc(func: any): any {
|
||||
return typeof func === 'function' ? { func } : func
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"definitions": {},
|
||||
"instances": {
|
||||
"http:*:0": {
|
||||
"definitionId": "betterAuthStatelessSession",
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth-middleware.gen.ts",
|
||||
"position": 236,
|
||||
"isFactoryCall": true
|
||||
}
|
||||
},
|
||||
"httpGroups": {
|
||||
"*": {
|
||||
"exportName": null,
|
||||
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/server-and-serverless/packages/functions/src/scaffold/auth-middleware.gen.ts",
|
||||
"position": 236,
|
||||
"services": {
|
||||
"optimized": false,
|
||||
"services": []
|
||||
},
|
||||
"count": 1,
|
||||
"instanceIds": [
|
||||
"http:*:0"
|
||||
],
|
||||
"isFactory": false
|
||||
}
|
||||
},
|
||||
"tagGroups": {},
|
||||
"channelMiddleware": {
|
||||
"definitions": {},
|
||||
"instances": {},
|
||||
"tagGroups": {}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user