mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(Supabase Node): Add support for database schema (#13339)
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com> Co-authored-by: Dana Lee <dana@n8n.io>
This commit is contained in:
@@ -28,6 +28,15 @@ export async function supabaseApiRequest(
|
|||||||
serviceRole: string;
|
serviceRole: string;
|
||||||
}>('supabaseApi');
|
}>('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 = {
|
const options: IRequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
Prefer: 'return=representation',
|
Prefer: 'return=representation',
|
||||||
@@ -35,18 +44,20 @@ export async function supabaseApiRequest(
|
|||||||
method,
|
method,
|
||||||
qs,
|
qs,
|
||||||
body,
|
body,
|
||||||
uri: uri || `${credentials.host}/rest/v1${resource}`,
|
uri: uri ?? `${credentials.host}/rest/v1${resource}`,
|
||||||
json: true,
|
json: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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) {
|
if (Object.keys(body).length === 0) {
|
||||||
delete options.body;
|
delete options.body;
|
||||||
}
|
}
|
||||||
return await this.helpers.requestWithAuthentication.call(this, 'supabaseApi', options);
|
return await this.helpers.requestWithAuthentication.call(this, 'supabaseApi', options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.description) {
|
||||||
|
error.message = `${error.message}: ${error.description}`;
|
||||||
|
}
|
||||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const rowFields: INodeProperties[] = [
|
|||||||
description:
|
description:
|
||||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: ['useCustomSchema', 'schema'],
|
||||||
loadOptionsMethod: 'getTables',
|
loadOptionsMethod: 'getTables',
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -51,6 +51,24 @@ export class Supabase implements INodeType {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
properties: [
|
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 <a href="https://supabase.com/docs/guides/api/using-custom-schemas?queryGroups=language&language=curl#exposing-custom-schemas">Supabase API</a>)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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',
|
displayName: 'Resource',
|
||||||
name: 'resource',
|
name: 'resource',
|
||||||
|
|||||||
@@ -14,14 +14,22 @@ import { Supabase } from '../Supabase.node';
|
|||||||
|
|
||||||
describe('Test Supabase Node', () => {
|
describe('Test Supabase Node', () => {
|
||||||
const node = new Supabase();
|
const node = new Supabase();
|
||||||
|
|
||||||
const input = [{ json: {} }];
|
const input = [{ json: {} }];
|
||||||
|
const mockRequestWithAuthentication = jest.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
const createMockExecuteFunction = (
|
const createMockExecuteFunction = (
|
||||||
nodeParameters: IDataObject,
|
nodeParameters: IDataObject,
|
||||||
continueOnFail: boolean = false,
|
continueOnFail: boolean = false,
|
||||||
) => {
|
) => {
|
||||||
const fakeExecuteFunction = {
|
const fakeExecuteFunction = {
|
||||||
|
getCredentials: jest.fn().mockResolvedValue({
|
||||||
|
host: 'https://api.supabase.io',
|
||||||
|
serviceRole: 'service_role',
|
||||||
|
}),
|
||||||
getNodeParameter(
|
getNodeParameter(
|
||||||
parameterName: string,
|
parameterName: string,
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
@@ -29,13 +37,10 @@ describe('Test Supabase Node', () => {
|
|||||||
options?: IGetNodeParameterOptions | undefined,
|
options?: IGetNodeParameterOptions | undefined,
|
||||||
) {
|
) {
|
||||||
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
|
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
|
||||||
|
|
||||||
const parameterValue = get(nodeParameters, parameter, fallbackValue);
|
const parameterValue = get(nodeParameters, parameter, fallbackValue);
|
||||||
|
|
||||||
if ((parameterValue as IDataObject)?.nodeOperationError) {
|
if ((parameterValue as IDataObject)?.nodeOperationError) {
|
||||||
throw new NodeOperationError(mock(), 'Get Options Error', { itemIndex });
|
throw new NodeOperationError(mock(), 'Get Options Error', { itemIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameterValue;
|
return parameterValue;
|
||||||
},
|
},
|
||||||
getNode() {
|
getNode() {
|
||||||
@@ -44,6 +49,7 @@ describe('Test Supabase Node', () => {
|
|||||||
continueOnFail: () => continueOnFail,
|
continueOnFail: () => continueOnFail,
|
||||||
getInputData: () => input,
|
getInputData: () => input,
|
||||||
helpers: {
|
helpers: {
|
||||||
|
requestWithAuthentication: mockRequestWithAuthentication,
|
||||||
constructExecutionMetaData: (
|
constructExecutionMetaData: (
|
||||||
_inputData: INodeExecutionData[],
|
_inputData: INodeExecutionData[],
|
||||||
_options: { itemData: IPairedItemData | IPairedItemData[] },
|
_options: { itemData: IPairedItemData | IPairedItemData[] },
|
||||||
@@ -95,5 +101,102 @@ describe('Test Supabase Node', () => {
|
|||||||
offset: 0,
|
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',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user