Compare commits
11 Commits
fix-vtiger
...
d-x
Author | SHA1 | Date | |
---|---|---|---|
![]() |
35ef66e3fd | ||
![]() |
44f815db02 | ||
![]() |
5224749725 | ||
![]() |
9330e0976b | ||
![]() |
8f72c8c8fa | ||
![]() |
7a632c2ab9 | ||
![]() |
d1e7b6b9eb | ||
![]() |
ba17cde59b | ||
![]() |
2f0125e871 | ||
![]() |
b2dba22674 | ||
![]() |
a3d50e2766 |
@@ -0,0 +1,61 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Get Device Location',
|
||||
key: 'getDeviceLocation',
|
||||
description: 'Get the location of the device.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'MSISDN',
|
||||
key: 'msisdn',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Subscriber number in E.164 format (starting with country code). Optionally prefixed with ' +
|
||||
'.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Latitude',
|
||||
key: 'latitude',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Latitude component of location',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Longitude',
|
||||
key: 'longitude',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Longitude component of location',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Accuracy',
|
||||
key: 'accuracy',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Accuracy expected for location verification in km',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const payload = {
|
||||
ueId: {
|
||||
msisdn: $.step.parameters.msisdn,
|
||||
},
|
||||
latitude: $.step.parameters.latitude,
|
||||
longitude: $.step.parameters.longitude,
|
||||
accuracy: $.step.parameters.accuracy,
|
||||
};
|
||||
|
||||
const response = await $.http.post(
|
||||
'https://api-eu.vonage.com/camara/location/v0/verify',
|
||||
payload
|
||||
);
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/5g-api/actions/index.ts
Normal file
3
packages/backend/src/apps/5g-api/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import getDeviceLocation from './get-device-location';
|
||||
|
||||
export default [getDeviceLocation];
|
4
packages/backend/src/apps/5g-api/assets/favicon.svg
Normal file
4
packages/backend/src/apps/5g-api/assets/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 38" part="logo-svg" fill="#e20074" role="img" aria-labelledby="logo-title-2">
|
||||
<path d="M7.6 25.1H0v-7.6h7.6v7.6ZM0 0v12.9h2.3v-.4c0-6.1 3.4-9.9 9.9-9.9h.4V30c0 3.8-1.5 5.3-5.3 5.3H6.1V38h19.8v-2.7h-1.1c-3.8 0-5.3-1.5-5.3-5.3V2.7h.4c6.5 0 9.9 3.8 9.9 9.9v.4h2.3V0H0Zm24.3 25.1h7.6v-7.6h-7.6v7.6Z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
45
packages/backend/src/apps/5g-api/auth/index.ts
Normal file
45
packages/backend/src/apps/5g-api/auth/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
// import refreshToken from './refresh-token';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
label: 'Private Key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Private Key of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'applicationId',
|
||||
label: 'Application ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'App ID of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
verifyCredentials,
|
||||
// isStillVerified,
|
||||
// refreshToken,
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
// const user = await getCurrentUser($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
39
packages/backend/src/apps/5g-api/auth/verify-credentials.ts
Normal file
39
packages/backend/src/apps/5g-api/auth/verify-credentials.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const claims = {
|
||||
application_id: $.auth.data.applicationId as string,
|
||||
iat: Math.floor(new Date().getTime() / 1000),
|
||||
exp: Math.floor(new Date().getTime() / 1000) + 24 * 60 * 60,
|
||||
jti: uuidv4(),
|
||||
};
|
||||
|
||||
const privateKey = ($.auth.data.privateKey as string).replaceAll('\\n', '\n');
|
||||
|
||||
const jwtToken = jwt.sign(claims, privateKey, {
|
||||
algorithm: 'RS256',
|
||||
});
|
||||
|
||||
const headers = {
|
||||
Authorization: 'Bearer ' + jwtToken,
|
||||
};
|
||||
|
||||
const response = await $.http.post(
|
||||
'https://api.nexmo.com/oauth2/token?grant_type=client_credentials',
|
||||
null,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: responseData.access_token,
|
||||
tokenType: responseData.token_type,
|
||||
expiresIn: responseData.expires_in,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
13
packages/backend/src/apps/5g-api/common/add-auth-header.ts
Normal file
13
packages/backend/src/apps/5g-api/common/add-auth-header.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const { accessToken } = $.auth.data;
|
||||
|
||||
if (accessToken) {
|
||||
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
0
packages/backend/src/apps/5g-api/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/5g-api/index.d.ts
vendored
Normal file
18
packages/backend/src/apps/5g-api/index.ts
Normal file
18
packages/backend/src/apps/5g-api/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: '5G API',
|
||||
key: '5g-api',
|
||||
iconUrl: '{BASE_URL}/apps/5g-api/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/5g-api/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://developer.telekom.de',
|
||||
apiBaseUrl: 'https://api.developer.telekom.de',
|
||||
primaryColor: 'e20074',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
});
|
@@ -0,0 +1,20 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Get Battery Level',
|
||||
key: 'getBatteryLevel',
|
||||
description: 'Get the battery level of a vehicle',
|
||||
|
||||
async run($) {
|
||||
const response = await $.http.get(
|
||||
`https://sandbox.rest-api.high-mobility.com/v5/charging`
|
||||
);
|
||||
|
||||
$.setActionItem({
|
||||
raw: {
|
||||
batteryLevel: response.data.batteryLevel.data,
|
||||
estimatedRange: response.data.estimatedRange.data,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,15 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Get Vehicle Location',
|
||||
key: 'getVehicleLocation',
|
||||
description: 'Get the location of a vehicle',
|
||||
|
||||
async run($) {
|
||||
const response = await $.http.get(
|
||||
`https://sandbox.rest-api.high-mobility.com/v5/vehicle_location`
|
||||
);
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/high-mobility/actions/index.ts
Normal file
4
packages/backend/src/apps/high-mobility/actions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import getVehicleLocation from './get-vehicle-location';
|
||||
import getBatteryLevel from './get-battery-level';
|
||||
|
||||
export default [getVehicleLocation, getBatteryLevel];
|
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1098 0H21.9778C27.5162 0 32.0436 4.52747 31.9997 10.0659V21.9341C31.9997 27.4725 27.4723 32 21.9338 32H10.0658C4.52743 32 0 27.4725 0 21.9341V10.0659C0 4.52747 4.52743 0 10.1098 0ZM9.58232 17.1868H14.4174C14.9889 17.1868 15.4724 16.7033 15.4724 16.1319C15.4724 15.5604 14.9889 15.0769 14.4174 15.0769H9.58232V10.4615C9.58232 9.89011 9.09881 9.40659 8.52739 9.40659C7.95597 9.40659 7.47245 9.89011 7.47245 10.4615V21.5824C7.47245 22.1538 7.95597 22.6374 8.52739 22.6374C9.09881 22.6374 9.58232 22.1538 9.58232 21.5824V17.1868ZM23.5163 22.6374C24.0877 22.6374 24.5712 22.1538 24.5712 21.5824H24.5674V10.4691C24.5712 10.4336 24.5731 10.3975 24.5731 10.361C24.5731 9.80393 24.1216 9.35237 23.5645 9.35237C23.2507 9.35237 22.9704 9.49569 22.7854 9.72044L22.7851 9.7192L17.4943 14.4615C17.0548 14.8571 17.0108 15.5165 17.4064 15.956C17.6262 16.1758 17.8899 16.3077 18.1976 16.3077C18.4614 16.3077 18.6811 16.2198 18.9009 16.044L22.4613 12.8352V21.5824C22.4613 22.1538 22.9448 22.6374 23.5163 22.6374Z" fill="#0A4B58"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,21 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: $.auth.data.clientId as string,
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
const url = `https://sandbox.owner-panel.high-mobility.com/oauth/new?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
93
packages/backend/src/apps/high-mobility/auth/index.ts
Normal file
93
packages/backend/src/apps/high-mobility/auth/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
import refreshToken from './refresh-token';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/high-mobility/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input an OAuth callback or redirect URL in High Mobility OAuth, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Client ID of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Client Secret of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
label: 'Private Key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Private Key of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'appId',
|
||||
label: 'App ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'App ID of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSerialNumber',
|
||||
label: 'Client Serial Number',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Client Serial Number of your High Mobility OAuth app.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import getVehicleInfo from '../common/get-vehicle-info';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
const user = await getVehicleInfo($);
|
||||
return !!user;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,27 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const refreshToken = async ($: IGlobalVariable) => {
|
||||
const payload = {
|
||||
client_id: $.auth.data.clientId as string,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
refresh_token: $.auth.data.refreshToken as string,
|
||||
grant_type: 'refresh_token',
|
||||
};
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'https://sandbox.api.high-mobility.com/v1/access_tokens',
|
||||
payload
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
tokenType: data.token_type,
|
||||
status: data.status,
|
||||
scope: data.scope,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresIn: data.expires_in,
|
||||
accessToken: data.access_token,
|
||||
authorizationId: data.authorization_id,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
@@ -0,0 +1,36 @@
|
||||
import { IGlobalVariable, IField } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
|
||||
const payload = {
|
||||
client_id: $.auth.data.clientId as string,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
code: $.auth.data.code as string,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
};
|
||||
|
||||
const response = await $.http.post(
|
||||
'https://sandbox.api.high-mobility.com/v1/access_tokens',
|
||||
payload
|
||||
);
|
||||
|
||||
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||
|
||||
await $.auth.set({
|
||||
tokenType: responseData.token_type,
|
||||
status: responseData.status,
|
||||
scope: responseData.scope,
|
||||
refreshToken: responseData.refresh_token,
|
||||
expiresIn: responseData.expires_in,
|
||||
accessToken: responseData.access_token,
|
||||
authorizationId: responseData.authorization_id,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,41 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const { accessToken } = $.auth.data;
|
||||
const { url } = requestConfig;
|
||||
|
||||
if (accessToken && url === '/v1/vehicleinfo') {
|
||||
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return requestConfig;
|
||||
}
|
||||
|
||||
const config = {
|
||||
version: '3.0',
|
||||
type: 'rest_api',
|
||||
private_key: ($.auth.data.privateKey as string).replaceAll('\\n', '\n'),
|
||||
app_uri: 'https://sandbox.rest-api.high-mobility.com/v5',
|
||||
app_id: $.auth.data.appId as string,
|
||||
client_serial_number: $.auth.data.clientSerialNumber as string,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
ver: config.version,
|
||||
aud: config.app_uri,
|
||||
iss: config.client_serial_number,
|
||||
iat: Math.round(Date.now() / 1000),
|
||||
jti: uuidv4(),
|
||||
sub: $.auth.data.accessToken,
|
||||
};
|
||||
|
||||
const priv = Buffer.from(config.private_key, 'utf8');
|
||||
|
||||
const token = jwt.sign(payload, priv, { algorithm: 'ES256' });
|
||||
|
||||
requestConfig.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const getVehicleInfo = async ($: IGlobalVariable): Promise<IJSONObject> => {
|
||||
const response = await $.http.get(
|
||||
'https://sandbox.api.high-mobility.com/v1/vehicleinfo'
|
||||
);
|
||||
|
||||
const currentVehicle = response.data;
|
||||
|
||||
return currentVehicle;
|
||||
};
|
||||
|
||||
export default getVehicleInfo;
|
0
packages/backend/src/apps/high-mobility/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/high-mobility/index.d.ts
vendored
Normal file
18
packages/backend/src/apps/high-mobility/index.ts
Normal file
18
packages/backend/src/apps/high-mobility/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: 'High Mobility',
|
||||
key: 'high-mobility',
|
||||
iconUrl: '{BASE_URL}/apps/high-mobility/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/high-mobility/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://high-mobility.com',
|
||||
apiBaseUrl: 'https://api.high-mobility.com',
|
||||
primaryColor: '000000',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
});
|
3
packages/backend/src/apps/positionstack/actions/index.ts
Normal file
3
packages/backend/src/apps/positionstack/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import reverseGeocoding from './reverse-geocoding';
|
||||
|
||||
export default [reverseGeocoding];
|
@@ -0,0 +1,35 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Reverse Geocoding',
|
||||
key: 'reverseGeocoding',
|
||||
description: 'Get the address of a location from its longitude and latitude.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Latitude',
|
||||
key: 'latitude',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Longitude of the location.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Longitude',
|
||||
key: 'longitude',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Latitude of the location.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const response = await $.http.get(
|
||||
`/reverse?access_key=${$.auth.data.apiKey}&query=${$.step.parameters.longitude},${$.step.parameters.latitude}`
|
||||
);
|
||||
|
||||
$.setActionItem({
|
||||
raw: response.data.data[0],
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,4 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 21C15.5 17.4 19 14.1764 19 10.2C19 6.22355 15.866 3 12 3C8.13401 3 5 6.22355 5 10.2C5 14.1764 8.5 17.4 12 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 12C13.1046 12 14 11.1046 14 10C14 8.89543 13.1046 8 12 8C10.8954 8 10 8.89543 10 10C10 11.1046 10.8954 12 12 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 526 B |
33
packages/backend/src/apps/positionstack/auth/index.ts
Normal file
33
packages/backend/src/apps/positionstack/auth/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Positionstack API Key of your account.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
await $.http.get(
|
||||
`/reverse?access_key=${$.auth.data.apiKey}&query=50.94852,6.944772`
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
screenName: $.auth.data.screenName,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
0
packages/backend/src/apps/positionstack/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/positionstack/index.d.ts
vendored
Normal file
16
packages/backend/src/apps/positionstack/index.ts
Normal file
16
packages/backend/src/apps/positionstack/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import auth from './auth';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Positionstack',
|
||||
key: 'positionstack',
|
||||
iconUrl: '{BASE_URL}/apps/positionstack/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/positionstack/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://positionstack.com',
|
||||
apiBaseUrl: 'http://api.positionstack.com/v1',
|
||||
primaryColor: '0d2d45',
|
||||
auth,
|
||||
actions,
|
||||
});
|
@@ -0,0 +1,36 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Get EV Battery Level',
|
||||
key: 'getEvBatteryLevel',
|
||||
description: 'Get the battery level of an electric vehicle.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Vehicle',
|
||||
key: 'vehicle',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description: 'The vehicle to get the location of.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listVehicles',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const { vehicle } = $.step.parameters;
|
||||
const response = await $.http.get(
|
||||
`/vehicles/${vehicle}/battery?mode=simulated`
|
||||
);
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
@@ -0,0 +1,36 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Get Vehicle Location',
|
||||
key: 'getVehicleLocation',
|
||||
description: 'Get the location of a vehicle',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Vehicle',
|
||||
key: 'vehicle',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description: 'The vehicle to get the location of.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listVehicles',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const { vehicle } = $.step.parameters;
|
||||
const response = await $.http.get(
|
||||
`/vehicles/${vehicle}/location?mode=simulated`
|
||||
);
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/smartcar/actions/index.ts
Normal file
4
packages/backend/src/apps/smartcar/actions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import getVehicleLocation from './get-vehicle-location';
|
||||
import getEvBatteryLevel from './get-ev-battery-level';
|
||||
|
||||
export default [getVehicleLocation, getEvBatteryLevel];
|
11
packages/backend/src/apps/smartcar/assets/favicon.svg
Normal file
11
packages/backend/src/apps/smartcar/assets/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="217" height="60" viewBox="0 0 217 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30 0C13.4328 0 0 13.4328 0 30C0 46.5672 13.4328 60 30 60C46.5672 60 60 46.5672 60 30C60 13.4328 46.5672 0 30 0ZM30 7.5C39.7625 7.5 48.0107 13.7875 51.123 22.5H8.877C11.9893 13.7875 20.2375 7.5 30 7.5ZM30 33.75C29.2583 33.75 28.5333 33.5301 27.9166 33.118C27.2999 32.706 26.8193 32.1203 26.5355 31.4351C26.2516 30.7498 26.1774 29.9958 26.3221 29.2684C26.4668 28.541 26.8239 27.8728 27.3483 27.3483C27.8728 26.8239 28.541 26.4668 29.2684 26.3221C29.9958 26.1774 30.7498 26.2516 31.4351 26.5355C32.1203 26.8193 32.706 27.2999 33.118 27.9166C33.5301 28.5333 33.75 29.2583 33.75 30C33.7503 30.4925 33.6536 30.9803 33.4652 31.4355C33.2769 31.8906 33.0007 32.3041 32.6524 32.6524C32.3041 33.0007 31.8906 33.2769 31.4355 33.4652C30.9803 33.6536 30.4925 33.7503 30 33.75ZM7.5 30C17.7467 30 26.0448 39.873 26.2206 52.119C15.615 50.3027 7.5 41.1107 7.5 30ZM33.7794 52.119C33.9552 39.873 42.2533 30 52.5 30C52.5 41.1107 44.385 50.3027 33.7794 52.119Z" fill="black"/>
|
||||
<path d="M79.8724 40.5406C78.2018 40.5406 76.7369 40.2739 75.4776 39.7405C74.2182 39.184 73.1131 38.3608 72.1622 37.2709L75.015 34.7665C75.6575 35.5781 76.3771 36.1926 77.1738 36.61C77.9705 37.0274 78.8958 37.2361 79.9495 37.2361C81.9028 37.2361 82.8794 36.4941 82.8794 35.01C82.8794 34.4303 82.7123 33.9781 82.3782 33.6535C82.0698 33.3056 81.5301 33.0737 80.7591 32.9578L78.8701 32.6447C76.7626 32.2969 75.2334 31.7056 74.2825 30.8708C73.3316 30.036 72.8561 28.8186 72.8561 27.2185C72.8561 25.3171 73.4729 23.9025 74.7065 22.975C75.9402 22.0242 77.6878 21.5489 79.9495 21.5489C81.5172 21.5489 82.8665 21.7808 83.9974 22.2445C85.1539 22.7083 86.1562 23.3924 87.0044 24.2968L84.2672 26.7664C83.7532 26.1866 83.1235 25.7229 82.3782 25.375C81.6586 25.0272 80.849 24.8533 79.9495 24.8533C78.1504 24.8533 77.2509 25.5837 77.2509 27.0446C77.2509 27.6475 77.4308 28.1113 77.7906 28.436C78.1504 28.7374 78.7159 28.9461 79.4869 29.0621L81.4144 29.3751C83.5219 29.7229 85.0254 30.3143 85.9249 31.1491C86.8502 31.9607 87.3128 33.1085 87.3128 34.5926C87.3128 36.4709 86.6702 37.9318 85.3852 38.9753C84.1002 40.0188 82.2626 40.5406 79.8724 40.5406Z" fill="black"/>
|
||||
<path d="M91.0992 40.1232V31.0447V21.9663H95.7639V24.9924H96.3036C97.1003 22.6967 98.6809 21.5489 101.045 21.5489C103.564 21.5489 105.196 22.7431 105.941 25.1315H106.404C106.841 23.9025 107.509 22.9982 108.409 22.4185C109.334 21.8387 110.452 21.5489 111.763 21.5489C113.459 21.5489 114.782 22.1054 115.733 23.2185C116.684 24.3083 117.16 25.8736 117.16 27.9142V40.1232H112.495V28.6447C112.495 26.2794 111.557 25.0968 109.681 25.0968C108.781 25.0968 108.01 25.3518 107.368 25.862C106.751 26.349 106.443 27.0446 106.443 27.949V40.1232H101.778V28.6447C101.778 27.3925 101.547 26.4881 101.084 25.9316C100.621 25.375 99.9274 25.0968 99.0022 25.0968C98.1027 25.0968 97.3316 25.3518 96.6891 25.862C96.0723 26.349 95.7639 27.0446 95.7639 27.949V40.1232H91.0992Z" fill="black"/>
|
||||
<path d="M135.115 40.1232C133.034 40.1232 131.941 39.0912 131.838 37.0274H131.376C131.119 38.1405 130.541 39.0101 129.641 39.6362C128.741 40.2391 127.624 40.5406 126.287 40.5406C124.437 40.5406 123.049 40.0768 122.124 39.1492C121.198 38.2217 120.736 36.9231 120.736 35.2535C120.736 33.352 121.391 31.9375 122.702 31.0099C124.013 30.0824 125.902 29.6186 128.369 29.6186H131.491V28.0533C131.491 26.9867 131.234 26.1982 130.72 25.6881C130.232 25.1779 129.41 24.9228 128.253 24.9228C127.302 24.9228 126.506 25.1315 125.863 25.5489C125.22 25.9432 124.655 26.5113 124.167 27.2533L121.43 25.0272C122.175 23.9837 123.113 23.1489 124.244 22.5228C125.4 21.8735 126.865 21.5489 128.639 21.5489C133.65 21.5489 136.156 23.6822 136.156 27.949V36.7492H137.93V40.1232H135.115ZM128.06 37.4448C129.011 37.4448 129.821 37.2014 130.489 36.7144C131.157 36.2042 131.491 35.5086 131.491 34.6274V32.2273H128.677C126.518 32.2273 125.439 32.981 125.439 34.4883V35.2535C125.439 36.0187 125.67 36.5753 126.133 36.9231C126.595 37.2709 127.238 37.4448 128.06 37.4448Z" fill="black"/>
|
||||
<path d="M141.547 40.1232V21.9663H146.212V25.6881H146.751C146.983 24.4823 147.458 23.5663 148.178 22.9402C148.923 22.2909 149.887 21.9663 151.069 21.9663H152.071V26.2794H150.529C149.064 26.2794 147.972 26.4881 147.253 26.9055C146.559 27.3229 146.212 28.0533 146.212 29.0968V40.1232H141.547Z" fill="black"/>
|
||||
<path d="M161.367 40.1232C159.748 40.1232 158.578 39.7985 157.859 39.1492C157.139 38.4999 156.779 37.468 156.779 36.0535V25.3402H154.119V21.9663H155.507C156.175 21.9663 156.625 21.8271 156.856 21.5489C157.113 21.2706 157.242 20.83 157.242 20.2271V17.027H161.444V21.9663H165.068V25.3402H161.444V36.7492H164.759V40.1232H161.367Z" fill="black"/>
|
||||
<path d="M175.672 40.5406C172.948 40.5406 170.866 39.7753 169.427 38.2449C168.013 36.7144 167.307 34.3143 167.307 31.0447C167.307 27.7751 168.013 25.375 169.427 23.8446C170.866 22.3141 172.948 21.5489 175.672 21.5489C177.651 21.5489 179.219 21.9547 180.376 22.7663C181.558 23.5779 182.406 24.691 182.92 26.1055L179.103 27.6359C178.898 26.7548 178.525 26.0939 177.985 25.6533C177.471 25.1895 176.7 24.9576 175.672 24.9576C173.359 24.9576 172.203 26.2098 172.203 28.7142V33.3752C172.203 35.8796 173.359 37.1318 175.672 37.1318C176.752 37.1318 177.574 36.8999 178.14 36.4361C178.705 35.9723 179.129 35.2535 179.412 34.2796L183.036 35.8448C181.853 38.9753 179.399 40.5406 175.672 40.5406Z" fill="black"/>
|
||||
<path d="M199.53 40.1232C197.449 40.1232 196.356 39.0912 196.253 37.0274H195.791C195.534 38.1405 194.956 39.0101 194.056 39.6362C193.157 40.2391 192.039 40.5406 190.702 40.5406C188.852 40.5406 187.464 40.0768 186.539 39.1492C185.613 38.2217 185.151 36.9231 185.151 35.2535C185.151 33.352 185.806 31.9375 187.117 31.0099C188.428 30.0824 190.317 29.6186 192.784 29.6186H195.907V28.0533C195.907 26.9867 195.649 26.1982 195.135 25.6881C194.647 25.1779 193.825 24.9228 192.668 24.9228C191.717 24.9228 190.921 25.1315 190.278 25.5489C189.636 25.9432 189.07 26.5113 188.582 27.2533L185.845 25.0272C186.59 23.9837 187.528 23.1489 188.659 22.5228C189.815 21.8735 191.28 21.5489 193.054 21.5489C198.065 21.5489 200.571 23.6822 200.571 27.949V36.7492H202.345V40.1232H199.53ZM192.475 37.4448C193.426 37.4448 194.236 37.2014 194.904 36.7144C195.572 36.2042 195.907 35.5086 195.907 34.6274V32.2273H193.092C190.933 32.2273 189.854 32.981 189.854 34.4883V35.2535C189.854 36.0187 190.085 36.5753 190.548 36.9231C191.011 37.2709 191.653 37.4448 192.475 37.4448Z" fill="black"/>
|
||||
<path d="M205.962 40.1232V21.9663H210.627V25.6881H211.166C211.398 24.4823 211.873 23.5663 212.593 22.9402C213.338 22.2909 214.302 21.9663 215.484 21.9663H216.486V26.2794H214.944C213.48 26.2794 212.387 26.4881 211.668 26.9055C210.974 27.3229 210.627 28.0533 210.627 29.0968V40.1232H205.962Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
30
packages/backend/src/apps/smartcar/auth/generate-auth-url.ts
Normal file
30
packages/backend/src/apps/smartcar/auth/generate-auth-url.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const scopes = [
|
||||
'read_odometer',
|
||||
'required:read_vehicle_info',
|
||||
'required:read_location',
|
||||
'required:read_battery',
|
||||
];
|
||||
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: $.auth.data.clientId as string,
|
||||
scope: scopes.join(' '),
|
||||
redirect_uri: redirectUri,
|
||||
mode: 'simulated',
|
||||
});
|
||||
|
||||
const url = `https://connect.smartcar.com/oauth/authorize?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
58
packages/backend/src/apps/smartcar/auth/index.ts
Normal file
58
packages/backend/src/apps/smartcar/auth/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/twitter/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input an OAuth callback or redirect URL in Smartcar OAuth, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentUser from '../common/get-current-user';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
const user = await getCurrentUser($);
|
||||
return !!user;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,41 @@
|
||||
import { IGlobalVariable, IField } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const token = Buffer.from(
|
||||
`${$.auth.data.clientId}:${$.auth.data.clientSecret}`
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
Authorization: `Basic ${token}`,
|
||||
};
|
||||
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: $.auth.data.code as string,
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
const response = await $.http.post(
|
||||
`https://auth.smartcar.com/oauth/token`,
|
||||
params.toString(),
|
||||
{ headers }
|
||||
);
|
||||
|
||||
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: responseData.access_token,
|
||||
tokenType: responseData.token_type,
|
||||
expiresIn: responseData.expires_in,
|
||||
refreshToken: responseData.refresh_token,
|
||||
screenName: $.auth.data.screenName,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
13
packages/backend/src/apps/smartcar/common/add-auth-header.ts
Normal file
13
packages/backend/src/apps/smartcar/common/add-auth-header.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const { accessToken } = $.auth.data;
|
||||
|
||||
if (accessToken) {
|
||||
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,10 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
|
||||
const response = await $.http.get('/user');
|
||||
const currentUser = response.data;
|
||||
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
3
packages/backend/src/apps/smartcar/dynamic-data/index.ts
Normal file
3
packages/backend/src/apps/smartcar/dynamic-data/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import listVehicles from './list-vehicles';
|
||||
|
||||
export default [listVehicles];
|
@@ -0,0 +1,31 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List vehicles',
|
||||
key: 'listVehicles',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const vehicles: {
|
||||
data: IJSONObject[];
|
||||
error: IJSONObject | null;
|
||||
} = {
|
||||
data: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
const response: any = await $.http.get('/vehicles');
|
||||
|
||||
for (const vehicle of response.data.vehicles) {
|
||||
const response: any = await $.http.get(`/vehicles/${vehicle}`);
|
||||
|
||||
const vehicleName = `${response.data.make} - ${response.data.model} (${response.data.year})`;
|
||||
|
||||
vehicles.data.push({
|
||||
value: vehicle as string,
|
||||
name: vehicleName,
|
||||
});
|
||||
}
|
||||
|
||||
return vehicles;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/smartcar/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/smartcar/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/smartcar/index.ts
Normal file
20
packages/backend/src/apps/smartcar/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import actions from './actions';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Smartcar',
|
||||
key: 'smartcar',
|
||||
iconUrl: '{BASE_URL}/apps/smartcar/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/smartcar/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://smartcar.com',
|
||||
apiBaseUrl: 'https://api.smartcar.com/v2.0',
|
||||
primaryColor: '000000',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
dynamicData,
|
||||
});
|
3
packages/backend/src/apps/vonage/actions/index.ts
Normal file
3
packages/backend/src/apps/vonage/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import sendMessage from './send-message';
|
||||
|
||||
export default [sendMessage];
|
@@ -0,0 +1,82 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Send Message',
|
||||
key: 'sendMessage',
|
||||
description: 'Send a message to a number.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Message Type',
|
||||
key: 'messageType',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'The type of message to send. e.g. text',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
key: 'channel',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The channel to send the message through. e.g. sms, whatsapp',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'From Number',
|
||||
key: 'fromNumber',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The number to send the message from. Include country code. Example: 15551234567',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'To Number',
|
||||
key: 'toNumber',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The number to send the message to. Include country code. Example: 15551234567',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
key: 'message',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'The message to send.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const messageType = $.step.parameters.messageType as string;
|
||||
const channel = $.step.parameters.channel as string;
|
||||
const messageBody = $.step.parameters.message as string;
|
||||
const fromNumber = ($.step.parameters.fromNumber as string).trim();
|
||||
const toNumber = ($.step.parameters.toNumber as string).trim();
|
||||
|
||||
const basicAuthToken = Buffer.from(
|
||||
`${$.auth.data.apiKey}:${$.auth.data.apiSecret}`
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
message_type: messageType,
|
||||
text: messageBody,
|
||||
to: toNumber,
|
||||
from: fromNumber,
|
||||
channel,
|
||||
};
|
||||
|
||||
const response = await $.http.post('/messages', payload, { headers });
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/vonage/assets/favicon.svg
Normal file
4
packages/backend/src/apps/vonage/assets/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 230 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M45.3408,0 L-0.0002,0 L64.6808,146.958 C65.1748,148.081 66.7718,148.07 67.2508,146.942 L88.7628,96.337 L45.3408,0 Z"></path>
|
||||
<path fill="currentColor" d="M183.4502,0 C183.4502,0 113.9562,159.156 104.6482,173.833 C93.8292,190.896 86.6592,197.409 73.3912,199.496 C73.2682,199.515 73.1772,199.621 73.1772,199.746 C73.1772,199.886 73.2912,200 73.4312,200 L114.9552,200 C132.9432,200 145.9152,184.979 153.1042,171.714 C161.2742,156.637 229.5902,0 229.5902,0 L183.4502,0 Z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 593 B |
44
packages/backend/src/apps/vonage/auth/index.ts
Normal file
44
packages/backend/src/apps/vonage/auth/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'API Key from Vonage.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'apiSecret',
|
||||
label: 'API Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'API Secret from Vonage.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
13
packages/backend/src/apps/vonage/auth/verify-credentials.ts
Normal file
13
packages/backend/src/apps/vonage/auth/verify-credentials.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
await $.http.get(
|
||||
`https://rest.nexmo.com/account/get-balance?api_key=${$.auth.data.apiKey}&api_secret=${$.auth.data.apiSecret}`
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
screenName: $.auth.data.screenName,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
0
packages/backend/src/apps/vonage/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/vonage/index.d.ts
vendored
Normal file
18
packages/backend/src/apps/vonage/index.ts
Normal file
18
packages/backend/src/apps/vonage/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Vonage',
|
||||
key: 'vonage',
|
||||
iconUrl: '{BASE_URL}/apps/vonage/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/vonage/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://vonage.com',
|
||||
apiBaseUrl: 'https://messages-sandbox.nexmo.com/v1',
|
||||
primaryColor: '000000',
|
||||
auth,
|
||||
triggers,
|
||||
actions,
|
||||
});
|
3
packages/backend/src/apps/vonage/triggers/index.ts
Normal file
3
packages/backend/src/apps/vonage/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import receiveMessage from './receive-message';
|
||||
|
||||
export default [receiveMessage];
|
@@ -0,0 +1,63 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'Receive message - Sandbox',
|
||||
key: 'receiveMessage',
|
||||
type: 'webhook',
|
||||
description:
|
||||
'Triggers when a message is received from Vonage sandbox number. (+14157386102)',
|
||||
arguments: [
|
||||
{
|
||||
label: 'From Number',
|
||||
key: 'fromNumber',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The number from which the message was sent. (e.g. 491234567899)',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async testRun($) {
|
||||
const lastExecutionStep = await $.getLastExecutionStep();
|
||||
|
||||
if (!isEmpty(lastExecutionStep?.dataOut)) {
|
||||
$.pushTriggerItem({
|
||||
raw: lastExecutionStep.dataOut,
|
||||
meta: {
|
||||
internalId: '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const sampleData = {
|
||||
to: '14157386102',
|
||||
from: $.step.parameters.fromNumber as string,
|
||||
text: 'Tell me how will be the weather tomorrow for Berlin?',
|
||||
channel: 'whatsapp',
|
||||
profile: {
|
||||
name: 'Sample User',
|
||||
},
|
||||
timestamp: '2023-09-18T19:52:36Z',
|
||||
message_type: 'text',
|
||||
message_uuid: '318960cc-16ff-4f66-8397-45574333a435',
|
||||
context_status: 'none',
|
||||
};
|
||||
|
||||
$.pushTriggerItem({
|
||||
raw: sampleData,
|
||||
meta: {
|
||||
internalId: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async registerHook() {
|
||||
// void
|
||||
},
|
||||
|
||||
async unregisterHook() {
|
||||
// void
|
||||
},
|
||||
});
|
34
packages/backend/src/controllers/webhooks/handler-by-app.ts
Normal file
34
packages/backend/src/controllers/webhooks/handler-by-app.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Response } from 'express';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
|
||||
import Step from '../../models/step';
|
||||
import logger from '../../helpers/logger';
|
||||
import handler from '../../helpers/webhook-handler';
|
||||
|
||||
export default async (request: IRequest, response: Response) => {
|
||||
const computedRequestPayload = {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
query: request.query,
|
||||
params: request.params,
|
||||
};
|
||||
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
|
||||
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
|
||||
|
||||
const triggerSteps = await Step.query().where({
|
||||
type: 'trigger',
|
||||
app_key: 'vonage',
|
||||
key: 'receiveMessage',
|
||||
});
|
||||
|
||||
if (triggerSteps.length === 0) return response.sendStatus(404);
|
||||
|
||||
for (const triggerStep of triggerSteps) {
|
||||
const flow = await triggerStep.$relatedQuery('flow');
|
||||
if (flow.status !== 'published') continue;
|
||||
|
||||
await handler(triggerStep.flowId, request, response);
|
||||
}
|
||||
|
||||
response.sendStatus(204);
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
import appConfig from '../../config/app';
|
||||
import User from '../../models/user';
|
||||
import Role from '../../models/role';
|
||||
|
||||
@@ -10,6 +11,8 @@ type Params = {
|
||||
};
|
||||
|
||||
const registerUser = async (_parent: unknown, params: Params) => {
|
||||
if (!appConfig.isCloud) return;
|
||||
|
||||
const { fullName, email, password } = params.input;
|
||||
|
||||
const existingUser = await User.query().findOne({ email });
|
||||
|
@@ -1,32 +1,45 @@
|
||||
import express, { Response, Router, NextFunction, RequestHandler } from 'express';
|
||||
import express, {
|
||||
Response,
|
||||
Router,
|
||||
NextFunction,
|
||||
RequestHandler,
|
||||
} from 'express';
|
||||
import multer from 'multer';
|
||||
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import appConfig from '../config/app';
|
||||
import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id';
|
||||
import webhookHandlerByConnectionIdAndRefValue from '../controllers/webhooks/handler-by-connection-id-and-ref-value';
|
||||
import webhookHandlerByApp from '../controllers/webhooks/handler-by-app';
|
||||
|
||||
const router = Router();
|
||||
const upload = multer();
|
||||
|
||||
router.use(upload.none());
|
||||
|
||||
router.use(express.text({
|
||||
router.use(
|
||||
express.text({
|
||||
limit: appConfig.requestBodySizeLimit,
|
||||
verify(req, res, buf) {
|
||||
(req as IRequest).rawBody = buf;
|
||||
},
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const exposeError = (handler: RequestHandler) => async (req: IRequest, res: Response, next: NextFunction) => {
|
||||
const exposeError =
|
||||
(handler: RequestHandler) =>
|
||||
async (req: IRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await handler(req, res, next);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createRouteHandler(path: string, handler: (req: IRequest, res: Response, next: NextFunction) => void) {
|
||||
function createRouteHandler(
|
||||
path: string,
|
||||
handler: (req: IRequest, res: Response, next: NextFunction) => void
|
||||
) {
|
||||
const wrappedHandler = exposeError(handler);
|
||||
|
||||
router
|
||||
@@ -35,11 +48,18 @@ function createRouteHandler(path: string, handler: (req: IRequest, res: Response
|
||||
.put(wrappedHandler)
|
||||
.patch(wrappedHandler)
|
||||
.post(wrappedHandler);
|
||||
};
|
||||
}
|
||||
|
||||
createRouteHandler('/connections/:connectionId/:refValue', webhookHandlerByConnectionIdAndRefValue);
|
||||
createRouteHandler('/connections/:connectionId', webhookHandlerByConnectionIdAndRefValue);
|
||||
createRouteHandler(
|
||||
'/connections/:connectionId/:refValue',
|
||||
webhookHandlerByConnectionIdAndRefValue
|
||||
);
|
||||
createRouteHandler(
|
||||
'/connections/:connectionId',
|
||||
webhookHandlerByConnectionIdAndRefValue
|
||||
);
|
||||
createRouteHandler('/flows/:flowId', webhookHandlerByFlowId);
|
||||
createRouteHandler('/:flowId', webhookHandlerByFlowId);
|
||||
createRouteHandler('/apps/vonage', webhookHandlerByApp);
|
||||
|
||||
export default router;
|
||||
|
@@ -80,7 +80,7 @@ function ChooseAppAndEventSubstep(
|
||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
||||
|
||||
const appOptions = React.useMemo(
|
||||
() => apps?.map((app) => optionGenerator(app)),
|
||||
() => apps?.map((app) => optionGenerator(app)) || [],
|
||||
[apps]
|
||||
);
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
||||
@@ -167,7 +167,7 @@ function ChooseAppAndEventSubstep(
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
disablePortal
|
||||
disableClearable
|
||||
disableClearable={getOption(appOptions, step.appKey) !== undefined}
|
||||
disabled={editorContext.readOnly}
|
||||
options={appOptions}
|
||||
renderInput={(params) => (
|
||||
@@ -176,7 +176,7 @@ function ChooseAppAndEventSubstep(
|
||||
label={formatMessage('flowEditor.chooseApp')}
|
||||
/>
|
||||
)}
|
||||
value={getOption(appOptions, step.appKey)}
|
||||
value={getOption(appOptions, step.appKey) || null}
|
||||
onChange={onAppChange}
|
||||
data-test="choose-app-autocomplete"
|
||||
/>
|
||||
@@ -191,7 +191,9 @@ function ChooseAppAndEventSubstep(
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
disablePortal
|
||||
disableClearable
|
||||
disableClearable={
|
||||
getOption(actionOrTriggerOptions, step.key) !== undefined
|
||||
}
|
||||
disabled={editorContext.readOnly}
|
||||
options={actionOrTriggerOptions}
|
||||
renderInput={(params) => (
|
||||
@@ -235,7 +237,7 @@ function ChooseAppAndEventSubstep(
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
value={getOption(actionOrTriggerOptions, step.key)}
|
||||
value={getOption(actionOrTriggerOptions, step.key) || null}
|
||||
onChange={onEventChange}
|
||||
data-test="choose-event-autocomplete"
|
||||
/>
|
||||
|
@@ -255,7 +255,7 @@ function ControlledCustomAutocomplete(
|
||||
/>
|
||||
|
||||
<ActionButtonsWrapper direction="row" mr={1.5}>
|
||||
{isSingleChoice && serialize(editor.children) && (
|
||||
{isSingleChoice && serialize(editor.children) !== '' && (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
|
@@ -55,10 +55,9 @@ export default function InputCreator(
|
||||
} = schema;
|
||||
|
||||
const { data, loading } = useDynamicData(stepId, schema);
|
||||
const {
|
||||
data: additionalFields,
|
||||
loading: additionalFieldsLoading
|
||||
} = useDynamicFields(stepId, schema);
|
||||
|
||||
const { data: additionalFields, loading: additionalFieldsLoading } =
|
||||
useDynamicFields(stepId, schema);
|
||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||
|
||||
if (type === 'dynamic') {
|
||||
@@ -121,9 +120,11 @@ export default function InputCreator(
|
||||
/>
|
||||
)}
|
||||
|
||||
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||
{additionalFieldsLoading && !additionalFields?.length && (
|
||||
<div>
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{additionalFields?.map((field) => (
|
||||
<InputCreator
|
||||
@@ -154,9 +155,13 @@ export default function InputCreator(
|
||||
shouldUnregister={shouldUnregister}
|
||||
/>
|
||||
|
||||
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
</div>}
|
||||
{additionalFieldsLoading && !additionalFields?.length && (
|
||||
<div>
|
||||
<CircularProgress
|
||||
sx={{ display: 'block', margin: '20px auto' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{additionalFields?.map((field) => (
|
||||
<InputCreator
|
||||
@@ -191,9 +196,11 @@ export default function InputCreator(
|
||||
shouldUnregister={shouldUnregister}
|
||||
/>
|
||||
|
||||
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||
{additionalFieldsLoading && !additionalFields?.length && (
|
||||
<div>
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{additionalFields?.map((field) => (
|
||||
<InputCreator
|
||||
|
@@ -92,19 +92,13 @@ const variableRegExp = /({{.*?}})/;
|
||||
const stepIdRegExp = /^step.([\da-zA-Z-]*)/;
|
||||
|
||||
export const deserialize = (
|
||||
value: string,
|
||||
value: boolean | string | number,
|
||||
options: readonly IFieldDropdownOption[],
|
||||
stepsWithVariables: StepsWithVariables
|
||||
): Descendant[] => {
|
||||
if (!value)
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
const selectedNativeOption = options.find((option) => value === option.value);
|
||||
const selectedNativeOption = options?.find(
|
||||
(option) => value === option.value
|
||||
);
|
||||
|
||||
if (selectedNativeOption) {
|
||||
return [
|
||||
@@ -116,7 +110,18 @@ export const deserialize = (
|
||||
];
|
||||
}
|
||||
|
||||
return value.split('\n').map((line) => {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
return value
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const nodes = line.split(variableRegExp);
|
||||
|
||||
if (nodes.length > 1) {
|
||||
@@ -152,7 +157,7 @@ export const deserialize = (
|
||||
});
|
||||
};
|
||||
|
||||
export const serialize = (value: Descendant[]): string => {
|
||||
export const serialize = (value: Descendant[]): string | number | null => {
|
||||
const serializedNodes = value.map((node) => serializeNode(node));
|
||||
|
||||
const hasSingleNode = value.length === 1;
|
||||
@@ -169,8 +174,12 @@ export const serialize = (value: Descendant[]): string => {
|
||||
return serializedValue;
|
||||
};
|
||||
|
||||
const serializeNode = (node: CustomElement | Descendant): string => {
|
||||
if (isCustomText(node)) return node.value;
|
||||
const serializeNode = (
|
||||
node: CustomElement | Descendant
|
||||
): string | number | null => {
|
||||
if (isCustomText(node)) {
|
||||
return node.value;
|
||||
}
|
||||
|
||||
if (Text.isText(node)) {
|
||||
return node.text;
|
||||
|
@@ -27,7 +27,7 @@ function computeArguments(
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (computedValue === undefined)
|
||||
if (computedValue === undefined || computedValue === '')
|
||||
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
Reference in New Issue
Block a user