Skip to content

Commit 23f25ce

Browse files
riaschodana-gill
andauthored
feat(Supabase Node): Add support for database schema (#13339)
Co-authored-by: Dana <[email protected]> Co-authored-by: Dana Lee <[email protected]>
1 parent de03452 commit 23f25ce

File tree

4 files changed

+141
-8
lines changed

4 files changed

+141
-8
lines changed

packages/nodes-base/nodes/Supabase/GenericFunctions.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,36 @@ export async function supabaseApiRequest(
2828
serviceRole: string;
2929
}>('supabaseApi');
3030

31+
if (this.getNodeParameter('useCustomSchema', false)) {
32+
const schema = this.getNodeParameter('schema', 'public');
33+
if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
34+
headers['Content-Profile'] = schema;
35+
} else if (['GET', 'HEAD'].includes(method)) {
36+
headers['Accept-Profile'] = schema;
37+
}
38+
}
39+
3140
const options: IRequestOptions = {
3241
headers: {
3342
Prefer: 'return=representation',
3443
},
3544
method,
3645
qs,
3746
body,
38-
uri: uri || `${credentials.host}/rest/v1${resource}`,
47+
uri: uri ?? `${credentials.host}/rest/v1${resource}`,
3948
json: true,
4049
};
50+
4151
try {
42-
if (Object.keys(headers).length !== 0) {
43-
options.headers = Object.assign({}, options.headers, headers);
44-
}
52+
options.headers = Object.assign({}, options.headers, headers);
4553
if (Object.keys(body).length === 0) {
4654
delete options.body;
4755
}
4856
return await this.helpers.requestWithAuthentication.call(this, 'supabaseApi', options);
4957
} catch (error) {
58+
if (error.description) {
59+
error.message = `${error.message}: ${error.description}`;
60+
}
5061
throw new NodeApiError(this.getNode(), error as JsonObject);
5162
}
5263
}

