diff --git a/packages/nodes-base/nodes/Supabase/GenericFunctions.ts b/packages/nodes-base/nodes/Supabase/GenericFunctions.ts
index 50df54863e..6f92f35c44 100644
--- a/packages/nodes-base/nodes/Supabase/GenericFunctions.ts
+++ b/packages/nodes-base/nodes/Supabase/GenericFunctions.ts
@@ -28,6 +28,15 @@ export async function supabaseApiRequest(
serviceRole: string;
}>('supabaseApi');
+ if (this.getNodeParameter('useCustomSchema', false)) {
+ const schema = this.getNodeParameter('schema', 'public');
+ if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
+ headers['Content-Profile'] = schema;
+ } else if (['GET', 'HEAD'].includes(method)) {
+ headers['Accept-Profile'] = schema;
+ }
+ }
+
const options: IRequestOptions = {
headers: {
Prefer: 'return=representation',
@@ -35,18 +44,20 @@ export async function supabaseApiRequest(
method,
qs,
body,
- uri: uri || `${credentials.host}/rest/v1${resource}`,
+ uri: uri ?? `${credentials.host}/rest/v1${resource}`,
json: true,
};
+
try {
- if (Object.keys(headers).length !== 0) {
- options.headers = Object.assign({}, options.headers, headers);
- }
+ options.headers = Object.assign({}, options.headers, headers);
if (Object.keys(body).length === 0) {
delete options.body;
}
return await this.helpers.requestWithAuthentication.call(this, 'supabaseApi', options);
} catch (error) {
+ if (error.description) {
+ error.message = `${error.message}: ${error.description}`;
+ }
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
diff --git a/packages/nodes-base/nodes/Supabase/RowDescription.ts b/packages/nodes-base/nodes/Supabase/RowDescription.ts
index eafe601752..3d894bcef1 100644
--- a/packages/nodes-base/nodes/Supabase/RowDescription.ts
+++ b/packages/nodes-base/nodes/Supabase/RowDescription.ts
@@ -60,6 +60,7 @@ export const rowFields: INodeProperties[] = [
description:
'Choose from the list, or specify an ID using an expression',
typeOptions: {
+ loadOptionsDependsOn: ['useCustomSchema', 'schema'],
loadOptionsMethod: 'getTables',
},
required: true,
diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts
index 4853a6ad3f..ea42dfca4b 100644
--- a/packages/nodes-base/nodes/Supabase/Supabase.node.ts
+++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts
@@ -51,6 +51,24 @@ export class Supabase implements INodeType {
},
],
properties: [
+ {
+ displayName: 'Use Custom Schema',
+ name: 'useCustomSchema',
+ type: 'boolean',
+ default: false,
+ noDataExpression: true,
+ description:
+ 'Whether to use a database schema different from the default "public" schema (requires schema exposure in the Supabase API)',
+ },
+ {
+ displayName: 'Schema',
+ name: 'schema',
+ type: 'string',
+ default: 'public',
+ description: 'Name of database schema to use for table',
+ noDataExpression: false,
+ displayOptions: { show: { useCustomSchema: [true] } },
+ },
{
displayName: 'Resource',
name: 'resource',
diff --git a/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts b/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
index f786426b3d..cba5ff173f 100644
--- a/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
+++ b/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
@@ -14,14 +14,22 @@ import { Supabase } from '../Supabase.node';
describe('Test Supabase Node', () => {
const node = new Supabase();
-
const input = [{ json: {} }];
+ const mockRequestWithAuthentication = jest.fn().mockResolvedValue([]);
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
const createMockExecuteFunction = (
nodeParameters: IDataObject,
continueOnFail: boolean = false,
) => {
const fakeExecuteFunction = {
+ getCredentials: jest.fn().mockResolvedValue({
+ host: 'https://api.supabase.io',
+ serviceRole: 'service_role',
+ }),
getNodeParameter(
parameterName: string,
itemIndex: number,
@@ -29,13 +37,10 @@ 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) {
throw new NodeOperationError(mock(), 'Get Options Error', { itemIndex });
}
-
return parameterValue;
},
getNode() {
@@ -44,6 +49,7 @@ describe('Test Supabase Node', () => {
continueOnFail: () => continueOnFail,
getInputData: () => input,
helpers: {
+ requestWithAuthentication: mockRequestWithAuthentication,
constructExecutionMetaData: (
_inputData: INodeExecutionData[],
_options: { itemData: IPairedItemData | IPairedItemData[] },
@@ -95,5 +101,102 @@ describe('Test Supabase Node', () => {
offset: 0,
},
);
+
+ supabaseApiRequest.mockRestore();
+ });
+
+ it('should not set schema headers if no custom schema is used', async () => {
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: true,
+ useCustomSchema: false,
+ schema: 'public',
+ tableId: 'my_table',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
+ 'supabaseApi',
+ expect.objectContaining({
+ method: 'GET',
+ headers: expect.objectContaining({
+ Prefer: 'return=representation',
+ }),
+ uri: 'https://api.supabase.io/rest/v1/my_table',
+ }),
+ );
+ });
+
+ it('should set the schema headers for GET calls if custom schema is used', async () => {
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: true,
+ useCustomSchema: true,
+ schema: 'custom_schema',
+ tableId: 'my_table',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
+ 'supabaseApi',
+ expect.objectContaining({
+ method: 'GET',
+ headers: expect.objectContaining({
+ 'Accept-Profile': 'custom_schema',
+ Prefer: 'return=representation',
+ }),
+ uri: 'https://api.supabase.io/rest/v1/my_table',
+ }),
+ );
+ });
+
+ it('should set the schema headers for POST calls if custom schema is used', async () => {
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'create',
+ returnAll: true,
+ useCustomSchema: true,
+ schema: 'custom_schema',
+ tableId: 'my_table',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
+ 'supabaseApi',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ 'Content-Profile': 'custom_schema',
+ Prefer: 'return=representation',
+ }),
+ uri: 'https://api.supabase.io/rest/v1/my_table',
+ }),
+ );
+ });
+
+ it('should show descriptive message when error is caught', async () => {
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'create',
+ returnAll: true,
+ useCustomSchema: true,
+ schema: '',
+ tableId: 'my_table',
+ });
+
+ fakeExecuteFunction.helpers.requestWithAuthentication = jest.fn().mockRejectedValue({
+ description: 'Something when wrong',
+ message: 'error',
+ });
+
+ await expect(node.execute.call(fakeExecuteFunction)).rejects.toHaveProperty(
+ 'message',
+ 'error: Something when wrong',
+ );
});
});