typed-bridge 2.1.4 → 3.0.0

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.
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const __1 = require("..");
4
- // Global middleware — runs for every route (lowest specificity)
5
- (0, __1.createMiddleware)('*', async (req, res) => {
6
- return {
7
- context: {
8
- requestedAt: Date.now()
9
- }
10
- };
11
- });
12
- // Auth middleware — runs for all user.* routes
13
- (0, __1.createMiddleware)('user.*', async (req, res) => {
14
- const token = req.headers.authorization;
15
- if (!token || !token.startsWith('Bearer ')) {
16
- res.status(401).send('Unauthorized');
17
- return { next: false };
18
- }
19
- const userId = parseInt(token.split(' ')[1]) || 1;
20
- return {
21
- context: { userId }
22
- };
23
- });
24
- // Auth middleware — runs for all product.* routes
25
- (0, __1.createMiddleware)('product.*', async (req, res) => {
26
- const token = req.headers.authorization;
27
- if (!token || !token.startsWith('Bearer ')) {
28
- res.status(401).send('Unauthorized');
29
- return { next: false };
30
- }
31
- const userId = parseInt(token.split(' ')[1]) || 1;
32
- return {
33
- context: { userId }
34
- };
35
- });
36
- // Auth middleware — runs for all order.* routes
37
- (0, __1.createMiddleware)('order.*', async (req, res) => {
38
- const token = req.headers.authorization;
39
- if (!token || !token.startsWith('Bearer ')) {
40
- res.status(401).send('Unauthorized');
41
- return { next: false };
42
- }
43
- const userId = parseInt(token.split(' ')[1]) || 1;
44
- return {
45
- context: { userId }
46
- };
47
- });
48
- // Admin middleware — runs only for user.remove (highest specificity)
49
- (0, __1.createMiddleware)('user.remove', async (req, res) => {
50
- const isAdmin = req.headers['x-admin'] === 'true';
51
- if (!isAdmin) {
52
- res.status(403).send('Admin access required');
53
- return { next: false };
54
- }
55
- return {
56
- context: { isAdmin: true }
57
- };
58
- });
@@ -1,56 +0,0 @@
1
- ## Automatically sync bridge file from back-end to front-end
2
-
3
- ### Step 1: Host the bridge file
4
-
5
- ```ts
6
- // Back-end
7
-
8
- const app = createBridge(bridge, 8080, '/')
9
-
10
- app.use(express.static(path.join(__dirname, 'public')))
11
- ```
12
-
13
- ### Step 2: Configure generate command to build in public directory
14
-
15
- ```json
16
- {
17
- "scripts": {
18
- "gen-typed-bridge": "typed-bridge gen-typed-bridge --src ./src/bridge/index.ts --dest ./public/bridge.ts"
19
- }
20
- }
21
- ```
22
-
23
- ### Step 3: Setup typed-bridge types file clone script
24
-
25
- [Clone Kit](https://www.npmjs.com/package/clone-kit)
26
-
27
- ```bash
28
- npm install clone-kit
29
- ```
30
-
31
- Add clone-kit configuration file in front-end project `clone-kit.json`
32
-
33
- ```json
34
- {
35
- "files": [
36
- {
37
- "name": "Typed Bridge",
38
- "src": "https://www.my-server.com/bridge.ts",
39
- "dst": "src/bridge.ts"
40
- }
41
- ]
42
- }
43
- ```
44
-
45
- ### Step 4: Setup clone script to run before project start/build
46
-
47
- ```json
48
- {
49
- "scripts": {
50
- "start": "next start && clone-kit ./clone-kit.json",
51
- "build": "next build && clone-kit ./clone-kit.json"
52
- }
53
- }
54
- ```
55
-
56
- > It's completely safe to share Typed Bridge types file publicly as it only contains request & response types.
package/test/bridge.ts DELETED
@@ -1,283 +0,0 @@
1
- /* eslint-disable */
2
- interface User {
3
- id: number;
4
- name: string;
5
- email: string;
6
- createdAt: Date;
7
- }
8
- interface Product {
9
- id: number;
10
- name: string;
11
- price: number;
12
- createdAt: Date;
13
- }
14
- interface OrderItem {
15
- productId: number;
16
- quantity: number;
17
- price: number;
18
- notes?: string;
19
- discount: number | null;
20
- }
21
- interface Address {
22
- street: string;
23
- city: string;
24
- state: string;
25
- zip: string;
26
- country: string;
27
- }
28
- interface Order {
29
- id: number;
30
- customerId: number;
31
- status: string;
32
- total: number;
33
- items: OrderItem[];
34
- shippingAddress: Address;
35
- billingAddress: Address | null;
36
- isGift: boolean;
37
- giftMessage: string | null;
38
- createdAt: Date;
39
- updatedAt: Date | null;
40
- }
41
- declare const _default: {
42
- 'user.fetch': (args: {
43
- id: number;
44
- }) => Promise<{
45
- id: number;
46
- name: string;
47
- email: string;
48
- }>;
49
- 'user.create': (args: {
50
- name: string;
51
- email: string;
52
- }) => Promise<{
53
- id: number;
54
- name: string;
55
- email: string;
56
- createdAt: Date;
57
- }>;
58
- 'user.update': (args: {
59
- id: number;
60
- name?: string;
61
- email?: string;
62
- }) => Promise<User>;
63
- 'user.remove': (args: {
64
- id: number;
65
- }) => Promise<{
66
- success: boolean;
67
- }>;
68
- 'user.fetchAll': () => Promise<User[]>;
69
- 'product.fetch': (args: {
70
- id: number;
71
- }) => Promise<{
72
- id: number;
73
- name: string;
74
- price: number;
75
- createdAt: Date;
76
- }>;
77
- 'product.create': (args: {
78
- name: string;
79
- price: number;
80
- }) => Promise<{
81
- id: number;
82
- name: string;
83
- price: number;
84
- createdAt: Date;
85
- }>;
86
- 'product.list': () => Promise<Product[]>;
87
- 'order.create': (args: {
88
- customerId: number;
89
- items: {
90
- productId: number;
91
- quantity: number;
92
- price: number;
93
- notes?: string;
94
- discount: number | null;
95
- }[];
96
- shippingAddress: {
97
- street: string;
98
- city: string;
99
- state: string;
100
- zip: string;
101
- country: string;
102
- };
103
- billingAddress: {
104
- street: string;
105
- city: string;
106
- state: string;
107
- zip: string;
108
- country: string;
109
- } | null;
110
- couponCode?: string | null;
111
- scheduledDate?: Date;
112
- isGift: boolean;
113
- giftMessage: string | null;
114
- }) => Promise<{
115
- id: number;
116
- status: string;
117
- total: number;
118
- items: {
119
- productId: number;
120
- quantity: number;
121
- price: number;
122
- notes?: string;
123
- discount: number | null;
124
- }[];
125
- createdAt: Date;
126
- }>;
127
- 'order.fetch': (args: {
128
- id: number;
129
- }) => Promise<{
130
- id: number;
131
- customerId: number;
132
- status: string;
133
- total: number;
134
- items: {
135
- productId: number;
136
- quantity: number;
137
- price: number;
138
- notes?: string;
139
- discount: number | null;
140
- }[];
141
- shippingAddress: {
142
- street: string;
143
- city: string;
144
- state: string;
145
- zip: string;
146
- country: string;
147
- };
148
- billingAddress: {
149
- street: string;
150
- city: string;
151
- state: string;
152
- zip: string;
153
- country: string;
154
- } | null;
155
- isGift: boolean;
156
- giftMessage: string | null;
157
- createdAt: Date;
158
- updatedAt: Date | null;
159
- }>;
160
- 'order.update': (args: {
161
- id: number;
162
- status?: string;
163
- shippingAddress?: {
164
- street: string;
165
- city: string;
166
- state: string;
167
- zip: string;
168
- country: string;
169
- };
170
- giftMessage?: string | null;
171
- }) => Promise<{
172
- id: number;
173
- status: string;
174
- updatedAt: Date;
175
- }>;
176
- 'order.list': () => Promise<Order[]>;
177
- 'order.resolve': (args: {
178
- id: number;
179
- }) => Promise<{
180
- status: "found";
181
- order: {
182
- id: number;
183
- customerName: string;
184
- orderStatus: "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
185
- total: number;
186
- };
187
- } | {
188
- status: "not_found";
189
- }>;
190
- 'order.statusFilter': (args: {
191
- status: "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
192
- }) => Promise<{
193
- orders: {
194
- id: number;
195
- status: "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
196
- total: number;
197
- }[];
198
- }>;
199
- 'order.tag': (args: {
200
- orderId: number;
201
- tag: string | number;
202
- }) => Promise<{
203
- orderId: number;
204
- tag: string | number;
205
- appliedAt: Date;
206
- }>;
207
- 'order.primitives': (args: {
208
- key: string;
209
- }) => Promise<{
210
- str: string;
211
- num: number;
212
- bool: boolean;
213
- date: Date;
214
- nul: null;
215
- undef: undefined;
216
- unk: unknown;
217
- whatever: any;
218
- optStr?: string;
219
- nullStr: string | null;
220
- defStr: string;
221
- optNullStr?: string | null;
222
- nullOptStr: (string | undefined) | null;
223
- tags: string[];
224
- scores: number[] | null;
225
- optDates?: Date[];
226
- nested: {
227
- a: number;
228
- b?: string;
229
- c: {
230
- d: boolean;
231
- e: string[] | null;
232
- };
233
- };
234
- }>;
235
- };
236
-
237
- type typedBridgeConfig = {
238
- host: string
239
- headers: { [key: string]: string }
240
- onResponse: (res: Response) => void
241
- }
242
-
243
- export const typedBridgeConfig: typedBridgeConfig = {
244
- host: '',
245
- headers: { 'Content-Type': 'application/json' },
246
- onResponse: (res: Response) => {}
247
- }
248
-
249
- export const typedBridge = new Proxy(
250
- {},
251
- {
252
- get(_, methodName: string) {
253
- return async (args: any) => {
254
- const response = await fetch(
255
- typedBridgeConfig.host + (typedBridgeConfig.host.endsWith('/') ? '' : '/') + methodName,
256
- {
257
- method: 'POST',
258
- headers: {
259
- 'Content-Type': 'application/json',
260
- ...typedBridgeConfig.headers
261
- },
262
- body: JSON.stringify(args)
263
- }
264
- )
265
-
266
- typedBridgeConfig.onResponse(response)
267
-
268
- if (!response.ok) {
269
- const errorText = await response.text()
270
- console.error('REQ_FAILED', response.url, errorText)
271
- throw new Error(errorText)
272
- }
273
-
274
- return response.json().catch(error => {
275
- console.error('RES_NOT_JSON', response.url, error)
276
- throw new Error(error.message)
277
- })
278
- }
279
- }
280
- }
281
- ) as typeof _default
282
-
283
- export default typedBridge
package/test/index.ts DELETED
@@ -1,251 +0,0 @@
1
- import bridge, { typedBridgeConfig } from './bridge'
2
-
3
- typedBridgeConfig.host = 'http://localhost:8080/bridge'
4
- typedBridgeConfig.headers = {
5
- 'Content-Type': 'application/json',
6
- Authorization: 'Bearer 123'
7
- }
8
-
9
- let passed = 0
10
- let failed = 0
11
-
12
- const test = async (name: string, fn: () => Promise<void>) => {
13
- try {
14
- await fn()
15
- passed++
16
- console.log(` ✅ ${name}`)
17
- } catch (error: any) {
18
- failed++
19
- console.error(` ❌ ${name}: ${error.message}`)
20
- }
21
- }
22
-
23
- const assert = (condition: boolean, message: string) => {
24
- if (!condition) throw new Error(message)
25
- }
26
-
27
- const expectError = async (fn: () => Promise<any>, expectedSubstring?: string) => {
28
- try {
29
- await fn()
30
- throw new Error('Expected an error but none was thrown')
31
- } catch (error: any) {
32
- if (expectedSubstring && !error.message.includes(expectedSubstring)) {
33
- throw new Error(`Expected error containing "${expectedSubstring}", got: "${error.message}"`)
34
- }
35
- }
36
- }
37
-
38
- const main = async () => {
39
- console.log('\n--- User routes ---\n')
40
-
41
- await test('user.fetchAll returns users', async () => {
42
- const users = await bridge['user.fetchAll']()
43
- assert(users.length >= 3, `Expected at least 3 users, got ${users.length}`)
44
- assert(typeof users[0].id === 'number', `Expected numeric id`)
45
- assert(typeof users[0].name === 'string', `Expected string name`)
46
- })
47
-
48
- await test('user.fetch returns a user by id', async () => {
49
- const user = await bridge['user.fetch']({ id: 1 })
50
- assert(user.id === 1, `Expected id 1, got ${user.id}`)
51
- assert(typeof user.name === 'string', `Expected string name`)
52
- })
53
-
54
- await test('user.create adds a new user', async () => {
55
- const user = await bridge['user.create']({ name: 'Alice', email: 'alice@example.com' })
56
- assert(user.name === 'Alice', `Expected name "Alice", got "${user.name}"`)
57
- assert(user.email === 'alice@example.com', `Expected email "alice@example.com"`)
58
- assert(typeof user.id === 'number', `Expected numeric id`)
59
- })
60
-
61
- await test('user.update modifies an existing user', async () => {
62
- const user = await bridge['user.update']({ id: 1, name: 'Neil Updated' })
63
- assert(user.name === 'Neil Updated', `Expected name "Neil Updated", got "${user.name}"`)
64
- })
65
-
66
- await test('user.fetch not found throws error', async () => {
67
- await expectError(() => bridge['user.fetch']({ id: 999 }), 'User with ID 999 not found')
68
- })
69
-
70
- await test('user.fetch zod validation rejects id < 1', async () => {
71
- await expectError(() => bridge['user.fetch']({ id: 0 }))
72
- })
73
-
74
- await test('user.remove requires admin header', async () => {
75
- await expectError(() => bridge['user.remove']({ id: 999 }), 'Admin access required')
76
- })
77
-
78
- await test('user.remove works with admin header', async () => {
79
- const created = await bridge['user.create']({ name: 'ToRemove', email: 'remove@test.com' })
80
- typedBridgeConfig.headers['X-Admin'] = 'true'
81
- const result = await bridge['user.remove']({ id: created.id })
82
- delete typedBridgeConfig.headers['X-Admin']
83
- assert(result.success === true, `Expected success: true`)
84
- })
85
-
86
- console.log('\n--- Product routes ---\n')
87
-
88
- await test('product.list returns products', async () => {
89
- const products = await bridge['product.list']()
90
- assert(products.length >= 3, `Expected at least 3 products, got ${products.length}`)
91
- })
92
-
93
- await test('product.fetch returns a product by id', async () => {
94
- const product = await bridge['product.fetch']({ id: 1 })
95
- assert(product.id === 1, `Expected id 1, got ${product.id}`)
96
- assert(typeof product.name === 'string', `Expected string name`)
97
- })
98
-
99
- await test('product.create adds a new product', async () => {
100
- const product = await bridge['product.create']({ name: 'Mouse', price: 29 })
101
- assert(product.name === 'Mouse', `Expected "Mouse", got "${product.name}"`)
102
- assert(product.price === 29, `Expected price 29, got ${product.price}`)
103
- })
104
-
105
- await test('product.fetch not found throws error', async () => {
106
- await expectError(() => bridge['product.fetch']({ id: 999 }), 'Product with ID 999 not found')
107
- })
108
-
109
- await test('product.fetch zod validation rejects id < 1', async () => {
110
- await expectError(() => bridge['product.fetch']({ id: 0 }))
111
- })
112
-
113
- console.log('\n--- Order routes (complex Zod types) ---\n')
114
-
115
- let createdOrderId: number
116
-
117
- await test('order.create with nested objects, arrays, nullable, optional', async () => {
118
- const order = await bridge['order.create']({
119
- customerId: 1,
120
- items: [
121
- { productId: 1, quantity: 2, price: 999, notes: 'Gift wrap', discount: 50 },
122
- { productId: 2, quantity: 1, price: 79, discount: null }
123
- ],
124
- shippingAddress: { street: '123 Main St', city: 'NYC', state: 'NY', zip: '10001' },
125
- billingAddress: null,
126
- couponCode: 'SAVE10',
127
- isGift: true,
128
- giftMessage: 'Happy birthday!'
129
- })
130
- assert(typeof order.id === 'number', `Expected numeric id`)
131
- assert(order.status === 'pending', `Expected status "pending"`)
132
- assert(order.total === 2077, `Expected total 2077, got ${order.total}`)
133
- assert(order.items.length === 2, `Expected 2 items`)
134
- assert(order.items[0].notes === 'Gift wrap', `Expected notes`)
135
- assert(order.items[1].discount === null, `Expected null discount`)
136
- createdOrderId = order.id
137
- })
138
-
139
- await test('order.fetch returns full order with all nested types', async () => {
140
- const order = await bridge['order.fetch']({ id: createdOrderId })
141
- assert(order.customerId === 1, `Expected customerId 1`)
142
- assert(order.shippingAddress.city === 'NYC', `Expected city "NYC"`)
143
- assert(order.billingAddress === null, `Expected null billingAddress`)
144
- assert(order.isGift === true, `Expected isGift true`)
145
- assert(order.giftMessage === 'Happy birthday!', `Expected giftMessage`)
146
- assert(order.updatedAt === null, `Expected null updatedAt`)
147
- })
148
-
149
- await test('order.create with optional fields omitted', async () => {
150
- const order = await bridge['order.create']({
151
- customerId: 2,
152
- items: [{ productId: 3, quantity: 1, price: 349, discount: null }],
153
- shippingAddress: { street: '456 Oak Ave', city: 'LA', state: 'CA', zip: '90001' },
154
- billingAddress: { street: '789 Elm St', city: 'LA', state: 'CA', zip: '90002' },
155
- isGift: false,
156
- giftMessage: null
157
- })
158
- assert(order.status === 'pending', `Expected status "pending"`)
159
- assert(order.items[0].notes === undefined, `Expected undefined notes`)
160
- })
161
-
162
- await test('order.update with optional nested object', async () => {
163
- const result = await bridge['order.update']({
164
- id: createdOrderId,
165
- status: 'shipped',
166
- shippingAddress: { street: '999 New St', city: 'SF', state: 'CA', zip: '94101' }
167
- })
168
- assert(result.status === 'shipped', `Expected status "shipped"`)
169
- })
170
-
171
- await test('order.list returns created orders', async () => {
172
- const orders = await bridge['order.list']()
173
- assert(orders.length >= 2, `Expected at least 2 orders, got ${orders.length}`)
174
- })
175
-
176
- await test('order.fetch not found throws error', async () => {
177
- await expectError(() => bridge['order.fetch']({ id: 999 }), 'Order with ID 999 not found')
178
- })
179
-
180
- await test('order.fetch zod validation rejects id < 1', async () => {
181
- await expectError(() => bridge['order.fetch']({ id: 0 }))
182
- })
183
-
184
- await test('order.primitives returns all resolved primitive types', async () => {
185
- const res = await bridge['order.primitives']({ key: 'test' })
186
- assert(res.str === 'hello', `Expected str "hello"`)
187
- assert(res.num === 42, `Expected num 42`)
188
- assert(res.bool === true, `Expected bool true`)
189
- assert(typeof res.date === 'string', `Expected date serialized as string`)
190
- assert(res.nul === null, `Expected nul null`)
191
- assert(res.undef === undefined || !('undef' in res), `Expected undef undefined`)
192
- assert(res.whatever === 'literally anything', `Expected any value`)
193
- assert(res.optStr === 'present', `Expected optStr "present"`)
194
- assert(res.nullStr === null, `Expected nullStr null`)
195
- assert(res.defStr === 'default value', `Expected defStr "default value"`)
196
- assert(res.optNullStr === null, `Expected optNullStr null`)
197
- assert(Array.isArray(res.tags) && res.tags.length === 3, `Expected 3 tags`)
198
- assert(Array.isArray(res.scores) && res.scores.length === 3, `Expected 3 scores`)
199
- assert(Array.isArray(res.optDates) && res.optDates.length === 1, `Expected 1 optDate`)
200
- assert(res.nested.a === 1, `Expected nested.a === 1`)
201
- assert(res.nested.b === 'nested', `Expected nested.b === "nested"`)
202
- assert(res.nested.c.d === true, `Expected nested.c.d === true`)
203
- assert(Array.isArray(res.nested.c.e) && res.nested.c.e[0] === 'deep', `Expected nested.c.e[0] === "deep"`)
204
- })
205
-
206
- console.log('\n--- Middleware ---\n')
207
-
208
- await test('unauthorized request is rejected', async () => {
209
- const savedAuth = typedBridgeConfig.headers['Authorization']
210
- delete typedBridgeConfig.headers['Authorization']
211
-
212
- await expectError(() => bridge['user.fetch']({ id: 1 }), 'Unauthorized')
213
-
214
- typedBridgeConfig.headers['Authorization'] = savedAuth
215
- })
216
-
217
- console.log('\n--- Custom routes ---\n')
218
-
219
- await test('health endpoint returns status OK', async () => {
220
- const res = await fetch('http://localhost:8080/bridge/health')
221
- const data = await res.json() as any
222
- assert(data.status === 'OK', `Expected status "OK", got "${data.status}"`)
223
- })
224
-
225
- await test('custom /status route returns uptime', async () => {
226
- const res = await fetch('http://localhost:8080/status')
227
- const data = await res.json() as any
228
- assert(data.status === 'ok', `Expected status "ok", got "${data.status}"`)
229
- assert(typeof data.uptime === 'number', `Expected numeric uptime`)
230
- })
231
-
232
- console.log('\n--- onResponse callback ---\n')
233
-
234
- await test('onResponse is called on every request', async () => {
235
- let callbackFired = false
236
- typedBridgeConfig.onResponse = () => {
237
- callbackFired = true
238
- }
239
-
240
- await bridge['user.fetchAll']()
241
- assert(callbackFired, 'onResponse callback was not called')
242
-
243
- typedBridgeConfig.onResponse = () => {}
244
- })
245
-
246
- // Summary
247
- console.log(`\n--- Results: ${passed} passed, ${failed} failed ---\n`)
248
- process.exit(failed > 0 ? 1 : 0)
249
- }
250
-
251
- main()