Skip to content

Commit

Permalink
feat(api): add slug parser in the api requests (#6705)
Browse files Browse the repository at this point in the history
Co-authored-by: GalT <39020298+tatarco@users.noreply.github.com>
  • Loading branch information
djabarovgeorge and tatarco authored Oct 23, 2024
1 parent ca3dddc commit 0e27b89
Show file tree
Hide file tree
Showing 28 changed files with 731 additions and 219 deletions.
3 changes: 1 addition & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@
"shortid": "^2.2.16",
"slugify": "^1.4.6",
"swagger-ui-express": "^4.4.0",
"twilio": "^4.14.1",
"zod": "^3.23.8",
"twilio": "^4.14.1","zod": "^3.23.8",
"json-schema-to-ts": "^3.0.0",
"uuid": "^8.3.2"
},
Expand Down
14 changes: 1 addition & 13 deletions apps/api/src/app/auth/services/passport/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
const environmentIdFromHeader =
(req.headers[HttpRequestHeaderKeysEnum.NOVU_ENVIRONMENT_ID.toLowerCase()] as string) || '';

let currentEnvironmentId = '';

const environments = await this.environmentRepository.findOrganizationEnvironments(session.organizationId);
const environmentIds = environments.map((env) => env._id);
const developmentEnvironmentId = environments.find((env) => env.name === 'Development')?._id || '';

currentEnvironmentId = developmentEnvironmentId;

if (environmentIds.includes(environmentIdFromHeader)) {
currentEnvironmentId = environmentIdFromHeader;
}

// eslint-disable-next-line no-param-reassign
session.environmentId = currentEnvironmentId;
session.environmentId = environmentIdFromHeader;
}
}
150 changes: 150 additions & 0 deletions apps/api/src/app/shared/helpers/base62/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* eslint-disable */
/* cspell:disable */

/*
* base-x encoding / decoding
* Copyright (c) 2018 base-x contributors
* Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp)
* Distributed under the MIT software license, see the accompanying
* file LICENSE or http://www.opensource.org/licenses/mit-license.php.
* "version": "5.0.0",
*/

