@igstack/app-catalog-backend-core 5.0.0 → 5.0.1-alpha-20260923024813

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.
@@ -2,8 +2,12 @@ import { TRPCError, initTRPC } from "@trpc/server";
2
2
 
3
3
  //#region src/server/trpcSetup.ts
4
4
  const t = initTRPC.context().create({ errorFormatter({ error, shape }) {
5
+ if (shape.data.httpStatus < 500) {
6
+ console.info(`[tRPC] ${error.code} ${shape.data.path ?? "<no path>"}: ${error.message}`);
7
+ return shape;
8
+ }
5
9
  console.error("[tRPC Error]", {
6
- path: shape.data?.path,
10
+ path: shape.data.path,
7
11
  code: error.code,
8
12
  message: error.message,
9
13
  cause: error.cause,
@@ -1 +1 @@
1
- {"version":3,"file":"trpcSetup.mjs","names":[],"sources":["../../src/server/trpcSetup.ts"],"sourcesContent":["import { TRPCError, initTRPC } from '@trpc/server'\nimport type { AcTrpcContext } from './acTrpcContext'\n\nexport const t = initTRPC.context<AcTrpcContext>().create({\n errorFormatter({ error, shape }: { error: unknown; shape: unknown }) {\n console.error('[tRPC Error]', {\n path: (shape as { data?: { path?: string } }).data?.path,\n code: (error as { code?: string }).code,\n message: (error as { message?: string }).message,\n cause: (error as { cause?: unknown }).cause,\n stack: (error as { stack?: string }).stack,\n })\n return shape\n },\n})\n\n/**\n * Export reusable router and procedure helpers\n */\nexport const router = t.router\nexport const publicProcedure = t.procedure\n\n/**\n * Middleware to check if user is authenticated\n */\nconst isAuthenticated = t.middleware(({ ctx, next }) => {\n if (!ctx.user) {\n throw new TRPCError({\n code: 'UNAUTHORIZED',\n message: 'You must be logged in to access this resource',\n })\n }\n return next({\n ctx: {\n ...ctx,\n user: ctx.user,\n },\n })\n})\n\n/**\n * Middleware to check if user is an admin\n */\nconst isAdminMiddleware = t.middleware(({ ctx, next }) => {\n if (!ctx.user) {\n throw new TRPCError({\n code: 'UNAUTHORIZED',\n message: 'You must be logged in',\n })\n }\n if (!ctx.isAdmin) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Admin access required',\n })\n }\n return next({\n ctx: {\n ...ctx,\n user: ctx.user,\n },\n })\n})\n\n/**\n * Admin procedure that requires admin permissions\n */\nexport const adminProcedure = t.procedure.use(isAdminMiddleware)\n\n/**\n * Protected procedure that requires authentication (but not admin)\n */\nexport const protectedProcedure = t.procedure.use(isAuthenticated)\n"],"mappings":";;;AAGA,MAAa,IAAI,SAAS,SAAwB,CAAC,OAAO,EACxD,eAAe,EAAE,OAAO,SAA6C;AACnE,SAAQ,MAAM,gBAAgB;EAC5B,MAAO,MAAuC,MAAM;EACpD,MAAO,MAA4B;EACnC,SAAU,MAA+B;EACzC,OAAQ,MAA8B;EACtC,OAAQ,MAA6B;EACtC,CAAC;AACF,QAAO;GAEV,CAAC;;;;AAKF,MAAa,SAAS,EAAE;AACxB,MAAa,kBAAkB,EAAE;;;;AAKjC,MAAM,kBAAkB,EAAE,YAAY,EAAE,KAAK,WAAW;AACtD,KAAI,CAAC,IAAI,KACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,QAAO,KAAK,EACV,KAAK;EACH,GAAG;EACH,MAAM,IAAI;EACX,EACF,CAAC;EACF;;;;AAKF,MAAM,oBAAoB,EAAE,YAAY,EAAE,KAAK,WAAW;AACxD,KAAI,CAAC,IAAI,KACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,KAAI,CAAC,IAAI,QACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,QAAO,KAAK,EACV,KAAK;EACH,GAAG;EACH,MAAM,IAAI;EACX,EACF,CAAC;EACF;;;;AAKF,MAAa,iBAAiB,EAAE,UAAU,IAAI,kBAAkB;;;;AAKhE,MAAa,qBAAqB,EAAE,UAAU,IAAI,gBAAgB"}
1
+ {"version":3,"file":"trpcSetup.mjs","names":[],"sources":["../../src/server/trpcSetup.ts"],"sourcesContent":["import { TRPCError, initTRPC } from '@trpc/server'\nimport type { AcTrpcContext } from './acTrpcContext'\n\nexport const t = initTRPC.context<AcTrpcContext>().create({\n errorFormatter({ error, shape }) {\n // A 4xx is the caller's mistake (unknown path, bad input, no session). One\n // plain line on stdout is enough; a stack on stderr would make log collectors\n // file it as status:error next to real failures.\n if (shape.data.httpStatus < 500) {\n console.info(\n `[tRPC] ${error.code} ${shape.data.path ?? '<no path>'}: ${error.message}`,\n )\n return shape\n }\n console.error('[tRPC Error]', {\n path: shape.data.path,\n code: error.code,\n message: error.message,\n cause: error.cause,\n stack: error.stack,\n })\n return shape\n },\n})\n\n/**\n * Export reusable router and procedure helpers\n */\nexport const router = t.router\nexport const publicProcedure = t.procedure\n\n/**\n * Middleware to check if user is authenticated\n */\nconst isAuthenticated = t.middleware(({ ctx, next }) => {\n if (!ctx.user) {\n throw new TRPCError({\n code: 'UNAUTHORIZED',\n message: 'You must be logged in to access this resource',\n })\n }\n return next({\n ctx: {\n ...ctx,\n user: ctx.user,\n },\n })\n})\n\n/**\n * Middleware to check if user is an admin\n */\nconst isAdminMiddleware = t.middleware(({ ctx, next }) => {\n if (!ctx.user) {\n throw new TRPCError({\n code: 'UNAUTHORIZED',\n message: 'You must be logged in',\n })\n }\n if (!ctx.isAdmin) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Admin access required',\n })\n }\n return next({\n ctx: {\n ...ctx,\n user: ctx.user,\n },\n })\n})\n\n/**\n * Admin procedure that requires admin permissions\n */\nexport const adminProcedure = t.procedure.use(isAdminMiddleware)\n\n/**\n * Protected procedure that requires authentication (but not admin)\n */\nexport const protectedProcedure = t.procedure.use(isAuthenticated)\n"],"mappings":";;;AAGA,MAAa,IAAI,SAAS,SAAwB,CAAC,OAAO,EACxD,eAAe,EAAE,OAAO,SAAS;AAI/B,KAAI,MAAM,KAAK,aAAa,KAAK;AAC/B,UAAQ,KACN,UAAU,MAAM,KAAK,GAAG,MAAM,KAAK,QAAQ,YAAY,IAAI,MAAM,UAClE;AACD,SAAO;;AAET,SAAQ,MAAM,gBAAgB;EAC5B,MAAM,MAAM,KAAK;EACjB,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,OAAO,MAAM;EACb,OAAO,MAAM;EACd,CAAC;AACF,QAAO;GAEV,CAAC;;;;AAKF,MAAa,SAAS,EAAE;AACxB,MAAa,kBAAkB,EAAE;;;;AAKjC,MAAM,kBAAkB,EAAE,YAAY,EAAE,KAAK,WAAW;AACtD,KAAI,CAAC,IAAI,KACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,QAAO,KAAK,EACV,KAAK;EACH,GAAG;EACH,MAAM,IAAI;EACX,EACF,CAAC;EACF;;;;AAKF,MAAM,oBAAoB,EAAE,YAAY,EAAE,KAAK,WAAW;AACxD,KAAI,CAAC,IAAI,KACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,KAAI,CAAC,IAAI,QACP,OAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;EACV,CAAC;AAEJ,QAAO,KAAK,EACV,KAAK;EACH,GAAG;EACH,MAAM,IAAI;EACX,EACF,CAAC;EACF;;;;AAKF,MAAa,iBAAiB,EAAE,UAAU,IAAI,kBAAkB;;;;AAKhE,MAAa,qBAAqB,EAAE,UAAU,IAAI,gBAAgB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "5.0.0",
3
+ "version": "5.0.1-alpha-20260923024813",
4
4
  "description": "Backend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "tsyringe": "^4.10.0",
46
46
  "yaml": "^2.8.0",
47
47
  "zod": "^4.3.5",
48
- "@igstack/app-catalog-shared-core": "5.0.0",
49
- "@igstack/app-catalog-table-sync": "5.0.0"
48
+ "@igstack/app-catalog-shared-core": "5.0.1-alpha-20260923024813",
49
+ "@igstack/app-catalog-table-sync": "5.0.1-alpha-20260923024813"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tanstack/vite-config": "^0.4.3",
@@ -73,7 +73,7 @@
73
73
  "engines": {
74
74
  "node": ">=24"
75
75
  },
76
- "gitHead": "6858ad093a344473e13ab3187e1c8a5b11211b9d",
76
+ "gitHead": "c43d402ba1478450c2865bfab1e736652c6d53fa",
77
77
  "scripts": {
78
78
  "build": "tsdown",
79
79
  "build:lenient": "tsdown",
@@ -0,0 +1,66 @@
1
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import type { MockInstance } from 'vitest'
4
+ import type { AcTrpcContext } from '../server/acTrpcContext'
5
+ import { publicProcedure, router } from '../server/trpcSetup'
6
+
7
+ // Every tRPC error passes through the errorFormatter in trpcSetup. A client mistake
8
+ // (bad path, bad input, no session) must not reach the log as an error with a stack:
9
+ // log collectors turn stderr into status:error and a probe at a guessed URL then
10
+ // pollutes the same alerting a real failure feeds.
11
+ const appRouter = router({
12
+ boom: publicProcedure.query(() => {
13
+ throw new Error('db exploded')
14
+ }),
15
+ })
16
+
17
+ function call(path: string) {
18
+ return fetchRequestHandler({
19
+ endpoint: '/api/trpc',
20
+ req: new Request(`http://localhost/api/trpc/${path}`),
21
+ router: appRouter,
22
+ createContext: () => ({}) as AcTrpcContext,
23
+ })
24
+ }
25
+
26
+ describe('tRPC error logging', () => {
27
+ let error: MockInstance
28
+ let info: MockInstance
29
+
30
+ beforeEach(() => {
31
+ error = vi.spyOn(console, 'error').mockImplementation(() => {})
32
+ info = vi.spyOn(console, 'info').mockImplementation(() => {})
33
+ })
34
+ afterEach(() => {
35
+ vi.restoreAllMocks()
36
+ })
37
+
38
+ it('logs an unknown procedure path as one info line, not an error', async () => {
39
+ const res = await call('appCatalog.getVersionInfo')
40
+
41
+ expect(res.status).toBe(404)
42
+ expect(error).not.toHaveBeenCalled()
43
+ expect(info).toHaveBeenCalledTimes(1)
44
+ expect(info).toHaveBeenCalledWith(
45
+ expect.stringContaining('NOT_FOUND appCatalog.getVersionInfo'),
46
+ )
47
+ expect(info).toHaveBeenCalledWith(expect.not.stringContaining(' at '))
48
+ })
49
+
50
+ it('keeps a procedure failure at error level with its stack', async () => {
51
+ const res = await call('boom')
52
+
53
+ expect(res.status).toBe(500)
54
+ expect(info).not.toHaveBeenCalled()
55
+ expect(error).toHaveBeenCalledTimes(1)
56
+ expect(error).toHaveBeenCalledWith(
57
+ '[tRPC Error]',
58
+ expect.objectContaining({
59
+ code: 'INTERNAL_SERVER_ERROR',
60
+ path: 'boom',
61
+ message: 'db exploded',
62
+ stack: expect.stringContaining(' at '),
63
+ }),
64
+ )
65
+ })
66
+ })
@@ -2,13 +2,22 @@ import { TRPCError, initTRPC } from '@trpc/server'
2
2
  import type { AcTrpcContext } from './acTrpcContext'
3
3
 
4
4
  export const t = initTRPC.context<AcTrpcContext>().create({
5
- errorFormatter({ error, shape }: { error: unknown; shape: unknown }) {
5
+ errorFormatter({ error, shape }) {
6
+ // A 4xx is the caller's mistake (unknown path, bad input, no session). One
7
+ // plain line on stdout is enough; a stack on stderr would make log collectors
8
+ // file it as status:error next to real failures.
9
+ if (shape.data.httpStatus < 500) {
10
+ console.info(
11
+ `[tRPC] ${error.code} ${shape.data.path ?? '<no path>'}: ${error.message}`,
12
+ )
13
+ return shape
14
+ }
6
15
  console.error('[tRPC Error]', {
7
- path: (shape as { data?: { path?: string } }).data?.path,
8
- code: (error as { code?: string }).code,
9
- message: (error as { message?: string }).message,
10
- cause: (error as { cause?: unknown }).cause,
11
- stack: (error as { stack?: string }).stack,
16
+ path: shape.data.path,
17
+ code: error.code,
18
+ message: error.message,
19
+ cause: error.cause,
20
+ stack: error.stack,
12
21
  })
13
22
  return shape
14
23
  },