diff --git a/packages/nodes-base/credentials/StrapiApi.credentials.ts b/packages/nodes-base/credentials/StrapiApi.credentials.ts
new file mode 100644
index 0000000000..395bbc7f9d
--- /dev/null
+++ b/packages/nodes-base/credentials/StrapiApi.credentials.ts
@@ -0,0 +1,29 @@
+import {
+ ICredentialType,
+ NodePropertyTypes,
+} from 'n8n-workflow';
+
+export class StrapiApi implements ICredentialType {
+ name = 'strapiApi';
+ displayName = 'Strapi API';
+ properties = [
+ {
+ displayName: 'Email',
+ name: 'email',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ {
+ displayName: 'Password',
+ name: 'password',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ {
+ displayName: 'URL',
+ name: 'url',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ ];
+}
diff --git a/packages/nodes-base/nodes/Strapi/EntryDescription.ts b/packages/nodes-base/nodes/Strapi/EntryDescription.ts
new file mode 100644
index 0000000000..293304a929
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/EntryDescription.ts
@@ -0,0 +1,346 @@
+import {
+ INodeProperties,
+} from 'n8n-workflow';
+
+export const entryOperations = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create an entry',
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete an entry',
+ },
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Get an entry',
+ },
+ {
+ name: 'Get All',
+ value: 'getAll',
+ description: 'Get all entries',
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Update an entry',
+ },
+ ],
+ default: 'get',
+ description: 'The operation to perform.',
+ },
+] as INodeProperties[];
+
+export const entryFields = [
+ /* -------------------------------------------------------------------------- */
+ /* entry:create */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Content Type',
+ name: 'contentType',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'create',
+ ],
+ },
+ },
+ description: 'Name of the content type',
+ },
+ {
+ displayName: 'Columns',
+ name: 'columns',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'create',
+ ],
+ },
+ },
+ default: '',
+ placeholder: 'id,name,description',
+ description: 'Comma separated list of the properties which should used as columns for the new rows.',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* entry:delete */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Content Type',
+ name: 'contentType',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'delete',
+ ],
+ },
+ },
+ description: 'Name of the content type',
+ },
+ {
+ displayName: 'Entry ID',
+ name: 'entryId',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'delete',
+ ],
+ },
+ },
+ description: 'The ID of the entry to get.',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* entry:get */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Content Type',
+ name: 'contentType',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'get',
+ ],
+ },
+ },
+ description: 'Name of the content type',
+ },
+ {
+ displayName: 'Entry ID',
+ name: 'entryId',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'get',
+ ],
+ },
+ },
+ description: 'The ID of the entry to get.',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* entry:getAll */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Content Type',
+ name: 'contentType',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ description: 'Name of the content type',
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ default: false,
+ description: 'Returns a list of your user contacts.',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ typeOptions: {
+ minValue: 1,
+ maxValue: 100,
+ },
+ default: 50,
+ description: 'How many results to return.',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'Publication State',
+ name: 'publicationState',
+ type: 'options',
+ options: [
+ {
+ name: 'Live',
+ value: 'live',
+ },
+ {
+ name: 'Preview',
+ value: 'preview',
+ },
+ ],
+ default: '',
+ description: 'Only select entries matching the publication state provided.',
+ },
+ {
+ displayName: 'Sort Fields',
+ name: 'sort',
+ type: 'string',
+ typeOptions: {
+ multipleValues: true,
+ multipleValueButtonText: 'Add Sort Field',
+ },
+ default: '',
+ placeholder: 'name:asc',
+ description: `Name of the fields to sort the data by. By default will be sorted ascendingly.
+ To modify that behavior, you have to add the sort direction after the name of sort field preceded by a colon.
+ For example: name:asc`,
+ },
+ {
+ displayName: 'Where (JSON)',
+ name: 'where',
+ type: 'string',
+ typeOptions: {
+ alwaysOpenEditWindow: true,
+ },
+ default: '',
+ description: 'JSON query to filter the data. Info',
+ },
+ ],
+ },
+ /* -------------------------------------------------------------------------- */
+ /* entry:update */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Content Type',
+ name: 'contentType',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'update',
+ ],
+ },
+ },
+ description: 'Name of the content type',
+ },
+ {
+ displayName: 'Update Key',
+ name: 'updateKey',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'update',
+ ],
+ },
+ },
+ default: 'id',
+ required: true,
+ description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
+ },
+ {
+ displayName: 'Columns',
+ name: 'columns',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: [
+ 'entry',
+ ],
+ operation: [
+ 'update',
+ ],
+ },
+ },
+ default: '',
+ placeholder: 'id,name,description',
+ description: 'Comma separated list of the properties which should used as columns for the new rows.',
+ },
+] as INodeProperties[];
diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts
new file mode 100644
index 0000000000..6431ad0bd9
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts
@@ -0,0 +1,106 @@
+import {
+ OptionsWithUri,
+} from 'request';
+
+import {
+ IExecuteFunctions,
+ IHookFunctions,
+ ILoadOptionsFunctions,
+ IWebhookFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+} from 'n8n-workflow';
+
+export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any
+
+ const credentials = this.getCredentials('strapiApi') as IDataObject;
+
+ try {
+ const options: OptionsWithUri = {
+ headers: {
+ 'Authorization': `Bearer ${qs.jwt}`,
+ },
+ method,
+ body,
+ qs,
+ uri: uri || `${credentials.url}${resource}`,
+ json: true,
+ };
+ if (Object.keys(headers).length !== 0) {
+ options.headers = Object.assign({}, options.headers, headers);
+ }
+ if (Object.keys(body).length === 0) {
+ delete options.body;
+ }
+ delete qs.jwt;
+
+ //@ts-ignore
+ return await this.helpers?.request(options);
+ } catch (error) {
+ if (error.response && error.response.body && error.response.body.message) {
+
+ let messages = error.response.body.message;
+
+ if (Array.isArray(error.response.body.message)) {
+ messages = messages[0].messages.map((e: IDataObject) => e.message).join('|');
+ }
+ // Try to return the error prettier
+ throw new Error(
+ `Strapi error response [${error.statusCode}]: ${messages}`,
+ );
+ }
+ throw error;
+ }
+}
+
+export async function getToken(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions): Promise { // tslint:disable-line:no-any
+ const credentials = this.getCredentials('strapiApi') as IDataObject;
+
+ const options: OptionsWithUri = {
+ headers: {
+ 'content-type': `application/json`,
+ },
+ method: 'POST',
+ uri: `${credentials.url}/auth/local`,
+ body: {
+ identifier: credentials.email,
+ password: credentials.password,
+ },
+ json: true,
+ };
+
+ return this.helpers.request!(options);
+}
+
+export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any
+
+ const returnData: IDataObject[] = [];
+
+ let responseData;
+
+ query._limit = 20;
+
+ query._start = 0;
+
+ do {
+ responseData = await strapiApiRequest.call(this, method, resource, body, query);
+ query._start += query._limit;
+ returnData.push.apply(returnData, responseData);
+ } while (
+ responseData.length !== 0
+ );
+
+ return returnData;
+}
+
+export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
+ let result;
+ try {
+ result = JSON.parse(json!);
+ } catch (exception) {
+ result = undefined;
+ }
+ return result;
+}
diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts
new file mode 100644
index 0000000000..3b38ae7aad
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts
@@ -0,0 +1,191 @@
+import {
+ IExecuteFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+
+import {
+ getToken,
+ strapiApiRequest,
+ strapiApiRequestAllItems,
+ validateJSON,
+} from './GenericFunctions';
+
+import {
+ entryFields,
+ entryOperations,
+} from './EntryDescription';
+
+export class Strapi implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Strapi',
+ name: 'strapi',
+ icon: 'file:strapi.svg',
+ group: ['input'],
+ version: 1,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Consume Strapi API.',
+ defaults: {
+ name: 'Strapi',
+ color: '#725ed8',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'strapiApi',
+ required: true,
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ options: [
+ {
+ name: 'Entry',
+ value: 'entry',
+ },
+ ],
+ default: 'entry',
+ description: 'The resource to operate on.',
+ },
+ ...entryOperations,
+ ...entryFields,
+ ],
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const returnData: IDataObject[] = [];
+ const length = (items.length as unknown) as number;
+ const qs: IDataObject = {};
+ let responseData;
+ const resource = this.getNodeParameter('resource', 0) as string;
+ const operation = this.getNodeParameter('operation', 0) as string;
+
+ const { jwt } = await getToken.call(this);
+
+ qs.jwt = jwt;
+
+ if (resource === 'entry') {
+ if (operation === 'create') {
+ for (let i = 0; i < length; i++) {
+
+ const body: IDataObject = {};
+
+ const contentType = this.getNodeParameter('contentType', i) as string;
+
+ const columns = this.getNodeParameter('columns', i) as string;
+
+ const columnList = columns.split(',').map(column => column.trim());
+
+ for (const key of Object.keys(items[i].json)) {
+ if (columnList.includes(key)) {
+ body[key] = items[i].json[key];
+ }
+ }
+ responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs);
+
+ returnData.push(responseData);
+ }
+ }
+
+ if (operation === 'delete') {
+ for (let i = 0; i < length; i++) {
+ const contentType = this.getNodeParameter('contentType', i) as string;
+
+ const entryId = this.getNodeParameter('entryId', i) as string;
+
+ responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs);
+
+ returnData.push(responseData);
+ }
+ }
+
+ if (operation === 'getAll') {
+ for (let i = 0; i < length; i++) {
+
+ const returnAll = this.getNodeParameter('returnAll', i) as boolean;
+
+ const contentType = this.getNodeParameter('contentType', i) as string;
+
+ const options = this.getNodeParameter('options', i) as IDataObject;
+
+ if (options.sort && (options.sort as string[]).length !== 0) {
+ const sortFields = options.sort as string[];
+ qs._sort = sortFields.join(',');
+ }
+
+ if (options.where) {
+ const query = validateJSON(options.where as string);
+ if (query !== undefined) {
+ qs._where = query;
+ } else {
+ throw new Error('Query must be a valid JSON');
+ }
+ }
+
+ if (options.publicationState) {
+ qs._publicationState = options.publicationState as string;
+ }
+
+ if (returnAll) {
+ responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs);
+ } else {
+ qs._limit = this.getNodeParameter('limit', i) as number;
+
+ responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs);
+ }
+ returnData.push.apply(returnData, responseData);
+ }
+ }
+
+ if (operation === 'get') {
+ for (let i = 0; i < length; i++) {
+
+ const contentType = this.getNodeParameter('contentType', i) as string;
+
+ const entryId = this.getNodeParameter('entryId', i) as string;
+
+ responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs);
+
+ returnData.push(responseData);
+ }
+ }
+
+ if (operation === 'update') {
+ for (let i = 0; i < length; i++) {
+
+ const body: IDataObject = {};
+
+ const contentType = this.getNodeParameter('contentType', i) as string;
+
+ const columns = this.getNodeParameter('columns', i) as string;
+
+ const updateKey = this.getNodeParameter('updateKey', i) as string;
+
+ const columnList = columns.split(',').map(column => column.trim());
+
+ const entryId = items[i].json[updateKey];
+
+ for (const key of Object.keys(items[i].json)) {
+ if (columnList.includes(key)) {
+ body[key] = items[i].json[key];
+ }
+ }
+ responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs);
+
+ returnData.push(responseData);
+ }
+ }
+ }
+ return [this.helpers.returnJsonArray(returnData)];
+ }
+}
diff --git a/packages/nodes-base/nodes/Strapi/strapi.svg b/packages/nodes-base/nodes/Strapi/strapi.svg
new file mode 100644
index 0000000000..bf9f95847a
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/strapi.svg
@@ -0,0 +1,72 @@
+
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index 7fe2ff5992..64cbc4de9e 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -180,6 +180,7 @@
"dist/credentials/SpotifyOAuth2Api.credentials.js",
"dist/credentials/StoryblokContentApi.credentials.js",
"dist/credentials/StoryblokManagementApi.credentials.js",
+ "dist/credentials/StrapiApi.credentials.js",
"dist/credentials/SurveyMonkeyApi.credentials.js",
"dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
"dist/credentials/TaigaCloudApi.credentials.js",
@@ -387,6 +388,7 @@
"dist/nodes/SseTrigger.node.js",
"dist/nodes/Start.node.js",
"dist/nodes/Storyblok/Storyblok.node.js",
+ "dist/nodes/Strapi/Strapi.node.js",
"dist/nodes/Strava/Strava.node.js",
"dist/nodes/Strava/StravaTrigger.node.js",
"dist/nodes/Stripe/StripeTrigger.node.js",