export function base(ALPHABET) {
if (ALPHABET.length >= 255) {
throw new TypeError('Alphabet too long');
}
const BASE_MAP = new Uint8Array(256);
for (let j = 0; j < BASE_MAP.length; j++) {
BASE_MAP[j] = 255;
}
for (let i = 0; i < ALPHABET.length; i++) {
const x = ALPHABET.charAt(i);
const xc = x.charCodeAt(0);
if (BASE_MAP[xc] !== 255) {
throw new TypeError(`${x} is ambiguous`);
}
BASE_MAP[xc] = i;
}
const BASE = ALPHABET.length;
const LEADER = ALPHABET.charAt(0);
const FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
const iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
function encode(source) {
if (source instanceof Uint8Array) {
} else if (ArrayBuffer.isView(source)) {
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
} else if (Array.isArray(source)) {
source = Uint8Array.from(source);
}
if (!(source instanceof Uint8Array)) {
throw new TypeError('Expected Uint8Array');
}
if (source.length === 0) {
return '';
}
// Skip & count leading zeroes.
let zeroes = 0;
let length = 0;
let pbegin = 0;
const pend = source.length;
while (pbegin !== pend && source[pbegin] === 0) {
pbegin++;
zeroes++;
}
// Allocate enough space in big-endian base58 representation.
const size = ((pend - pbegin) * iFACTOR + 1) >>> 0;
const b58 = new Uint8Array(size);
// Process the bytes.
while (pbegin !== pend) {
let carry = source[pbegin];
// Apply "b58 = b58 * 256 + ch".
let i = 0;
for (let it1 = size - 1; (carry !== 0 || i < length) && it1 !== -1; it1--, i++) {
carry += (256 * b58[it1]) >>> 0;
b58[it1] = carry % BASE >>> 0;
carry = (carry / BASE) >>> 0;
}
if (carry !== 0) {
throw new Error('Non-zero carry');
}
length = i;
pbegin++;
}
// Skip leading zeroes in base58 result.
let it2 = size - length;
while (it2 !== size && b58[it2] === 0) {
it2++;
}
// Translate the result into a string.
let str = LEADER.repeat(zeroes);
for (; it2 < size; ++it2) {
str += ALPHABET.charAt(b58[it2]);
}

return str;
}
function decodeUnsafe(source) {
if (typeof source !== 'string') {
throw new TypeError('Expected String');
}
if (source.length === 0) {
return new Uint8Array();
}
let psz = 0;
// Skip and count leading '1's.
let zeroes = 0;
let length = 0;
while (source[psz] === LEADER) {
zeroes++;
psz++;
}
// Allocate enough space in big-endian base256 representation.
const size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.
const b256 = new Uint8Array(size);
// Process the characters.
while (psz < source.length) {
// Decode character
let carry = BASE_MAP[source.charCodeAt(psz)];
// Invalid character
if (carry === 255) {
return;
}
let i = 0;
for (let it3 = size - 1; (carry !== 0 || i < length) && it3 !== -1; it3--, i++) {
carry += (BASE * b256[it3]) >>> 0;
b256[it3] = carry % 256 >>> 0;
carry = (carry / 256) >>> 0;
}
if (carry !== 0) {
throw new Error('Non-zero carry');
}
length = i;
psz++;
}
// Skip leading zeroes in b256.
let it4 = size - length;
while (it4 !== size && b256[it4] === 0) {
it4++;
}
const vch = new Uint8Array(zeroes + (size - it4));
let j = zeroes;
while (it4 !== size) {
vch[j++] = b256[it4++];
}

return vch;
}
function decode(string) {
const buffer = decodeUnsafe(string);
if (buffer) {
return buffer;
}
throw new Error(`Non-base${BASE} character`);
}

return {
encode,
decode,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Base62 alphabet
* Modifying this alphabet is prohibited as it would invalidate existing encoded data
*/
export const BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
17 changes: 17 additions & 0 deletions apps/api/src/app/shared/helpers/base62/base62.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { base } from './base';
import { BASE62_ALPHABET } from './base62-alphabet.const';

const { encode, decode } = base(BASE62_ALPHABET);
const ENCODING = 'hex' satisfies BufferEncoding;

export function encodeBase62(value: string): string {
const buffer = Buffer.from(value, ENCODING);

return encode(buffer);
}

export function decodeBase62(encoded: string): string {
const uint8Array = decode(encoded);

return Buffer.from(uint8Array).toString(ENCODING);
}
1 change: 1 addition & 0 deletions apps/api/src/app/shared/helpers/base62/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './base62';
1 change: 1 addition & 0 deletions apps/api/src/app/shared/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './utils';
export * from './base62';
7 changes: 4 additions & 3 deletions apps/api/src/app/step-schemas/step-schemas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createGetStepSchemaCommand } from './usecases/get-step-schema/get-step-
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
import { GetStepSchemaUseCase } from './usecases/get-step-schema/get-step-schema.usecase';
import { StepSchemaDto } from './dtos/step-schema.dto';
import { ParseSlugIdPipe } from '../workflows-v2/pipes/parse-slug-Id.pipe';

@Controller('/step-schemas')
@UserAuthentication()
Expand All @@ -19,9 +20,9 @@ export class StepSchemasController {
@ExternalApiAccessible()
async getWorkflowStepSchema(
@UserSession() user: UserSessionData,
@Query('stepType') stepType: StepType,
@Query('workflowId') workflowId: string,
@Query('stepId') stepId: string
@Query('stepType') stepType?: StepType,
@Query('workflowId', ParseSlugIdPipe) workflowId?: string,
@Query('stepId', ParseSlugIdPipe) stepId?: string
): Promise<StepSchemaDto> {
return await this.getStepDefaultSchemaUsecase.execute(
createGetStepSchemaCommand(user, stepType, workflowId, stepId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BadRequestException } from '@nestjs/common';
import { EnvironmentWithUserCommand } from '@novu/application-generic';
import { ActionStepEnum, ChannelStepEnum, StepType } from '@novu/framework/internal';
import { UserSessionData } from '@novu/shared';
Expand Down Expand Up @@ -25,10 +26,10 @@ export type GetStepSchemaCommand = GetStepTypeSchemaCommand | GetExistingStepSch

export function createGetStepSchemaCommand(
user: UserSessionData,
stepType: StepType,
workflowId: string,
stepId: string
) {
stepType?: StepType,
workflowId?: string,
stepId?: string
): GetExistingStepSchemaCommand | GetStepTypeSchemaCommand {
if (workflowId && stepId) {
return GetExistingStepSchemaCommand.create({
organizationId: user.organizationId,
Expand All @@ -39,10 +40,14 @@ export function createGetStepSchemaCommand(
});
}

return GetStepTypeSchemaCommand.create({
organizationId: user.organizationId,
environmentId: user.environmentId,
userId: user._id,
stepType,
});
if (stepType) {
return GetStepTypeSchemaCommand.create({
organizationId: user.organizationId,
environmentId: user.environmentId,
userId: user._id,
stepType,
});
}

throw new BadRequestException('Invalid command, either workflowId and stepId or stepType is required');
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from './get-step-schema.command';
import { StepSchemaDto } from '../../dtos/step-schema.dto';
import { mapStepTypeToOutput, mapStepTypeToResult } from '../../shared';
import { encodeBase62 } from '../../../shared/helpers';

@Injectable()
export class GetStepSchemaUseCase {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/workflows-v2/generate-preview.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('Generate Preview', () => {
throw new Error(`Failed to create workflow ${JSON.stringify(workflowResult.error)}`);
}

return { workflowId: workflowResult.value._id, stepUuid: workflowResult.value.steps[0].stepUuid };
return { workflowId: workflowResult.value._id, stepUuid: workflowResult.value.steps[0]._id };
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DEFAULT_WORKFLOW_PREFERENCES,
PreferencesResponseDto,
PreferencesTypeEnum,
ShortIsPrefixEnum,
StepResponseDto,
StepTypeEnum,
WorkflowListResponseDto,
Expand All @@ -13,6 +14,7 @@ import {
} from '@novu/shared';
import { ControlValuesEntity, NotificationStepEntity, NotificationTemplateEntity } from '@novu/dal';
import { GetPreferencesResponseDto } from '@novu/application-generic';
import { encodeBase62 } from '../../shared/helpers';

export function toResponseWorkflowDto(
template: NotificationTemplateEntity,
Expand All @@ -23,15 +25,17 @@ export function toResponseWorkflowDto(
user: preferences?.source[PreferencesTypeEnum.USER_WORKFLOW] || null,
default: preferences?.source[PreferencesTypeEnum.WORKFLOW_RESOURCE] || DEFAULT_WORKFLOW_PREFERENCES,
};
const workflowName = template.name || 'Missing Name';

return {
_id: template._id,
slug: `${ShortIsPrefixEnum.WORKFLOW}${encodeBase62(template._id)}`,
workflowId: template.triggers[0].identifier,
name: workflowName,
tags: template.tags,
active: template.active,
preferences: preferencesDto,
steps: getSteps(template, stepIdToControlValuesMap),
name: template.name,
workflowId: template.triggers[0].identifier,
description: template.description,
origin: computeOrigin(template),
updatedAt: template.updatedAt || 'Missing Updated At',
Expand All @@ -55,10 +59,13 @@ function getSteps(template: NotificationTemplateEntity, controlValuesMap: { [p:
}

function toMinifiedWorkflowDto(template: NotificationTemplateEntity): WorkflowListResponseDto {
const workflowName = template.name || 'Missing Name';

return {
origin: computeOrigin(template),
_id: template._id,
name: template.name,
slug: `${ShortIsPrefixEnum.WORKFLOW}${encodeBase62(template._id)}`,
name: workflowName,
origin: computeOrigin(template),
tags: template.tags,
updatedAt: template.updatedAt || 'Missing Updated At',
stepTypeOverviews: template.steps.map(buildStepTypeOverview).filter((stepTypeEnum) => !!stepTypeEnum),
Expand All @@ -72,15 +79,17 @@ export function toWorkflowsMinifiedDtos(templates: NotificationTemplateEntity[])
}

function toStepResponseDto(step: NotificationStepEntity): StepResponseDto {
const stepName = step.name || 'Missing Name';

return {
name: step.name || 'Missing Name',
slug: step.stepId || 'Missing Name',
stepUuid: step._templateId,
_id: step._templateId,
slug: `${ShortIsPrefixEnum.STEP}${encodeBase62(step._templateId)}`,
name: stepName,
stepId: step.stepId || 'Missing Step Id',
type: step.template?.type || StepTypeEnum.EMAIL,
controls: convertControls(step),
controlValues: step.controlVariables || {},
};
} satisfies StepResponseDto;
}

function convertControls(step: NotificationStepEntity): ControlsSchema {
Expand Down
Loading

0 comments on commit 0e27b89

Please sign in to comment.