packages/nodes-base/nodes/Supabase/RowDescription.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const rowFields: INodeProperties[] = [
6060
description:
6161
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
6262
typeOptions: {
63+
loadOptionsDependsOn: ['useCustomSchema', 'schema'],
6364
loadOptionsMethod: 'getTables',
6465
},
6566
required: true,

packages/nodes-base/nodes/Supabase/Supabase.node.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ export class Supabase implements INodeType {
5151
},
5252
],
5353
properties: [
54+
{
55+
displayName: 'Use Custom Schema',
56+
name: 'useCustomSchema',
57+
type: 'boolean',
58+
default: false,
59+
noDataExpression: true,
60+
description:
61+
'Whether to use a database schema different from the default "public" schema (requires schema exposure in the <a href="https://supabase.com/docs/guides/api/using-custom-schemas?queryGroups=language&language=curl#exposing-custom-schemas">Supabase API</a>)',
62+
},
63+
{
64+
displayName: 'Schema',
65+
name: 'schema',
66+
type: 'string',
67+
default: 'public',
68+
description: 'Name of database schema to use for table',
69+
noDataExpression: false,
70+
displayOptions: { show: { useCustomSchema: [true] } },
71+
},
5472
{
5573
displayName: 'Resource',
5674
name: 'resource',

packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,33 @@ import { Supabase } from '../Supabase.node';
1414

1515
describe('Test Supabase Node', () => {
1616
const node = new Supabase();
17-
1817
const input = [{ json: {} }];
18+
const mockRequestWithAuthentication = jest.fn().mockResolvedValue([]);
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
});
1923

2024
const createMockExecuteFunction = (
2125
nodeParameters: IDataObject,
2226
continueOnFail: boolean = false,
2327
) => {
2428
const fakeExecuteFunction = {
29+
getCredentials: jest.fn().mockResolvedValue({
30+
host: 'https://api.supabase.io',
31+
serviceRole: 'service_role',
32+
}),
2533
getNodeParameter(
2634
parameterName: string,
2735
itemIndex: number,
2836
fallbackValue?: IDataObject | undefined,
2937
options?: IGetNodeParameterOptions | undefined,
3038
) {
3139
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
32-
3340
const parameterValue = get(nodeParameters, parameter, fallbackValue);
34-
3541
if ((parameterValue as IDataObject)?.nodeOperationError) {
3642
throw new NodeOperationError(mock(), 'Get Options Error', { itemIndex });
3743
}
38-
3944
return parameterValue;
4045
},
4146
getNode() {
@@ -44,6 +49,7 @@ describe('Test Supabase Node', () => {
4449
continueOnFail: () => continueOnFail,
4550
getInputData: () => input,
4651
helpers: {
52+
requestWithAuthentication: mockRequestWithAuthentication,
4753
constructExecutionMetaData: (
4854
_inputData: INodeExecutionData[],
4955
_options: { itemData: IPairedItemData | IPairedItemData[] },
@@ -95,5 +101,102 @@ describe('Test Supabase Node', () => {
95101
offset: 0,
96102
},
97103
);
104+
105+
supabaseApiRequest.mockRestore();
106+
});
107+
108+
it('should not set schema headers if no custom schema is used', async () => {
109+
const fakeExecuteFunction = createMockExecuteFunction({
110+
resource: 'row',
111+
operation: 'getAll',
112+
returnAll: true,
113+
useCustomSchema: false,
114+
schema: 'public',
115+
tableId: 'my_table',
116+
});
117+
118+
await node.execute.call(fakeExecuteFunction);
119+
120+
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
121+
'supabaseApi',
122+
expect.objectContaining({
123+
method: 'GET',
124+
headers: expect.objectContaining({
125+
Prefer: 'return=representation',
126+
}),
127+
uri: 'https://api.supabase.io/rest/v1/my_table',
128+
}),
129+
);
130+
});
131+
132+
it('should set the schema headers for GET calls if custom schema is used', async () => {
133+
const fakeExecuteFunction = createMockExecuteFunction({
134+
resource: 'row',
135+
operation: 'getAll',
136+
returnAll: true,
137+
useCustomSchema: true,
138+
schema: 'custom_schema',
139+
tableId: 'my_table',
140+
});
141+
142+
await node.execute.call(fakeExecuteFunction);
143+
144+
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
145+
'supabaseApi',
146+
expect.objectContaining({
147+
method: 'GET',
148+
headers: expect.objectContaining({
149+
'Accept-Profile': 'custom_schema',
150+
Prefer: 'return=representation',
151+
}),
152+
uri: 'https://api.supabase.io/rest/v1/my_table',
153+
}),
154+
);
155+
});
156+
157+
it('should set the schema headers for POST calls if custom schema is used', async () => {
158+
const fakeExecuteFunction = createMockExecuteFunction({
159+
resource: 'row',
160+
operation: 'create',
161+
returnAll: true,
162+
useCustomSchema: true,
163+
schema: 'custom_schema',
164+
tableId: 'my_table',
165+
});
166+
167+
await node.execute.call(fakeExecuteFunction);
168+
169+
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
170+
'supabaseApi',
171+
expect.objectContaining({
172+
method: 'POST',
173+
headers: expect.objectContaining({
174+
'Content-Profile': 'custom_schema',
175+
Prefer: 'return=representation',
176+
}),
177+
uri: 'https://api.supabase.io/rest/v1/my_table',
178+
}),
179+
);
180+
});
181+
182+
it('should show descriptive message when error is caught', async () => {
183+
const fakeExecuteFunction = createMockExecuteFunction({
184+
resource: 'row',
185+
operation: 'create',
186+
returnAll: true,
187+
useCustomSchema: true,
188+
schema: '',
189+
tableId: 'my_table',
190+
});
191+
192+
fakeExecuteFunction.helpers.requestWithAuthentication = jest.fn().mockRejectedValue({
193+
description: 'Something when wrong',
194+
message: 'error',
195+
});
196+
197+
await expect(node.execute.call(fakeExecuteFunction)).rejects.toHaveProperty(
198+
'message',
199+
'error: Something when wrong',
200+
);
98201
});
99202
});

0 commit comments

Comments
 (0)