@reactionary/source 0.0.30 → 0.0.32
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/CLAUDE.md +11 -0
- package/core/package.json +1 -1
- package/core/src/cache/cache-evaluation.interface.ts +19 -0
- package/core/src/cache/cache.interface.ts +38 -0
- package/core/src/cache/noop-cache.ts +42 -0
- package/core/src/cache/redis-cache.ts +55 -22
- package/core/src/client/client-builder.ts +63 -0
- package/core/src/client/client.ts +27 -3
- package/core/src/decorators/trpc.decorators.ts +144 -0
- package/core/src/index.ts +6 -1
- package/core/src/providers/analytics.provider.ts +3 -6
- package/core/src/providers/base.provider.ts +13 -63
- package/core/src/providers/cart.provider.ts +10 -6
- package/core/src/providers/identity.provider.ts +8 -5
- package/core/src/providers/inventory.provider.ts +5 -6
- package/core/src/providers/price.provider.ts +6 -6
- package/core/src/providers/product.provider.ts +6 -6
- package/core/src/providers/search.provider.ts +6 -6
- package/core/src/schemas/mutations/base.mutation.ts +0 -1
- package/core/src/schemas/mutations/cart.mutation.ts +0 -6
- package/core/src/schemas/mutations/identity.mutation.ts +0 -5
- package/core/src/schemas/mutations/product.mutation.ts +0 -1
- package/core/src/schemas/queries/base.query.ts +0 -1
- package/core/src/schemas/queries/cart.query.ts +1 -3
- package/core/src/schemas/queries/identity.query.ts +1 -3
- package/core/src/schemas/queries/inventory.query.ts +0 -1
- package/core/src/schemas/queries/price.query.ts +0 -3
- package/core/src/schemas/queries/product.query.ts +2 -7
- package/core/src/schemas/queries/search.query.ts +0 -3
- package/examples/node/package.json +1 -5
- package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +97 -0
- package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +84 -0
- package/examples/node/src/basic/basic-node-setup.spec.ts +40 -0
- package/otel/src/index.ts +3 -0
- package/otel/src/trace-decorator.ts +246 -0
- package/package.json +2 -1
- package/providers/algolia/src/core/initialize.ts +11 -9
- package/providers/algolia/src/providers/product.provider.ts +44 -11
- package/providers/algolia/src/providers/search.provider.ts +47 -66
- package/providers/commercetools/src/core/client.ts +0 -1
- package/providers/commercetools/src/core/initialize.ts +28 -24
- package/providers/commercetools/src/providers/cart.provider.ts +58 -89
- package/providers/commercetools/src/providers/identity.provider.ts +34 -50
- package/providers/commercetools/src/providers/inventory.provider.ts +16 -38
- package/providers/commercetools/src/providers/price.provider.ts +30 -35
- package/providers/commercetools/src/providers/product.provider.ts +48 -38
- package/providers/commercetools/src/providers/search.provider.ts +32 -47
- package/providers/commercetools/src/schema/capabilities.schema.ts +1 -1
- package/providers/fake/package.json +1 -0
- package/providers/fake/src/core/initialize.ts +17 -14
- package/providers/fake/src/index.ts +4 -0
- package/providers/fake/src/providers/analytics.provider.ts +19 -0
- package/providers/fake/src/providers/cart.provider.ts +107 -0
- package/providers/fake/src/providers/identity.provider.ts +78 -67
- package/providers/fake/src/providers/inventory.provider.ts +54 -0
- package/providers/fake/src/providers/price.provider.ts +60 -0
- package/providers/fake/src/providers/product.provider.ts +53 -49
- package/providers/fake/src/providers/search.provider.ts +15 -33
- package/providers/posthog/src/core/initialize.ts +6 -4
- package/trpc/__mocks__/superjson.js +25 -0
- package/trpc/jest.config.ts +14 -0
- package/trpc/package.json +2 -1
- package/trpc/src/client.ts +176 -0
- package/trpc/src/index.ts +35 -62
- package/trpc/src/integration.spec.ts +216 -0
- package/trpc/src/server.ts +123 -0
- package/trpc/src/transparent-client.spec.ts +160 -0
- package/trpc/src/types.ts +142 -0
- package/trpc/tsconfig.json +3 -0
- package/trpc/tsconfig.lib.json +2 -1
- package/trpc/tsconfig.spec.json +15 -0
- package/tsconfig.base.json +0 -2
- package/core/src/cache/caching-strategy.ts +0 -25
- package/examples/angular/e2e/example.spec.ts +0 -9
- package/examples/angular/eslint.config.mjs +0 -41
- package/examples/angular/playwright.config.ts +0 -38
- package/examples/angular/project.json +0 -86
- package/examples/angular/public/favicon.ico +0 -0
- package/examples/angular/src/app/app.component.html +0 -6
- package/examples/angular/src/app/app.component.scss +0 -22
- package/examples/angular/src/app/app.component.ts +0 -14
- package/examples/angular/src/app/app.config.ts +0 -16
- package/examples/angular/src/app/app.routes.ts +0 -25
- package/examples/angular/src/app/cart/cart.component.html +0 -4
- package/examples/angular/src/app/cart/cart.component.scss +0 -14
- package/examples/angular/src/app/cart/cart.component.ts +0 -73
- package/examples/angular/src/app/identity/identity.component.html +0 -6
- package/examples/angular/src/app/identity/identity.component.scss +0 -18
- package/examples/angular/src/app/identity/identity.component.ts +0 -49
- package/examples/angular/src/app/product/product.component.html +0 -14
- package/examples/angular/src/app/product/product.component.scss +0 -11
- package/examples/angular/src/app/product/product.component.ts +0 -42
- package/examples/angular/src/app/search/search.component.html +0 -35
- package/examples/angular/src/app/search/search.component.scss +0 -129
- package/examples/angular/src/app/search/search.component.ts +0 -50
- package/examples/angular/src/app/services/product.service.ts +0 -35
- package/examples/angular/src/app/services/search.service.ts +0 -48
- package/examples/angular/src/app/services/trpc.client.ts +0 -27
- package/examples/angular/src/index.html +0 -13
- package/examples/angular/src/main.ts +0 -7
- package/examples/angular/src/styles.scss +0 -17
- package/examples/angular/src/test-setup.ts +0 -6
- package/examples/angular/tsconfig.app.json +0 -10
- package/examples/angular/tsconfig.editor.json +0 -6
- package/examples/angular/tsconfig.json +0 -32
- package/examples/node/src/initialize-algolia.spec.ts +0 -29
- package/examples/node/src/initialize-commercetools.spec.ts +0 -31
- package/examples/node/src/initialize-extended-providers.spec.ts +0 -38
- package/examples/node/src/initialize-mixed-providers.spec.ts +0 -36
- package/examples/node/src/providers/custom-algolia-product.provider.ts +0 -18
- package/examples/node/src/schemas/custom-product.schema.ts +0 -8
- package/examples/trpc-node/.env.example +0 -52
- package/examples/trpc-node/eslint.config.mjs +0 -3
- package/examples/trpc-node/project.json +0 -61
- package/examples/trpc-node/src/assets/.gitkeep +0 -0
- package/examples/trpc-node/src/main.ts +0 -59
- package/examples/trpc-node/src/router-instance.ts +0 -52
- package/examples/trpc-node/tsconfig.app.json +0 -9
- package/examples/trpc-node/tsconfig.json +0 -13
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { buildClient, NoOpCache, SessionSchema } from '@reactionary/core';
|
|
2
|
+
import { withFakeCapabilities } from '@reactionary/provider-fake';
|
|
3
|
+
import { createTRPCServerRouter, createTRPCContext, type TRPCRouterFromClient } from './server';
|
|
4
|
+
import { createTRPCClient, type TRPCClientFromRouter } from './client';
|
|
5
|
+
import type { TransparentClient } from './types';
|
|
6
|
+
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
|
|
7
|
+
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
|
|
8
|
+
import * as http from 'http';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Integration test that actually starts an HTTP server and makes real network calls
|
|
12
|
+
* This is the real test that verifies the TRPC transparent transport works end-to-end
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Polyfill fetch for Node.js
|
|
16
|
+
global.fetch = global.fetch || require('node-fetch');
|
|
17
|
+
|
|
18
|
+
// Create the server-side client
|
|
19
|
+
const serverClient = buildClient(
|
|
20
|
+
[
|
|
21
|
+
withFakeCapabilities(
|
|
22
|
+
{
|
|
23
|
+
jitter: { mean: 0, deviation: 0 },
|
|
24
|
+
},
|
|
25
|
+
{ search: true, product: true, identity: false }
|
|
26
|
+
),
|
|
27
|
+
],
|
|
28
|
+
{ cache: new NoOpCache() }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Create TRPC router from the client (do this at module level for type inference)
|
|
32
|
+
const router = createTRPCServerRouter(serverClient);
|
|
33
|
+
type AppRouter = typeof router;
|
|
34
|
+
|
|
35
|
+
describe('TRPC Integration Test - Real HTTP Server', () => {
|
|
36
|
+
let server: http.Server;
|
|
37
|
+
let serverPort: number;
|
|
38
|
+
let trpcProxyClient: ReturnType<typeof createTRPCProxyClient<AppRouter>>;
|
|
39
|
+
let transparentClient: TransparentClient<typeof serverClient>;
|
|
40
|
+
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
|
|
43
|
+
// Create TRPC HTTP handler
|
|
44
|
+
const handler = createHTTPHandler({
|
|
45
|
+
router,
|
|
46
|
+
createContext: createTRPCContext,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Start HTTP server with TRPC handler
|
|
50
|
+
server = http.createServer(handler);
|
|
51
|
+
|
|
52
|
+
// Find available port
|
|
53
|
+
serverPort = 3001;
|
|
54
|
+
await new Promise<void>((resolve, reject) => {
|
|
55
|
+
const tryPort = (port: number) => {
|
|
56
|
+
server.listen(port, (err?: Error) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
if (port < 3010) {
|
|
59
|
+
tryPort(port + 1);
|
|
60
|
+
} else {
|
|
61
|
+
reject(err);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
serverPort = port;
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
tryPort(serverPort);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Create real TRPC proxy client (no transformer for testing)
|
|
73
|
+
trpcProxyClient = createTRPCProxyClient<AppRouter>({
|
|
74
|
+
links: [
|
|
75
|
+
httpBatchLink({
|
|
76
|
+
url: `http://localhost:${serverPort}`,
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Create transparent client using the real implementation - now properly typed!
|
|
82
|
+
transparentClient = createTRPCClient<typeof serverClient>(trpcProxyClient);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterAll(async () => {
|
|
86
|
+
if (server) {
|
|
87
|
+
await new Promise<void>((resolve) => {
|
|
88
|
+
server.close(() => resolve());
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const session = SessionSchema.parse({
|
|
94
|
+
id: '1234567890',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('Product Provider via HTTP', () => {
|
|
98
|
+
it('should fetch product by slug through real HTTP calls', async () => {
|
|
99
|
+
const slug = 'integration-test-product';
|
|
100
|
+
|
|
101
|
+
// Get result from transparent client (through HTTP/TRPC)
|
|
102
|
+
const trpcResult = await transparentClient.product.getBySlug(
|
|
103
|
+
{ slug },
|
|
104
|
+
session
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Get result from server client (direct call)
|
|
108
|
+
const directResult = await serverClient.product.getBySlug(
|
|
109
|
+
{ slug },
|
|
110
|
+
session
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Results should have the same structure
|
|
114
|
+
expect(trpcResult).toBeDefined();
|
|
115
|
+
expect(trpcResult.slug).toBe(slug);
|
|
116
|
+
expect(trpcResult.name).toBeDefined();
|
|
117
|
+
expect(trpcResult.description).toBeDefined();
|
|
118
|
+
expect(trpcResult.image).toBeDefined();
|
|
119
|
+
|
|
120
|
+
// Both should have the same slug (faker uses seed for consistency)
|
|
121
|
+
expect(trpcResult.slug).toBe(directResult.slug);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should fetch product by id through real HTTP calls', async () => {
|
|
125
|
+
const productId = 'integration-test-id';
|
|
126
|
+
|
|
127
|
+
const trpcResult = await transparentClient.product.getById(
|
|
128
|
+
{ id: productId },
|
|
129
|
+
session
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const directResult = await serverClient.product.getById(
|
|
133
|
+
{ id: productId },
|
|
134
|
+
session
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(trpcResult).toBeDefined();
|
|
138
|
+
expect(trpcResult.identifier.key).toBe(productId);
|
|
139
|
+
expect(trpcResult.name).toBeDefined();
|
|
140
|
+
expect(trpcResult.description).toBeDefined();
|
|
141
|
+
|
|
142
|
+
// Should match direct call
|
|
143
|
+
expect(trpcResult.identifier?.key).toBe(directResult.identifier?.key);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Search Provider via HTTP', () => {
|
|
148
|
+
it('should perform search through real HTTP calls', async () => {
|
|
149
|
+
const searchTerm = 'integration test search';
|
|
150
|
+
|
|
151
|
+
const trpcResult = await transparentClient.search.queryByTerm(
|
|
152
|
+
{
|
|
153
|
+
search: {
|
|
154
|
+
term: searchTerm,
|
|
155
|
+
page: 0,
|
|
156
|
+
pageSize: 10,
|
|
157
|
+
facets: []
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
session
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const directResult = await serverClient.search.queryByTerm(
|
|
164
|
+
{
|
|
165
|
+
search: {
|
|
166
|
+
term: searchTerm,
|
|
167
|
+
page: 0,
|
|
168
|
+
pageSize: 10,
|
|
169
|
+
facets: []
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
session
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(trpcResult).toBeDefined();
|
|
176
|
+
expect(trpcResult.products).toBeDefined();
|
|
177
|
+
expect(Array.isArray(trpcResult.products)).toBe(true);
|
|
178
|
+
expect(trpcResult.facets).toBeDefined();
|
|
179
|
+
|
|
180
|
+
// Should match direct call structure
|
|
181
|
+
expect(trpcResult.products.length).toBe(directResult.products.length);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Network Error Handling', () => {
|
|
186
|
+
it('should handle HTTP errors gracefully', async () => {
|
|
187
|
+
// This should work normally first
|
|
188
|
+
const result = await transparentClient.product.getById(
|
|
189
|
+
{ id: 'test-error-handling' },
|
|
190
|
+
session
|
|
191
|
+
);
|
|
192
|
+
expect(result).toBeDefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('API Equivalence', () => {
|
|
197
|
+
it('should produce identical results for TRPC vs direct calls', async () => {
|
|
198
|
+
const testId = 'equivalence-test';
|
|
199
|
+
|
|
200
|
+
// Make same call through both paths
|
|
201
|
+
const [trpcResult, directResult] = await Promise.all([
|
|
202
|
+
transparentClient.product.getById(
|
|
203
|
+
{ id: testId },
|
|
204
|
+
session
|
|
205
|
+
),
|
|
206
|
+
serverClient.product.getById({ id: testId }, session)
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
// Results should be structurally equivalent
|
|
210
|
+
expect(trpcResult.identifier.key).toBe(directResult.identifier.key);
|
|
211
|
+
expect(trpcResult.name).toBe(directResult.name);
|
|
212
|
+
expect(trpcResult.slug).toBe(directResult.slug);
|
|
213
|
+
expect(trpcResult.description).toBe(directResult.description);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { initTRPC } from '@trpc/server';
|
|
2
|
+
import { Client, Session } from '@reactionary/core';
|
|
3
|
+
import superjson from 'superjson';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { createTRPCTracing } from '@reactionary/otel';
|
|
6
|
+
import {
|
|
7
|
+
introspectClient,
|
|
8
|
+
MethodInfo
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
// Initialize TRPC with context containing session (no transformer for testing)
|
|
12
|
+
const t = initTRPC.context<{ session?: Session }>().create();
|
|
13
|
+
|
|
14
|
+
export const router = t.router;
|
|
15
|
+
export const mergeRouters = t.mergeRouters;
|
|
16
|
+
|
|
17
|
+
// Apply tracing middleware
|
|
18
|
+
const baseProcedure = t.procedure.use(createTRPCTracing());
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a TRPC router from a built client instance
|
|
22
|
+
* This function introspects the client and automatically creates TRPC procedures
|
|
23
|
+
* for all provider methods while maintaining type safety
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const client = buildClient([
|
|
28
|
+
* withFakeCapabilities(config, { product: true, search: true })
|
|
29
|
+
* ]);
|
|
30
|
+
*
|
|
31
|
+
* const router = createTRPCServerRouter(client);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createTRPCServerRouter<T extends Partial<Client>>(client: T) {
|
|
35
|
+
const methods = introspectClient(client);
|
|
36
|
+
|
|
37
|
+
// Group methods by provider
|
|
38
|
+
const providerMethods = methods.reduce((acc, method) => {
|
|
39
|
+
if (!acc[method.providerName]) {
|
|
40
|
+
acc[method.providerName] = [];
|
|
41
|
+
}
|
|
42
|
+
acc[method.providerName].push(method);
|
|
43
|
+
return acc;
|
|
44
|
+
}, {} as Record<string, MethodInfo[]>);
|
|
45
|
+
|
|
46
|
+
// Build router structure
|
|
47
|
+
const routes: Record<string, any> = {};
|
|
48
|
+
|
|
49
|
+
for (const [providerName, providerMethodsList] of Object.entries(providerMethods)) {
|
|
50
|
+
const providerRoutes: Record<string, any> = {};
|
|
51
|
+
|
|
52
|
+
for (const methodInfo of providerMethodsList) {
|
|
53
|
+
const procedure = createProcedureForMethod(methodInfo);
|
|
54
|
+
providerRoutes[methodInfo.name] = procedure;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
routes[providerName] = t.router(providerRoutes);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return t.router(routes);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a TRPC procedure for a specific provider method
|
|
65
|
+
*/
|
|
66
|
+
function createProcedureForMethod(methodInfo: MethodInfo) {
|
|
67
|
+
// Create input schema - we use a flexible schema since we want to preserve
|
|
68
|
+
// the original method signatures without requiring schema definitions
|
|
69
|
+
const inputSchema = z.object({
|
|
70
|
+
payload: z.any(), // The actual payload from the provider method
|
|
71
|
+
session: z.any().optional(), // Session is optional in input since it might come from context
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const procedureWithInput = baseProcedure.input(inputSchema);
|
|
75
|
+
|
|
76
|
+
if (methodInfo.isQuery) {
|
|
77
|
+
return procedureWithInput.query(async ({ input, ctx }) => {
|
|
78
|
+
const session = input.session || ctx.session;
|
|
79
|
+
|
|
80
|
+
// Call the original provider method
|
|
81
|
+
if (session) {
|
|
82
|
+
return await methodInfo.method(input.payload, session);
|
|
83
|
+
} else {
|
|
84
|
+
// Some methods might not require session
|
|
85
|
+
return await methodInfo.method(input.payload);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} else if (methodInfo.isMutation) {
|
|
89
|
+
return procedureWithInput.mutation(async ({ input, ctx }) => {
|
|
90
|
+
const session = input.session || ctx.session;
|
|
91
|
+
|
|
92
|
+
// Call the original provider method
|
|
93
|
+
if (session) {
|
|
94
|
+
return await methodInfo.method(input.payload, session);
|
|
95
|
+
} else {
|
|
96
|
+
// Some methods might not require session
|
|
97
|
+
return await methodInfo.method(input.payload);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error(`Method ${methodInfo.name} is neither query nor mutation`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Type helper to extract the router type from a client
|
|
107
|
+
* This enables full type safety on the client side
|
|
108
|
+
*/
|
|
109
|
+
export type TRPCRouterFromClient<T extends Partial<Client>> = ReturnType<typeof createTRPCServerRouter<T>>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Context creator for TRPC server
|
|
113
|
+
* Override this to provide session from your authentication system
|
|
114
|
+
*/
|
|
115
|
+
export function createTRPCContext(_opts: { req?: any; res?: any }) {
|
|
116
|
+
// Default implementation - you should override this based on your auth system
|
|
117
|
+
return {
|
|
118
|
+
session: undefined as Session | undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create publicProcedure here to avoid circular imports
|
|
123
|
+
export const publicProcedure = baseProcedure;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { buildClient, NoOpCache, SessionSchema } from '@reactionary/core';
|
|
2
|
+
import { withFakeCapabilities } from '@reactionary/provider-fake';
|
|
3
|
+
import { createTRPCServerRouter, introspectClient } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test suite for TRPC transparent client functionality
|
|
7
|
+
* This tests the core introspection and router generation without HTTP setup
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Jest test framework is now available
|
|
11
|
+
|
|
12
|
+
// Create the server-side client using the same pattern as examples/node
|
|
13
|
+
const serverClient = buildClient(
|
|
14
|
+
[
|
|
15
|
+
withFakeCapabilities(
|
|
16
|
+
{
|
|
17
|
+
jitter: {
|
|
18
|
+
mean: 0,
|
|
19
|
+
deviation: 0,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{ search: true, product: true, identity: false }
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
{
|
|
26
|
+
cache: new NoOpCache(),
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Create TRPC router from the client
|
|
31
|
+
const router = createTRPCServerRouter(serverClient);
|
|
32
|
+
|
|
33
|
+
describe('TRPC Transparent Client Core Functionality', () => {
|
|
34
|
+
|
|
35
|
+
const session = SessionSchema.parse({
|
|
36
|
+
id: '1234567890',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Client Introspection', () => {
|
|
40
|
+
it('should correctly introspect client methods', () => {
|
|
41
|
+
const methods = introspectClient(serverClient);
|
|
42
|
+
|
|
43
|
+
expect(methods.length).toBeGreaterThan(0);
|
|
44
|
+
|
|
45
|
+
// Should find product methods
|
|
46
|
+
const productMethods = methods.filter(m => m.providerName === 'product');
|
|
47
|
+
expect(productMethods.length).toBeGreaterThan(0);
|
|
48
|
+
|
|
49
|
+
const getBySlugMethod = productMethods.find(m => m.name === 'getBySlug');
|
|
50
|
+
expect(getBySlugMethod).toBeDefined();
|
|
51
|
+
expect(getBySlugMethod!.isQuery).toBe(true);
|
|
52
|
+
expect(getBySlugMethod!.isMutation).toBe(false);
|
|
53
|
+
|
|
54
|
+
// Should find search methods
|
|
55
|
+
const searchMethods = methods.filter(m => m.providerName === 'search');
|
|
56
|
+
expect(searchMethods.length).toBeGreaterThan(0);
|
|
57
|
+
|
|
58
|
+
const queryByTermMethod = searchMethods.find(m => m.name === 'queryByTerm');
|
|
59
|
+
expect(queryByTermMethod).toBeDefined();
|
|
60
|
+
expect(queryByTermMethod!.isQuery).toBe(true);
|
|
61
|
+
|
|
62
|
+
// Note: Only testing enabled providers (product and search)
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Router Generation', () => {
|
|
67
|
+
it('should create TRPC router from client', () => {
|
|
68
|
+
expect(router).toBeDefined();
|
|
69
|
+
|
|
70
|
+
// Router should be an object (TRPC router)
|
|
71
|
+
expect(typeof router).toBe('object');
|
|
72
|
+
expect(router).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle provider methods correctly', () => {
|
|
76
|
+
const methods = introspectClient(serverClient);
|
|
77
|
+
const providerNames = [...new Set(methods.map(m => m.providerName))];
|
|
78
|
+
|
|
79
|
+
// Should have enabled providers
|
|
80
|
+
expect(providerNames).toContain('product');
|
|
81
|
+
expect(providerNames).toContain('search');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Method Classification', () => {
|
|
86
|
+
it('should correctly classify query methods', () => {
|
|
87
|
+
const methods = introspectClient(serverClient);
|
|
88
|
+
|
|
89
|
+
const queryMethods = methods.filter(m => m.isQuery);
|
|
90
|
+
const queryMethodNames = queryMethods.map(m => m.name);
|
|
91
|
+
|
|
92
|
+
// Should include get* methods from enabled providers
|
|
93
|
+
expect(queryMethodNames).toContain('getById');
|
|
94
|
+
expect(queryMethodNames).toContain('getBySlug');
|
|
95
|
+
|
|
96
|
+
// Should include query* methods
|
|
97
|
+
expect(queryMethodNames).toContain('queryByTerm');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should correctly classify mutation methods', () => {
|
|
101
|
+
const methods = introspectClient(serverClient);
|
|
102
|
+
|
|
103
|
+
const mutationMethods = methods.filter(m => m.isMutation);
|
|
104
|
+
const mutationMethodNames = mutationMethods.map(m => m.name);
|
|
105
|
+
|
|
106
|
+
// With current setup, only search and product are enabled
|
|
107
|
+
// No mutations expected from these providers
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Direct Client Functionality', () => {
|
|
112
|
+
it('should have working product provider', async () => {
|
|
113
|
+
const result = await serverClient.product.getBySlug(
|
|
114
|
+
{ slug: 'test-product' },
|
|
115
|
+
session
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(result).toBeDefined();
|
|
119
|
+
expect(result.slug).toBe('test-product');
|
|
120
|
+
expect(result.name).toBeDefined();
|
|
121
|
+
expect(result.description).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should have working search provider', async () => {
|
|
125
|
+
const result = await serverClient.search.queryByTerm(
|
|
126
|
+
{
|
|
127
|
+
search: {
|
|
128
|
+
term: 'test search',
|
|
129
|
+
page: 0,
|
|
130
|
+
pageSize: 10,
|
|
131
|
+
facets: []
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
session
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(result).toBeDefined();
|
|
138
|
+
expect(result.products).toBeDefined();
|
|
139
|
+
expect(result.facets).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Only testing enabled providers (product and search)
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('Type System Validation', () => {
|
|
146
|
+
it('should maintain provider interface structure', () => {
|
|
147
|
+
// Verify the client has expected enabled provider structure
|
|
148
|
+
expect(serverClient.product).toBeDefined();
|
|
149
|
+
expect(serverClient.search).toBeDefined();
|
|
150
|
+
|
|
151
|
+
// Verify methods exist
|
|
152
|
+
expect(typeof serverClient.product.getById).toBe('function');
|
|
153
|
+
expect(typeof serverClient.product.getBySlug).toBe('function');
|
|
154
|
+
expect(typeof serverClient.search.queryByTerm).toBe('function');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Export for potential use in other tests
|
|
160
|
+
export { serverClient, router };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseProvider,
|
|
3
|
+
Client,
|
|
4
|
+
Session
|
|
5
|
+
} from '@reactionary/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract method names from a provider that match TRPC patterns
|
|
9
|
+
*/
|
|
10
|
+
export type ProviderMethods<T> = T extends BaseProvider
|
|
11
|
+
? {
|
|
12
|
+
[K in keyof T]: T[K] extends (...args: any[]) => any
|
|
13
|
+
? K extends `get${string}` | `query${string}` | 'add' | 'remove' | 'changeQuantity' | 'login' | 'logout' | 'getSelf'
|
|
14
|
+
? K
|
|
15
|
+
: never
|
|
16
|
+
: never
|
|
17
|
+
}[keyof T]
|
|
18
|
+
: never;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract method signature from a provider method
|
|
22
|
+
*/
|
|
23
|
+
export type ProviderMethodSignature<T, K extends keyof T> =
|
|
24
|
+
T[K] extends (...args: infer Args) => infer Return
|
|
25
|
+
? (...args: Args) => Return
|
|
26
|
+
: never;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map all methods of all providers in a client
|
|
30
|
+
*/
|
|
31
|
+
export type ClientMethodMap<T extends Partial<Client>> = {
|
|
32
|
+
[K in keyof T]: T[K] extends BaseProvider
|
|
33
|
+
? {
|
|
34
|
+
[M in ProviderMethods<T[K]>]: ProviderMethodSignature<T[K], M>
|
|
35
|
+
}
|
|
36
|
+
: never;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create transparent client that only includes methods matching TRPC patterns
|
|
42
|
+
*/
|
|
43
|
+
export type TransparentClient<T extends Partial<Client>> = {
|
|
44
|
+
[K in keyof T]: T[K] extends BaseProvider
|
|
45
|
+
? {
|
|
46
|
+
[M in ProviderMethods<T[K]>]: ProviderMethodSignature<T[K], M>
|
|
47
|
+
}
|
|
48
|
+
: never;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract method information for TRPC procedure creation
|
|
53
|
+
*/
|
|
54
|
+
export interface MethodInfo {
|
|
55
|
+
name: string;
|
|
56
|
+
providerName: string;
|
|
57
|
+
isQuery: boolean;
|
|
58
|
+
isMutation: boolean;
|
|
59
|
+
method: (...args: any[]) => any;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Utility to determine if a method is a query or mutation based on naming convention
|
|
65
|
+
*/
|
|
66
|
+
export function isQueryMethod(methodName: string): boolean {
|
|
67
|
+
return methodName.startsWith('get') ||
|
|
68
|
+
methodName.startsWith('query') ||
|
|
69
|
+
methodName === 'getSelf';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isMutationMethod(methodName: string): boolean {
|
|
73
|
+
return !isQueryMethod(methodName) && (
|
|
74
|
+
methodName === 'add' ||
|
|
75
|
+
methodName === 'remove' ||
|
|
76
|
+
methodName === 'changeQuantity' ||
|
|
77
|
+
methodName === 'login' ||
|
|
78
|
+
methodName === 'logout'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Introspect a client instance to extract all provider methods matching naming patterns
|
|
84
|
+
*/
|
|
85
|
+
export function introspectClient<T extends Partial<Client>>(client: T): MethodInfo[] {
|
|
86
|
+
const methods: MethodInfo[] = [];
|
|
87
|
+
|
|
88
|
+
for (const [providerName, provider] of Object.entries(client)) {
|
|
89
|
+
if (provider instanceof BaseProvider) {
|
|
90
|
+
// Get all methods that match our naming patterns
|
|
91
|
+
for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(provider))) {
|
|
92
|
+
const method = (provider as any)[key];
|
|
93
|
+
if (typeof method === 'function' && key !== 'constructor') {
|
|
94
|
+
if (isQueryMethod(key) || isMutationMethod(key)) {
|
|
95
|
+
methods.push({
|
|
96
|
+
name: key,
|
|
97
|
+
providerName,
|
|
98
|
+
isQuery: isQueryMethod(key),
|
|
99
|
+
isMutation: isMutationMethod(key),
|
|
100
|
+
method: method.bind(provider)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return methods;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Type for TRPC procedure input - includes payload and session
|
|
113
|
+
*/
|
|
114
|
+
export interface TRPCMethodInput<TPayload = any> {
|
|
115
|
+
payload: TPayload;
|
|
116
|
+
session?: Session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Helper to extract payload type from a provider method
|
|
121
|
+
*/
|
|
122
|
+
export type ExtractPayloadType<T> = T extends (payload: infer P, session: Session) => any
|
|
123
|
+
? P
|
|
124
|
+
: T extends (payload: infer P) => any
|
|
125
|
+
? P
|
|
126
|
+
: never;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Helper to extract return type from a provider method
|
|
130
|
+
*/
|
|
131
|
+
export type ExtractReturnType<T> = T extends (...args: any[]) => infer R
|
|
132
|
+
? R
|
|
133
|
+
: never;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a TRPC-compatible method signature from a provider method
|
|
137
|
+
*/
|
|
138
|
+
export type TRPCMethodSignature<T> = T extends (payload: infer P, session: Session) => infer R
|
|
139
|
+
? (input: TRPCMethodInput<P>) => R
|
|
140
|
+
: T extends (payload: infer P) => infer R
|
|
141
|
+
? (input: TRPCMethodInput<P>) => R
|
|
142
|
+
: never;
|
package/trpc/tsconfig.json
CHANGED
package/trpc/tsconfig.lib.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"moduleResolution": "node10",
|
|
7
|
+
"types": ["jest", "node"]
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"jest.config.ts",
|
|
11
|
+
"src/**/*.test.ts",
|
|
12
|
+
"src/**/*.spec.ts",
|
|
13
|
+
"src/**/*.d.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|