feat(supabase): add returnData toggle for RLS compatibility

- Added returnData option to create operation
- Updated tests for true/false and non-boolean cases
- Preserved original getAll test
This commit is contained in:
Ryan Sweeney 2025-02-25 03:07:59 -06:00
parent 37d4b00e3f
commit 83fe8ac118
4 changed files with 129 additions and 9 deletions

View file

@ -22,6 +22,7 @@ export async function supabaseApiRequest(
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
returnData: boolean = true,
) {
const credentials = await this.getCredentials<{
host: string;
@ -29,9 +30,7 @@ export async function supabaseApiRequest(
}>('supabaseApi');
const options: IRequestOptions = {
headers: {
Prefer: 'return=representation',
},
headers: returnData ? { Prefer: 'return=representation' } : { Prefer: 'return=minimal' },
method,
qs,
body,

View file

@ -170,6 +170,19 @@ export const rowFields: INodeProperties[] = [
},
],
},
{
displayName: 'Return Inserted Data',
name: 'returnData',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
},
},
default: true,
description: 'Whether to return the inserted data. Disable if RLS policies restrict SELECT.',
},
/* -------------------------------------------------------------------------- */
/* row:delete */
/* -------------------------------------------------------------------------- */

View file

@ -132,6 +132,7 @@ export class Supabase implements INodeType {
if (operation === 'create') {
const records: IDataObject[] = [];
const returnDataParam = this.getNodeParameter('returnData', 0, true) as boolean;
for (let i = 0; i < length; i++) {
const record: IDataObject = {};
@ -159,12 +160,17 @@ export class Supabase implements INodeType {
const endpoint = `/${tableId}`;
try {
const createdRows: IDataObject[] = await supabaseApiRequest.call(
const response = await supabaseApiRequest.call(
this,
'POST',
endpoint,
records,
{},
undefined,
{},
returnDataParam,
);
const createdRows = returnDataParam ? (response as IDataObject[]) : [];
createdRows.forEach((row, i) => {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(row),
@ -172,6 +178,13 @@ export class Supabase implements INodeType {
);
returnData.push(...executionData);
});
if (!returnDataParam) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true, message: 'Row inserted' }),
{ itemData: mapPairedItemsFrom(records) },
);
returnData.push(...executionData);
}
} catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData(

View file

@ -15,7 +15,8 @@ import { Supabase } from '../Supabase.node';
describe('Test Supabase Node', () => {
const node = new Supabase();
const input = [{ json: {} }];
// Input for create tests
const input = [{ json: { email: 'test@example.com', phone: '+1234567890' } }];
const createMockExecuteFunction = (
nodeParameters: IDataObject,
@ -29,7 +30,6 @@ describe('Test Supabase Node', () => {
options?: IGetNodeParameterOptions | undefined,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
const parameterValue = get(nodeParameters, parameter, fallbackValue);
if ((parameterValue as IDataObject)?.nodeOperationError) {
@ -45,10 +45,11 @@ describe('Test Supabase Node', () => {
getInputData: () => input,
helpers: {
constructExecutionMetaData: (
_inputData: INodeExecutionData[],
inputData: INodeExecutionData[],
_options: { itemData: IPairedItemData | IPairedItemData[] },
) => [],
returnJsonArray: (_jsonData: IDataObject | IDataObject[]) => [],
) => inputData,
returnJsonArray: (jsonData: IDataObject | IDataObject[]) =>
Array.isArray(jsonData) ? jsonData.map((data) => ({ json: data })) : [{ json: jsonData }],
},
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
@ -96,4 +97,98 @@ describe('Test Supabase Node', () => {
},
);
});
describe('Create Operation', () => {
it('should create a row and return data when returnData is true', async () => {
const supabaseApiRequest = jest
.spyOn(utils, 'supabaseApiRequest')
.mockResolvedValue([{ email: 'test@example.com', phone: '+1234567890' }]);
const fakeExecuteFunction = createMockExecuteFunction({
resource: 'row',
operation: 'create',
tableId: 'signups',
dataToSend: 'autoMapInputData',
inputsToIgnore: '',
returnData: true,
});
const result = await node.execute.call(fakeExecuteFunction);
expect(supabaseApiRequest).toHaveBeenCalledWith(
'POST',
'/signups',
[{ email: 'test@example.com', phone: '+1234567890' }],
{},
undefined,
{},
true,
);
expect(result[0][0].json).toEqual({
email: 'test@example.com',
phone: '+1234567890',
});
});
it('should create a row without returning data when returnData is false', async () => {
const supabaseApiRequest = jest.spyOn(utils, 'supabaseApiRequest').mockResolvedValue({});
const fakeExecuteFunction = createMockExecuteFunction({
resource: 'row',
operation: 'create',
tableId: 'signups',
dataToSend: 'autoMapInputData',
inputsToIgnore: '',
returnData: false,
});
const result = await node.execute.call(fakeExecuteFunction);
expect(supabaseApiRequest).toHaveBeenCalledWith(
'POST',
'/signups',
[{ email: 'test@example.com', phone: '+1234567890' }],
{},
undefined,
{},
false,
);
expect(result[0][0].json).toEqual({
success: true,
message: 'Row inserted',
});
});
it('should coerce non-boolean returnData to boolean and proceed', async () => {
const supabaseApiRequest = jest
.spyOn(utils, 'supabaseApiRequest')
.mockResolvedValue([{ email: 'test@example.com', phone: '+1234567890' }]);
const fakeExecuteFunction = createMockExecuteFunction({
resource: 'row',
operation: 'create',
tableId: 'signups',
dataToSend: 'autoMapInputData',
inputsToIgnore: '',
returnData: 'hello', // Non-boolean expression result
});
const result = await node.execute.call(fakeExecuteFunction);
// 'hello' is truthy, so expect returnData to be treated as true
expect(supabaseApiRequest).toHaveBeenCalledWith(
'POST',
'/signups',
[{ email: 'test@example.com', phone: '+1234567890' }],
{},
undefined,
{},
'hello', // Passed as-is, but coerced to true in JS
);
expect(result[0][0].json).toEqual({
email: 'test@example.com',
phone: '+1234567890',
});
});
});
});