Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions prisma/basePrismaClient.ts

This file was deleted.

14 changes: 0 additions & 14 deletions spec/__mocks__/singleton.ts

This file was deleted.

62 changes: 0 additions & 62 deletions spec/util/generateSlug.spec.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import handleApiKey from './util/handleApiKey.js';
import { IncomingHttpHeaders } from 'http';
import { DataSources, createDataSources } from './datasources/index.js';
import { Organisation } from '@prisma/client';
import { prismaClient } from './db.js';
interface Request {
headers: IncomingHttpHeaders;
}
Expand All @@ -27,7 +27,7 @@ export const createContext = async ({
let auth: Auth | null = null;
if (req.headers['x-api-key']) {
const apiKey = req.headers['x-api-key'] as string;
const organisation = await handleApiKey(apiKey);
const organisation = await prismaClient.getByApiKey(apiKey);
if (!organisation) throw new Error('Invalid API key provided');
console.log('Valid API key found for: ', organisation.name);
auth = {
Expand Down
113 changes: 83 additions & 30 deletions src/db.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,86 @@
import { Prisma } from '@prisma/client';
import { PrismaClient, Organisation } from '@prisma/client';
import {
PrismaInterface,
SlugOptionalOrganisationCreateInput,
} from './interfaces/prisma.js';
import { generateSlug } from './util/generateSlug.js';
import prisma from '../prisma/basePrismaClient.js';

type SlugOptionalOrganisationCreateInput = Omit<
Prisma.OrganisationCreateInput,
'slug'
> & { slug?: string };

const db = prisma.$extends({
name: 'slugify',
model: {
organisation: {
async createWithSlug(data: SlugOptionalOrganisationCreateInput) {
const slugifiedName = await generateSlug(data.name);
return prisma.organisation.create({
data: {
...data,
slug: slugifiedName,
},
});
},
async getByApiKey(apiKey: string) {
return prisma.apiKey.findFirst({
where: { apiKey },
include: { organisation: true },
});

export class PrismaDB implements PrismaInterface {
prisma: PrismaClient | null = null;

async initialiseClient() {
this.prisma = new PrismaClient();
return this.prisma;
}

async createWithSlug(data: SlugOptionalOrganisationCreateInput) {
if (!this.prisma) return null;

const slugifiedName = generateSlug(data.name);
const slug = await this.getSlug(slugifiedName);
return this.prisma.organisation.create({
data: {
...data,
slug: slug,
},
},
},
});
});
}

async getByApiKey(apiKey: string): Promise<Organisation | null> {
if (!this.prisma) return null;
const apiKeyResult = await this.prisma.apiKey.findFirst({
where: { apiKey },
include: { organisation: true },
});
if (!apiKeyResult) throw new Error('Invalid API key provided');

if (!apiKeyResult.isEnabled) {
throw new Error(
'Your API key is not enabled. Please renew your subscription or contact Testerloop support.'
);
}
return apiKeyResult.organisation;
}

async getSlug(slug: string, index: number = 0): Promise<string> {
if (!this.prisma) return slug;

const currentSlug = index === 0 ? slug : `${slug}-${index}`;
const existingOrganization = await this.prisma.organisation.findUnique({
where: { slug: currentSlug },
});

if (!existingOrganization) {
return currentSlug;
}

return this.getSlug(slug, index + 1);
}
}

export class NoPrismaDB implements PrismaInterface {
private throwError(): never {
throw new Error('Database operations are not allowed');
}

async initialiseClient(): Promise<PrismaClient | null> {
this.throwError();
}

async createWithSlug(
data: SlugOptionalOrganisationCreateInput
): Promise<Organisation | null> {
this.throwError();
}

async getByApiKey(apiKey: string): Promise<Organisation | null> {
this.throwError();
}

async getSlug(slug: string, index: number = 0): Promise<string> {
this.throwError();
}
}

export default db;
export const prismaClient: PrismaInterface =
process.env.ENABLE_DB === 'true' ? new PrismaDB() : new NoPrismaDB();
16 changes: 16 additions & 0 deletions src/interfaces/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PrismaClient, Prisma } from '@prisma/client';
import { Organisation } from '@prisma/client';

export type SlugOptionalOrganisationCreateInput = Omit<
Prisma.OrganisationCreateInput,
'slug'
> & { slug?: string };

export interface PrismaInterface {
initialiseClient: () => Promise<PrismaClient | null>;
createWithSlug: (
data: SlugOptionalOrganisationCreateInput
) => Promise<Organisation | null>;
getByApiKey: (apiKey: string) => Promise<Organisation | null>;
getSlug: (slug: string, index?: number) => Promise<string> | null;
}
40 changes: 17 additions & 23 deletions src/resolvers/Mutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
import { MutationResolvers } from './types/generated';
import { UploadInfo, TestExecutionCreationResponse } from './types/generated';
import { getBucketAndPath } from '../util/getBucketAndPath.js';
import { getOrganisationIdentifier } from '../util/getOrganisationIdentifier.js';

const resolvers: MutationResolvers = {
createTestRun: async (
Expand All @@ -11,23 +13,15 @@ const resolvers: MutationResolvers = {
): Promise<UploadInfo> => {
const runID = uuidv4();
console.log('Creating run with ID: ', runID);
let s3BucketName;
let customerPath;
if (!s3Config) {
if (!auth) {
throw new Error('Authorization required');
}
const organisation = auth.organisation;
customerPath = organisation.s3CustomPath;
s3BucketName = organisation.s3BucketName;
} else {
console.log('Using s3Config');
({ customerPath, bucket: s3BucketName } = s3Config);
}

const { s3BucketName, customerPath } = await getBucketAndPath(
auth,
s3Config || {}
);

if (!customerPath || !s3BucketName) {
throw new Error(
'Invalid configuration. Please provide s3BucketName and customerPath.'
'Failed to verify organisation details. Please provide API key or s3Config.'
);
}
const s3RunPath = `${s3BucketName}/${customerPath}/${runID}`;
Expand Down Expand Up @@ -65,15 +59,15 @@ const resolvers: MutationResolvers = {
{ testName, featureFile, s3Config },
{ auth }
): Promise<TestExecutionCreationResponse> => {
let organisationIdentifier;
if (!s3Config) {
if (!auth) {
throw new Error('Authorization required');
}
organisationIdentifier = auth.organisation.id;
} else {
console.log('Using s3Config');
organisationIdentifier = s3Config.customerPath;
const organisationIdentifier = getOrganisationIdentifier(
auth,
s3Config || {}
);

if (!organisationIdentifier) {
throw new Error(
'Failed to verify organisation details. Please provide API key or s3Config'
);
}

const NAMESPACE = 'c9412f45-51ba-4b4d-9867-6117fb1646e1';
Expand Down
18 changes: 1 addition & 17 deletions src/util/generateSlug.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import slugify from 'slugify';
import prisma from '../../prisma/basePrismaClient.js';

export async function getSlug(slug: string, index: number = 0): Promise<string> {
const currentSlug = index === 0 ? slug : `${slug}-${index}`;
const existingOrganization = await prisma.organisation.findUnique({
where: { slug: currentSlug },
});

if (!existingOrganization) {
return currentSlug;
}

return getSlug(slug, index + 1);
}

export function generateSlug(name: string) {
const slugifiedNameBase = slugify(name, {
return slugify(name, {
lower: true,
strict: true,
});

return getSlug(slugifiedNameBase);
}
29 changes: 29 additions & 0 deletions src/util/getBucketAndPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Organisation } from '@prisma/client';
import { S3Config } from 'src/resolvers/types/generated';

export async function getBucketAndPath(
auth: { organisation: Organisation } | null,
s3Config: S3Config
) {
let s3BucketName;
let customerPath;
if (!s3Config) {
if (!auth) {
throw new Error('Authorization required');
}
const organisation = auth.organisation;
customerPath = organisation.s3CustomPath;
s3BucketName = organisation.s3BucketName;
} else {
console.log('Using s3Config');
({ customerPath, bucket: s3BucketName } = s3Config);
}

if (!customerPath || !s3BucketName) {
throw new Error(
'Invalid configuration. Please provide s3BucketName and customerPath.'
);
}

return { s3BucketName, customerPath };
}
18 changes: 18 additions & 0 deletions src/util/getOrganisationIdentifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Organisation } from '@prisma/client';
import { S3Config } from 'src/resolvers/types/generated';

export function getOrganisationIdentifier(
auth: { organisation: Organisation } | null,
s3Config: S3Config
): string {
if (s3Config && s3Config.customerPath) {
console.log('Using s3Config');
return s3Config.customerPath;
}

if (!auth) {
throw new Error('Authorization required');
}

return auth.organisation.id;
}
19 changes: 0 additions & 19 deletions src/util/handleApiKey.ts

This file was deleted.