@empyria/webui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.oxfmtrc.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "ignorePatterns": [],
4
+ "useTabs": true,
5
+ "tabWidth": 4,
6
+ "singleQuote": true,
7
+ "trailingComma": "all",
8
+ "semi": false
9
+ }
package/.oxlintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "ignorePatterns": ["node_modules", ".nx", "coverage", "**/tmp/**"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Principia
2
+
3
+ The Architecture:
4
+
5
+ ```
6
+ Browser (Client) Nuxt Server Moleculer Backend
7
+ │ │ │
8
+ ├─ login() ──────────────>│ │
9
+ │ ├─ call('v1.Auth.login') ───>│
10
+ │ │<── returns user data ──────┤
11
+ │ ├─ setCookie(tokenKey) ──────┤ (secure, httpOnly)
12
+ │<── returns user data ───┤
13
+ ├─ update Pinia store
14
+
15
+ ├─ Future requests ───────>│
16
+ │ ├─ reads cookie ─────────────┤ (auto-included)
17
+ │ ├─ moleculer.call() ─────────>│ (with tokenKey)
18
+ ```
@@ -0,0 +1,7 @@
1
+ export default defineAppConfig({
2
+ ui: {
3
+ colors: {
4
+ primary: 'blue',
5
+ },
6
+ },
7
+ })
package/app/app.vue ADDED
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ const colorMode = useColorMode()
3
+
4
+ const toaster = { duration: 5000 }
5
+
6
+ const color = computed(() => (colorMode.value === 'dark' ? '#1b1718' : 'white'))
7
+
8
+ useHead({
9
+ meta: [
10
+ { charset: 'utf-8' },
11
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
12
+ { key: 'theme-color', name: 'theme-color', content: color },
13
+ ],
14
+ link: [{ rel: 'icon', href: '/favicon.ico' }],
15
+ htmlAttrs: {
16
+ lang: 'en',
17
+ },
18
+ })
19
+
20
+ const title = 'Principia Fluxus'
21
+ const description = 'AI-powered Data Lake platform'
22
+
23
+ useSeoMeta({
24
+ title,
25
+ description,
26
+ ogTitle: title,
27
+ ogDescription: description,
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <UApp :toaster="toaster">
33
+ <NuxtLayout>
34
+ <NuxtPage />
35
+ </NuxtLayout>
36
+ </UApp>
37
+ </template>
@@ -0,0 +1,189 @@
1
+ @import 'tailwindcss';
2
+ @import '@nuxt/ui';
3
+
4
+ @plugin "@tailwindcss/typography";
5
+
6
+ @theme static {
7
+ --font-sans: 'Public Sans', sans-serif;
8
+
9
+ --color-green-50: #effdf5;
10
+ --color-green-100: #d9fbe8;
11
+ --color-green-200: #b3f5d1;
12
+ --color-green-300: #75edae;
13
+ --color-green-400: #00dc82;
14
+ --color-green-500: #00c16a;
15
+ --color-green-600: #00a155;
16
+ --color-green-700: #007f45;
17
+ --color-green-800: #016538;
18
+ --color-green-900: #0a5331;
19
+ --color-green-950: #052e16;
20
+ }
21
+
22
+ @theme {
23
+ --color-cbh-50: #f4f6fb;
24
+ --color-cbh-100: #e8ebf6;
25
+ --color-cbh-200: #cdd6ea;
26
+ --color-cbh-300: #a1b3d8;
27
+ --color-cbh-400: #6e8bc2;
28
+ --color-cbh-500: #4c6dab;
29
+ --color-cbh-600: #3c5896;
30
+ --color-cbh-700: #2f4475;
31
+ --color-cbh-800: #2a3a62;
32
+ --color-cbh-900: #283452;
33
+ --color-cbh-950: #1a2137;
34
+
35
+ --font-sans: 'Montserrat', system-ui, sans-serif;
36
+ --breakpoint-3xl: 1920px;
37
+ --breakpoint-4xl: 2560px;
38
+ --breakpoint-5xl: 3840px;
39
+
40
+ @keyframes tada {
41
+ from {
42
+ transform: scale3d(1, 1, 1);
43
+ }
44
+
45
+ 10%,
46
+ 20% {
47
+ transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
48
+ }
49
+
50
+ 30%,
51
+ 50%,
52
+ 70%,
53
+ 90% {
54
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
55
+ }
56
+
57
+ 40%,
58
+ 60%,
59
+ 80% {
60
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
61
+ }
62
+
63
+ to {
64
+ transform: scale3d(1, 1, 1);
65
+ }
66
+ }
67
+
68
+ --animate-tada: tada 1s ease-in-out infinite;
69
+
70
+ @keyframes backOutLeft {
71
+ 0% {
72
+ transform: scale(1);
73
+ opacity: 1;
74
+ }
75
+
76
+ 20% {
77
+ transform: translateX(0px) scale(0.8);
78
+ opacity: 0.7;
79
+ }
80
+
81
+ 100% {
82
+ transform: translateX(-100px) scale(0.8);
83
+ opacity: 0.7;
84
+ }
85
+ }
86
+
87
+ --animate-backOutLeft: backOutLeft 0.8s ease-in-out infinite;
88
+
89
+ @keyframes backOutRight {
90
+ 0% {
91
+ transform: scale(1);
92
+ opacity: 1;
93
+ }
94
+
95
+ 20% {
96
+ transform: translateX(0px) scale(0.8);
97
+ opacity: 0.7;
98
+ }
99
+
100
+ 100% {
101
+ transform: translateX(50px) scale(0.8);
102
+ opacity: 0.5;
103
+ }
104
+ }
105
+
106
+ --animate-backOutRight: backOutRight 0.8s ease-in-out infinite;
107
+
108
+ @keyframes outRight {
109
+ 0% {
110
+ transform: scale(1);
111
+ opacity: 1;
112
+ }
113
+
114
+ 100% {
115
+ transform: translateX(50px);
116
+ opacity: 0.5;
117
+ }
118
+ }
119
+
120
+ --animate-outRight: outRight 0.8s ease-in-out infinite;
121
+
122
+ @keyframes outDown {
123
+ 0% {
124
+ transform: scale(1);
125
+ opacity: 1;
126
+ }
127
+
128
+ 100% {
129
+ transform: translateY(50px);
130
+ opacity: 0.5;
131
+ }
132
+ }
133
+
134
+ --animate-outDown: outDown 0.8s ease-in-out infinite;
135
+
136
+ @keyframes outUp {
137
+ 0% {
138
+ transform: scale(1);
139
+ opacity: 1;
140
+ }
141
+
142
+ 100% {
143
+ transform: translateY(-50px);
144
+ opacity: 0.5;
145
+ }
146
+ }
147
+
148
+ --animate-outUp: outUp 0.8s ease-in-out infinite;
149
+
150
+ @keyframes spinning {
151
+ 0% {
152
+ transform: rotate(0deg);
153
+ }
154
+
155
+ 100% {
156
+ transform: rotate(360deg);
157
+ }
158
+ }
159
+
160
+ --animate-spinning: spinning 0.8s linear infinite;
161
+ }
162
+
163
+ @utility animate-tada {
164
+ animation: var(--animate-tada);
165
+ }
166
+
167
+ @utility animate-backOutLeft {
168
+ animation: var(--animate-backOutLeft);
169
+ }
170
+
171
+ @utility animate-backOutRight {
172
+ animation: var(--animate-backOutRight);
173
+ }
174
+
175
+ @utility animate-outRight {
176
+ animation: var(--animate-outRight);
177
+ }
178
+
179
+ @utility animate-outDown {
180
+ animation: var(--animate-outDown);
181
+ }
182
+
183
+ @utility animate-outUp {
184
+ animation: var(--animate-outUp);
185
+ }
186
+
187
+ @utility animate-spinning {
188
+ animation: var(--animate-spinning);
189
+ }
package/app/consts.ts ADDED
@@ -0,0 +1,5 @@
1
+ export const MIN_WINDOW_WIDTH: number = 1280
2
+ export const MIN_WINDOW_HEIGHT: number = 700
3
+ export const OPTIMAL_WIDTH: number = 1500
4
+
5
+ export const AccessTokenCookieKey = 'cbh-access-token'
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <slot />
3
+ </template>
@@ -0,0 +1,7 @@
1
+ <script setup>
2
+ definePageMeta({ layout: 'default' })
3
+ </script>
4
+
5
+ <template>
6
+ <h1>Hello</h1>
7
+ </template>
@@ -0,0 +1,74 @@
1
+ import { createTRPCProxyClient, httpBatchStreamLink, httpLink, splitLink } from '@trpc/client'
2
+ import type { AppRouter } from '~/server/trpc/router'
3
+
4
+ export default defineNuxtPlugin(() => {
5
+ const isLoading = ref(false)
6
+ const toast = useToast()
7
+
8
+ let activeRequests = 0
9
+
10
+ const getBaseUrl = () => {
11
+ if (import.meta.server) {
12
+ return process.env.NUXT_PUBLIC_API_URL || 'http://localhost:3000'
13
+ }
14
+ return ''
15
+ }
16
+
17
+ const client = createTRPCProxyClient<AppRouter>({
18
+ links: [
19
+ splitLink({
20
+ condition: (op) => op.path.includes('login') || op.path.includes('auth'),
21
+ true: httpLink({
22
+ url: `${getBaseUrl()}/api/trpc`,
23
+ fetch(url, options) {
24
+ return fetch(url, { ...options, credentials: 'include' })
25
+ },
26
+ }),
27
+ false: httpBatchStreamLink({
28
+ url: `${getBaseUrl()}/api/trpc`,
29
+ fetch(url, options) {
30
+ activeRequests++
31
+ isLoading.value = true
32
+ return fetch(url, {
33
+ ...options,
34
+ credentials: 'include',
35
+ })
36
+ .then((response) => {
37
+ if (response.status >= 500) {
38
+ toast.add({
39
+ title: 'Server Error',
40
+ description: 'Something went wrong. Please try again.',
41
+ icon: 'i-lucide-server-crash',
42
+ color: 'error',
43
+ })
44
+ }
45
+ return response
46
+ })
47
+ .catch((error) => {
48
+ toast.add({
49
+ title: 'Connection Error',
50
+ description: 'Unable to reach the server.',
51
+ icon: 'i-lucide-server-off',
52
+ color: 'error',
53
+ })
54
+ throw error
55
+ })
56
+ .finally(() => {
57
+ activeRequests--
58
+ if (activeRequests === 0) {
59
+ isLoading.value = false
60
+ }
61
+ })
62
+ },
63
+ }),
64
+ }),
65
+ ],
66
+ })
67
+
68
+ return {
69
+ provide: {
70
+ client,
71
+ trpcLoading: readonly(isLoading),
72
+ },
73
+ }
74
+ })
@@ -0,0 +1,45 @@
1
+ import { defineStore } from 'pinia'
2
+
3
+ export const useCredentialStore = defineStore('principia-credential', {
4
+ state: () => ({
5
+ email: '',
6
+ actor: '',
7
+ federation: '',
8
+ role: '',
9
+ flowID: '',
10
+ processID: '',
11
+ cookieExpiration: 0,
12
+ }),
13
+ getters: {
14
+ isAuthenticated: (state) => state.actor !== null && state.federation !== null,
15
+ },
16
+ actions: {
17
+ setCredentials(credentials: {
18
+ email: string
19
+ actor: string
20
+ federation: string
21
+ role: string
22
+ flowID: string
23
+ processID: string
24
+ cookieExpiration: number
25
+ }) {
26
+ this.email = credentials.email
27
+ this.actor = credentials.actor
28
+ this.federation = credentials.federation
29
+ this.role = credentials.role
30
+ this.flowID = credentials.flowID
31
+ this.processID = credentials.processID
32
+ this.cookieExpiration = credentials.cookieExpiration
33
+ },
34
+
35
+ logout() {
36
+ this.email = ''
37
+ this.actor = ''
38
+ this.federation = ''
39
+ this.role = ''
40
+ this.cookieExpiration = 0
41
+ },
42
+ },
43
+
44
+ persist: true,
45
+ })
@@ -0,0 +1,33 @@
1
+ import { DateTime } from 'luxon'
2
+
3
+ const DATE_FORMAT = 'yyyy-LL-dd'
4
+
5
+ export const DISPLAY_DATE = 'dd / LLL / yyyy'
6
+ export const DISPLAY_TIME = 'dd / LLL / yyyy HH:mm'
7
+
8
+ export function dateToMillis(format: string) {
9
+ return DateTime.fromFormat(format, DATE_FORMAT).toMillis()
10
+ }
11
+ export function dateStartToMillis(format: string) {
12
+ return DateTime.fromFormat(format, DATE_FORMAT).startOf('day').toMillis()
13
+ }
14
+ export function dateEndToMillis(format: string) {
15
+ return DateTime.fromFormat(format, DATE_FORMAT).endOf('day').toMillis()
16
+ }
17
+ export function dateToFormat(millis?: number, format?: string) {
18
+ if (!millis) return undefined
19
+ return DateTime.fromMillis(millis).toFormat(format || DATE_FORMAT)
20
+ }
21
+
22
+ export function timeToFormat(millis?: number, format?: string) {
23
+ if (!millis) return undefined
24
+ return DateTime.fromMillis(millis).toFormat(format || DISPLAY_TIME)
25
+ }
26
+
27
+ export function isoToFormat(isoDate?: string, format?: string) {
28
+ return DateTime.fromISO(isoDate).toFormat(format || DATE_FORMAT)
29
+ }
30
+
31
+ export function nowISO() {
32
+ return DateTime.now().toISO()
33
+ }
@@ -0,0 +1,44 @@
1
+ // "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"
2
+ // status — possible values: pending, scheduled, ready, running, paused, backing-off, suspended, completed
3
+ // completion_result — only set when status = 'completed': success or failure
4
+
5
+ export const colors = {
6
+ status: {
7
+ completed: 'success',
8
+ pending: 'info',
9
+ scheduled: 'info',
10
+ ready: 'info',
11
+ running: 'warning',
12
+ 'backing-off': 'warning',
13
+ suspended: 'warning',
14
+ default: 'neutral',
15
+ },
16
+ completion: {
17
+ success: 'success',
18
+ failure: 'error',
19
+ },
20
+ }
21
+
22
+ export function componentColor(status: string, completion: string): string {
23
+ return colors.completion[completion] ?? colors.status[status] ?? colors.status.default
24
+ }
25
+
26
+ export function dotColor(status, completion_result): string {
27
+ return status !== 'completed'
28
+ ? 'yellow'
29
+ : !completion_result
30
+ ? 'ghost'
31
+ : completion_result === 'failure'
32
+ ? 'red'
33
+ : 'green'
34
+ }
35
+
36
+ export function textColor(status, completion_result): string {
37
+ return status !== 'completed'
38
+ ? 'text-warning'
39
+ : !completion_result
40
+ ? 'text-info'
41
+ : completion_result === 'failure'
42
+ ? 'text-error'
43
+ : 'text-success'
44
+ }
@@ -0,0 +1,7 @@
1
+ export const isFinished = (callChain) => {
2
+ return callChain?.status === 'completed'
3
+ }
4
+
5
+ export const wfOutput = (callChain) => {
6
+ return callChain?.journal?.result ?? callChain?.completion_failure
7
+ }
package/nuxt.config.ts ADDED
@@ -0,0 +1,57 @@
1
+ // https://nuxt.com/docs/api/configuration/nuxt-config
2
+ export default defineNuxtConfig({
3
+ ssr: false,
4
+ compatibilityDate: '2025-07-15',
5
+ devtools: { enabled: true },
6
+ experimental: {
7
+ asyncContext: true,
8
+ },
9
+ build: {
10
+ transpile: [],
11
+ },
12
+ modules: [
13
+ '@vueuse/nuxt',
14
+ '@pinia/nuxt',
15
+ 'pinia-plugin-persistedstate/nuxt',
16
+ '@nuxtjs/color-mode',
17
+ '@nuxt/ui',
18
+ '@nuxt/fonts',
19
+ '@nuxtjs/tailwindcss',
20
+ ],
21
+
22
+ css: ['~/assets/css/main.css'],
23
+
24
+ fonts: {
25
+ families: [{ name: 'Montserrat', provider: 'google' }],
26
+ providers: {
27
+ fontshare: false,
28
+ },
29
+ },
30
+
31
+ ui: {
32
+ theme: {
33
+ colors: [
34
+ 'primary',
35
+ 'secondary',
36
+ 'success',
37
+ 'info',
38
+ 'warning',
39
+ 'error',
40
+ 'neutral',
41
+ 'cbh',
42
+ ],
43
+ },
44
+ },
45
+
46
+ vite: {
47
+ server: {
48
+ watch: {
49
+ usePolling: true,
50
+ },
51
+ },
52
+ },
53
+
54
+ nitro: {
55
+ plugins: [],
56
+ },
57
+ })
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@empyria/webui",
3
+ "version": "0.1.1",
4
+ "description": "Restate.dev helpers for the Empyria nanoservice framework",
5
+ "license": "MIT",
6
+ "author": "Imre Fazekas <imre.fazekas@icloud.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/empyria-nano/guard-web.git"
10
+ },
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "nuxt build",
14
+ "dev": "nuxt dev",
15
+ "generate": "nuxt generate",
16
+ "preview": "nuxt preview",
17
+ "postinstall": "nuxt prepare",
18
+ "typecheck": "nuxt typecheck",
19
+ "clean": "pkill -9 -f 'nuxt dev' 2>/dev/null; rm -rf .nuxt .output dist node_modules/.cache node_modules/.vite",
20
+ "format": "bun oxfmt --check",
21
+ "format:fix": "bun oxfmt",
22
+ "lint": "bun oxlint",
23
+ "lint:fix": "bun oxlint --fix"
24
+ },
25
+ "dependencies": {
26
+ "@empyria/common": "0.1.2",
27
+ "@iconify-json/lucide": "^1.2.124",
28
+ "@iconify-json/simple-icons": "^1.2.93",
29
+ "@pinia/nuxt": "^1.0.2",
30
+ "@simplewebauthn/browser": "^13.3.0",
31
+ "@trpc/client": "^11.18.0",
32
+ "@trpc/server": "^11.18.0",
33
+ "@vueuse/core": "^14.4.0",
34
+ "@vueuse/nuxt": "^14.4.0",
35
+ "croner": "^10.0.1",
36
+ "cronstrue": "^3.24.0",
37
+ "moleculer": "0.15.1",
38
+ "ms": "^2.1.3",
39
+ "pinia-plugin-persistedstate": "^4.7.1",
40
+ "vue": "^3.5.41",
41
+ "vue-cal": "^5.0.1-rc.34",
42
+ "vue-router": "^5.2.0"
43
+ },
44
+ "devDependencies": {
45
+ "@nuxt/fonts": "^0.14.0",
46
+ "@nuxt/ui": "^4.10.0",
47
+ "@nuxtjs/tailwindcss": "7.0.0-beta.1",
48
+ "@tailwindcss/typography": "^0.5.20",
49
+ "nuxt": "^4.5.2",
50
+ "tailwindcss": "^4.3.3",
51
+ "typescript": "^7.0.2",
52
+ "vue-tsc": "^3.3.10",
53
+ "oxfmt": "0.64.0",
54
+ "oxlint": "1.79.0"
55
+ },
56
+ "engines": {
57
+ "bun": ">=1.4.0",
58
+ "node": ">=26"
59
+ },
60
+ "packageManager": "bun@1.4.0"
61
+ }
Binary file
package/renovate.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": ["github>nuxt/renovate-config-nuxt"],
3
+ "lockFileMaintenance": {
4
+ "enabled": true
5
+ },
6
+ "packageRules": [
7
+ {
8
+ "matchDepTypes": ["resolutions"],
9
+ "enabled": false
10
+ }
11
+ ],
12
+ "postUpdateOptions": ["pnpmDedupe"]
13
+ }
@@ -0,0 +1,16 @@
1
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
2
+ import { createContext } from '../../trpc/context'
3
+ import { appRouter } from '../../routers'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ // console.log('✅ Test route hit!')
7
+
8
+ const request = toWebRequest(event)
9
+
10
+ return fetchRequestHandler({
11
+ endpoint: '/api/trpc',
12
+ req: request,
13
+ router: appRouter,
14
+ createContext: () => createContext(event),
15
+ })
16
+ })
package/server/env.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createEnv, validate } from '@empyria/common'
2
+
3
+ const schema = createEnv({})
4
+
5
+ export const env = validate(schema, { ...process.env }, { coerceTypes: true })
6
+
7
+ export const settings = Object.assign({}, env, {})
@@ -0,0 +1,6 @@
1
+ // server/trpc/router.ts
2
+ import { router } from '../trpc'
3
+
4
+ export const appRouter = router({})
5
+
6
+ export type AppRouter = typeof appRouter
@@ -0,0 +1,33 @@
1
+ import { type CookieSerializeOptions } from 'cookie-es'
2
+ import {
3
+ type H3Event,
4
+ parseCookies,
5
+ getCookie as getCookieFromEvent,
6
+ setCookie as setCookieOnEvent,
7
+ deleteCookie as deleteCookieOnEvent,
8
+ } from 'h3'
9
+ import { AccessTokenCookieKey } from '../../app/consts'
10
+
11
+ export const createContext = async (event: H3Event) => {
12
+ const getCookie = (name: string) => getCookieFromEvent(event, name)
13
+ const setCookie = (name: string, value: string, serializeOptions?: CookieSerializeOptions) =>
14
+ setCookieOnEvent(event, name, value, serializeOptions)
15
+ const deleteCookie = (name: string, serializeOptions?: CookieSerializeOptions) =>
16
+ deleteCookieOnEvent(event, name, serializeOptions)
17
+
18
+ const tokenKey = getCookie(AccessTokenCookieKey)
19
+ if (tokenKey) {
20
+ event.context.tokenKey = tokenKey
21
+ }
22
+
23
+ return {
24
+ event,
25
+ cookies: parseCookies(event),
26
+ getCookie,
27
+ setCookie,
28
+ deleteCookie,
29
+ headers: event.headers,
30
+ }
31
+ }
32
+
33
+ export type Context = Awaited<ReturnType<typeof createContext>>
@@ -0,0 +1,72 @@
1
+ // server/trpc/trpc.ts
2
+ import { initTRPC, TRPCError } from '@trpc/server'
3
+
4
+ import { createValidator } from '@empyria/common'
5
+
6
+ import { AccessTokenCookieKey } from '../../app/consts'
7
+ import type { Context } from './context'
8
+
9
+ const t = initTRPC.context<Context>().create()
10
+
11
+ /**
12
+ * Public procedure - no authentication required
13
+ */
14
+ export const publicProcedure = t.procedure
15
+
16
+ /**
17
+ * Authorized procedure - requires access token in cookie
18
+ */
19
+ export const authorizedProcedure = t.procedure.use(async ({ ctx, next }) => {
20
+ const tokenKey = ctx.getCookie(AccessTokenCookieKey)
21
+
22
+ if (!tokenKey) {
23
+ throw new TRPCError({
24
+ code: 'UNAUTHORIZED',
25
+ message: 'You are not authorized to make this request',
26
+ })
27
+ }
28
+
29
+ // Token is validated, proceed with request
30
+ return next()
31
+ })
32
+
33
+ // Passthrough parser - accepts any input and returns it as-is
34
+ const passthroughParser = {
35
+ parse: (input: unknown) => input,
36
+ }
37
+
38
+ export const validatedMiddleware = (schema: object) => {
39
+ const validate = createValidator(schema)
40
+
41
+ return middleware(async ({ next, getRawInput }) => {
42
+ const rawInput = await getRawInput()
43
+ try {
44
+ validate(rawInput)
45
+ } catch (err) {
46
+ throw new TRPCError({
47
+ code: 'BAD_REQUEST',
48
+ message: 'Validation failed',
49
+ cause: {
50
+ errors: [err],
51
+ input: rawInput,
52
+ },
53
+ })
54
+ }
55
+ return next()
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Add AJV validation to any procedure
61
+ * @param baseProcedure - The base procedure (publicProcedure or authorizedProcedure)
62
+ * @param schema - The AJV schema to validate against
63
+ */
64
+ export const validatedProcedure = <T extends typeof publicProcedure>(
65
+ baseProcedure: T,
66
+ schema: object,
67
+ ) => {
68
+ return baseProcedure.input(passthroughParser).use(validatedMiddleware(schema))
69
+ }
70
+
71
+ export const router = t.router
72
+ export const middleware = t.middleware
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ // https://nuxt.com/docs/guide/concepts/typescript
3
+ "files": [],
4
+ "references": [
5
+ { "path": "./.nuxt/tsconfig.app.json" },
6
+ { "path": "./.nuxt/tsconfig.server.json" },
7
+ { "path": "./.nuxt/tsconfig.shared.json" },
8
+ { "path": "./.nuxt/tsconfig.node.json" }
9
+ ]
10
+ }