Skip to content

Commit 37f50ef

Browse files
committed
Respect Rails action toJSON bodies
1 parent 77c0098 commit 37f50ef

2 files changed

Lines changed: 26 additions & 32 deletions

File tree

packages/react-on-rails/src/railsAction.ts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const warnOnImplicitBodyWithDynamicPath = (): void => {
147147
);
148148
};
149149

150-
const nonJsonBodyTypeName = (requestBody: unknown, seenObjects = new WeakSet()): string | null => {
150+
const nonJsonBodyTypeName = (requestBody: unknown): string | null => {
151151
if (requestBody === undefined) {
152152
return 'undefined';
153153
}
@@ -197,21 +197,6 @@ const nonJsonBodyTypeName = (requestBody: unknown, seenObjects = new WeakSet()):
197197
return 'Promise';
198198
}
199199

200-
if (seenObjects.has(requestBody)) {
201-
return null;
202-
}
203-
seenObjects.add(requestBody);
204-
205-
const nestedValues = Array.isArray(requestBody)
206-
? requestBody
207-
: Object.values(requestBody as Record<string, unknown>);
208-
for (const nestedValue of nestedValues) {
209-
const nestedTypeName = nonJsonBodyTypeName(nestedValue, seenObjects);
210-
if (nestedTypeName !== null) {
211-
return nestedTypeName;
212-
}
213-
}
214-
215200
return null;
216201
};
217202

@@ -221,19 +206,6 @@ const jsonBodyTypeError = (bodyTypeName: string): TypeError =>
221206
'Return a plain JSON value, null, or undefined instead.',
222207
);
223208

224-
const assertJsonBodyValue = (requestBody: unknown, hasJsonBody: boolean): void => {
225-
if (!hasJsonBody) {
226-
return;
227-
}
228-
229-
const bodyTypeName = nonJsonBodyTypeName(requestBody);
230-
if (bodyTypeName === null) {
231-
return;
232-
}
233-
234-
throw jsonBodyTypeError(bodyTypeName);
235-
};
236-
237209
const stringifyJsonBody = (requestBody: unknown): string => {
238210
try {
239211
const serializedBody = JSON.stringify(requestBody, (_key, value: unknown) => {
@@ -386,7 +358,7 @@ export function createRailsAction<TVariables = undefined, TResponse = unknown>(
386358
const shouldWarnOnDiscardedDeleteBody =
387359
!warnedOnDiscardedDeleteBody && requestBody !== undefined && requestBody !== null;
388360
const shouldWarnOnImplicitDynamicPathBody = !warnedOnImplicitDynamicPathBody && hasJsonBody;
389-
assertJsonBodyValue(requestBody, hasJsonBody);
361+
const serializedRequestBody = hasJsonBody ? stringifyJsonBody(requestBody) : undefined;
390362
if (shouldWarnOnDiscardedDeleteBody) {
391363
warnOnDiscardedDeleteBody(requestBody);
392364
warnedOnDiscardedDeleteBody = true;
@@ -409,7 +381,7 @@ export function createRailsAction<TVariables = undefined, TResponse = unknown>(
409381
resolveHeaders(options.headers, typedVariables),
410382
callOptionsValue(callOptions, 'headers'),
411383
),
412-
body: hasJsonBody ? stringifyJsonBody(requestBody) : undefined,
384+
body: serializedRequestBody,
413385
});
414386
} catch (fetchError) {
415387
warnOnPossibleRedirectFetchError(fetchError);

packages/react-on-rails/tests/railsAction.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,10 +662,32 @@ describe('createRailsAction', () => {
662662
body: ({ id }) => ({ project: { id } }),
663663
});
664664

665-
await expect(createProject({ id: BigInt(1) })).rejects.toThrow(/resolved to BigInt/);
665+
await expect(createProject({ id: BigInt(1) })).rejects.toThrow(/contains a BigInt value/);
666666
expect(fetchMock).not.toHaveBeenCalled();
667667
});
668668

669+
it('serializes custom toJSON values without inspecting internal non-JSON state', async () => {
670+
class Money {
671+
raw = new Map([['internal', true]]);
672+
673+
constructor(private readonly cents: number) {}
674+
675+
toJSON(): number {
676+
return this.cents / 100;
677+
}
678+
}
679+
680+
const createProject = createRailsAction<undefined, { ok: true }>({
681+
path: '/api/projects',
682+
body: () => ({ project: { budget: new Money(1234) } }),
683+
});
684+
685+
await expect(createProject()).resolves.toEqual({ project: { id: 1 } });
686+
687+
const [, init] = fetchMock.mock.calls[0];
688+
expect(init.body).toBe(JSON.stringify({ project: { budget: 12.34 } }));
689+
});
690+
669691
it('rejects non-JSON values returned from custom toJSON methods before fetch', async () => {
670692
const customJsonValue = (jsonValue: unknown): { toJSON: () => unknown } => {
671693
const value = {};

0 commit comments

Comments
 (0)