fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(core): add JSON serialization utilities for query keys

+245
+111
packages/openapi-ts/src/plugins/@hey-api/client-core/__tests__/queryKeySerializer.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { 4 + queryKeyJsonReplacer, 5 + serializeQueryKeyValue, 6 + stringifyToJsonValue, 7 + } from '../bundle/queryKeySerializer'; 8 + 9 + describe('query key helpers', () => { 10 + describe('queryKeyJsonReplacer', () => { 11 + it('converts bigint to string', () => { 12 + expect(queryKeyJsonReplacer('value', 1n)).toBe('1'); 13 + }); 14 + 15 + it('converts Date to ISO string', () => { 16 + const date = new Date('2025-01-01T12:34:56.000Z'); 17 + expect(queryKeyJsonReplacer('value', date)).toBe(date.toISOString()); 18 + }); 19 + 20 + it('drops unsupported values', () => { 21 + expect(queryKeyJsonReplacer('value', undefined)).toBeUndefined(); 22 + expect(queryKeyJsonReplacer('value', () => {})).toBeUndefined(); 23 + expect(queryKeyJsonReplacer('value', Symbol('s'))).toBeUndefined(); 24 + }); 25 + }); 26 + 27 + describe('stringifyToJsonValue', () => { 28 + it('produces JSON-safe structures', () => { 29 + const input = { 30 + a: 1n, 31 + b: new Date('2025-01-02T00:00:00.000Z'), 32 + c: () => {}, 33 + d: undefined, 34 + }; 35 + 36 + expect(stringifyToJsonValue(input)).toEqual({ 37 + a: '1', 38 + b: '2025-01-02T00:00:00.000Z', 39 + }); 40 + }); 41 + 42 + it('returns undefined when value cannot be stringified', () => { 43 + const circular: { self?: unknown } = {}; 44 + circular.self = circular; 45 + 46 + expect(stringifyToJsonValue(circular)).toBeUndefined(); 47 + }); 48 + }); 49 + 50 + describe('serializeQueryKeyValue', () => { 51 + it('handles primitives and null', () => { 52 + expect(serializeQueryKeyValue(null)).toBeNull(); 53 + expect(serializeQueryKeyValue('')).toBe(''); 54 + expect(serializeQueryKeyValue(0)).toBe(0); 55 + expect(serializeQueryKeyValue(false)).toBe(false); 56 + }); 57 + 58 + it('converts special primitives', () => { 59 + expect(serializeQueryKeyValue(1n)).toBe('1'); 60 + 61 + const date = new Date('2025-03-04T05:06:07.000Z'); 62 + expect(serializeQueryKeyValue(date)).toBe(date.toISOString()); 63 + }); 64 + 65 + it('normalizes arrays', () => { 66 + const date = new Date('2025-03-04T05:06:07.000Z'); 67 + expect( 68 + serializeQueryKeyValue([1n, date, undefined, () => {}, 'ok']), 69 + ).toEqual(['1', date.toISOString(), null, null, 'ok']); 70 + }); 71 + 72 + it('normalizes plain objects', () => { 73 + const result = serializeQueryKeyValue({ 74 + a: 1, 75 + b: 1n, 76 + c: new Date('2025-06-07T08:09:10.000Z'), 77 + d: undefined, 78 + e: () => {}, 79 + }); 80 + 81 + expect(result).toEqual({ 82 + a: 1, 83 + b: '1', 84 + c: '2025-06-07T08:09:10.000Z', 85 + }); 86 + }); 87 + 88 + it('supports objects with null prototype', () => { 89 + const value = Object.assign(Object.create(null), { a: 1 }); 90 + expect(serializeQueryKeyValue(value)).toEqual({ a: 1 }); 91 + }); 92 + 93 + it('serializes URLSearchParams as JSON object', () => { 94 + const params = new URLSearchParams(); 95 + params.append('a', '1'); 96 + params.append('a', '2'); 97 + params.append('b', 'foo'); 98 + 99 + expect(serializeQueryKeyValue(params)).toEqual({ 100 + a: ['1', '2'], 101 + b: 'foo', 102 + }); 103 + }); 104 + 105 + it('rejects unsupported structures', () => { 106 + expect(serializeQueryKeyValue(new Map())).toBeUndefined(); 107 + expect(serializeQueryKeyValue(new Set())).toBeUndefined(); 108 + expect(serializeQueryKeyValue(new (class Example {})())).toBeUndefined(); 109 + }); 110 + }); 111 + });
+134
packages/openapi-ts/src/plugins/@hey-api/client-core/bundle/queryKeySerializer.ts
··· 1 + /** 2 + * JSON-friendly union that mirrors what Pinia Colada can hash. 3 + */ 4 + export type JsonValue = 5 + | null 6 + | string 7 + | number 8 + | boolean 9 + | JsonValue[] 10 + | { [key: string]: JsonValue }; 11 + 12 + /** 13 + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. 14 + */ 15 + export const queryKeyJsonReplacer = (_key: string, value: unknown) => { 16 + if ( 17 + value === undefined || 18 + typeof value === 'function' || 19 + typeof value === 'symbol' 20 + ) { 21 + return undefined; 22 + } 23 + if (typeof value === 'bigint') { 24 + return value.toString(); 25 + } 26 + if (value instanceof Date) { 27 + return value.toISOString(); 28 + } 29 + return value; 30 + }; 31 + 32 + /** 33 + * Safely stringifies a value and parses it back into a JsonValue. 34 + */ 35 + export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { 36 + try { 37 + const json = JSON.stringify(input, queryKeyJsonReplacer); 38 + if (json === undefined) { 39 + return undefined; 40 + } 41 + return JSON.parse(json) as JsonValue; 42 + } catch { 43 + return undefined; 44 + } 45 + }; 46 + 47 + /** 48 + * Detects plain objects (including objects with a null prototype). 49 + */ 50 + const isPlainObject = (value: unknown): value is Record<string, unknown> => { 51 + if (value === null || typeof value !== 'object') { 52 + return false; 53 + } 54 + const prototype = Object.getPrototypeOf(value as object); 55 + return prototype === Object.prototype || prototype === null; 56 + }; 57 + 58 + /** 59 + * Turns URLSearchParams into a sorted JSON object for deterministic keys. 60 + */ 61 + const serializeSearchParams = (params: URLSearchParams): JsonValue => { 62 + const entries = Array.from(params.entries()).sort(([a], [b]) => 63 + a.localeCompare(b), 64 + ); 65 + const result: Record<string, JsonValue> = {}; 66 + 67 + for (const [key, value] of entries) { 68 + const existing = result[key]; 69 + if (existing === undefined) { 70 + result[key] = value; 71 + continue; 72 + } 73 + 74 + if (Array.isArray(existing)) { 75 + (existing as string[]).push(value); 76 + } else { 77 + result[key] = [existing, value]; 78 + } 79 + } 80 + 81 + return result; 82 + }; 83 + 84 + /** 85 + * Normalizes any accepted value into a JSON-friendly shape for query keys. 86 + */ 87 + export const serializeQueryKeyValue = ( 88 + value: unknown, 89 + ): JsonValue | undefined => { 90 + if (value === null) { 91 + return null; 92 + } 93 + 94 + if ( 95 + typeof value === 'string' || 96 + typeof value === 'number' || 97 + typeof value === 'boolean' 98 + ) { 99 + return value; 100 + } 101 + 102 + if ( 103 + value === undefined || 104 + typeof value === 'function' || 105 + typeof value === 'symbol' 106 + ) { 107 + return undefined; 108 + } 109 + 110 + if (typeof value === 'bigint') { 111 + return value.toString(); 112 + } 113 + 114 + if (value instanceof Date) { 115 + return value.toISOString(); 116 + } 117 + 118 + if (Array.isArray(value)) { 119 + return stringifyToJsonValue(value); 120 + } 121 + 122 + if ( 123 + typeof URLSearchParams !== 'undefined' && 124 + value instanceof URLSearchParams 125 + ) { 126 + return serializeSearchParams(value); 127 + } 128 + 129 + if (isPlainObject(value)) { 130 + return stringifyToJsonValue(value); 131 + } 132 + 133 + return undefined; 134 + };