Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/paragraph-extraction #7623

Open
wants to merge 14 commits into
base: production
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,11 @@ module.exports = {
excludedFiles: './**/*.cy.tsx',
parser: '@typescript-eslint/parser',
parserOptions: { project: './tsconfig.json' },
rules: { ...rules },
rules: {
...rules,
'no-empty-function': ['error', { allow: ['constructors'] }],
'no-useless-constructor': 'off',
},
},
{
files: ['./cypress/**/*.ts', './cypress/**/*.d.ts', './**/*.cy.tsx'],
Expand Down
38 changes: 38 additions & 0 deletions app/api/common.v2/AbstractController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NextFunction, Request, Response } from 'express';

export type Dependencies = {
next: NextFunction;
res: Response;
};

export abstract class AbstractController {
protected next: NextFunction;

protected res: Response;

constructor({ next, res }: Dependencies) {
this.next = next;
this.res = res;
}

abstract handle(request: Request): Promise<void>;

serverError(error: Error) {
// Logging ?

return this.res.status(500).json({
message: error.message,
});
}

clientError(message: string) {
// Should we log this ?
// What about negative impacts spam on Notifications channel ?
return this.res.status(400).json({ message });
}

jsonResponse(body: any) {
this.res.status(200).json(body);
this.next();
}
}
3 changes: 3 additions & 0 deletions app/api/common.v2/contracts/UseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface UseCase<Input, Output> {
execute(input: Input): Promise<Output>;
}
58 changes: 58 additions & 0 deletions app/api/paragraphExtraction/application/PXCreateExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { z } from 'zod';

import { UseCase } from 'api/common.v2/contracts/UseCase';
import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource';

import { IdGenerator } from 'api/common.v2/contracts/IdGenerator';
import { PXExtractor } from '../domain/PXExtractor';
import { PXExtractorsDataSource } from '../domain/PXExtractorDataSource';
import { PXErrorCode, PXValidationError } from '../domain/PXValidationError';

type Input = z.infer<typeof InputSchema>;
type Output = PXExtractor;

type Dependencies = {
templatesDS: TemplatesDataSource;
extractorDS: PXExtractorsDataSource;
idGenerator: IdGenerator;
};

const InputSchema = z.object({
targetTemplateId: z.string({ message: 'You should provide a target template' }),
sourceTemplateId: z.string({ message: 'You should provide a source template' }),
});

export class PXCreateExtractor implements UseCase<Input, Output> {
constructor(private dependencies: Dependencies) {}

async execute(input: Input): Promise<Output> {
const [targetTemplate, sourceTemplate] = await Promise.all([
this.dependencies.templatesDS.getById(input.targetTemplateId),
this.dependencies.templatesDS.getById(input.sourceTemplateId),
]);

if (!targetTemplate) {
throw new PXValidationError(
PXErrorCode.TARGET_TEMPLATE_NOT_FOUND,
`Target template with id ${input.targetTemplateId} was not found`
);
}

if (!sourceTemplate) {
throw new PXValidationError(
PXErrorCode.SOURCE_TEMPLATE_NOT_FOUND,
`Source template with id ${input.sourceTemplateId} was not found`
);
}

const extractor = new PXExtractor({
id: this.dependencies.idGenerator.generate(),
targetTemplate,
sourceTemplate,
});

await this.dependencies.extractorDS.create(extractor);

return extractor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { ObjectId } from 'mongodb';

import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults';
import { getConnection } from 'api/common.v2/database/getConnectionForCurrentTenant';
import { MongoIdHandler } from 'api/common.v2/database/MongoIdGenerator';
import { DefaultTemplatesDataSource } from 'api/templates.v2/database/data_source_defaults';
import { getFixturesFactory } from 'api/utils/fixturesFactory';
import { testingEnvironment } from 'api/utils/testingEnvironment';

import { PXErrorCode } from 'api/paragraphExtraction/domain/PXValidationError';
import {
mongoPXExtractorsCollection,
MongoPXExtractorsDataSource,
} from '../../infrastructure/MongoPXExtractorsDataSource';
import { PXCreateExtractor } from '../PXCreateExtractor';

const factory = getFixturesFactory();

const setUpUseCase = () => {
const transaction = DefaultTransactionManager();
const templatesDS = DefaultTemplatesDataSource(transaction);

const extractorDS = new MongoPXExtractorsDataSource(getConnection(), transaction);

return {
createExtractor: new PXCreateExtractor({
extractorDS,
templatesDS,
idGenerator: MongoIdHandler,
}),
};
};

const sourceTemplate = factory.template('Source Template', [factory.property('text', 'text')]);
const targetTemplate = factory.template('Target Template', [
factory.property('rich_text', 'markdown'),
]);
const invalidTargetTemplate = factory.template('Invalid Target');

describe('PXCreateExtractor', () => {
beforeEach(async () => {
await testingEnvironment.setUp({
templates: [sourceTemplate, targetTemplate, invalidTargetTemplate],
});
});

afterAll(async () => {
await testingEnvironment.tearDown();
});

it('should create an Extractor correctly', async () => {
const { createExtractor } = setUpUseCase();

await createExtractor.execute({
sourceTemplateId: sourceTemplate._id.toString(),
targetTemplateId: targetTemplate._id.toString(),
});

const dbPXExtractors = await testingEnvironment.db.getAllFrom(mongoPXExtractorsCollection);

expect(dbPXExtractors).toEqual([
{
_id: expect.any(ObjectId),
sourceTemplateId: expect.any(ObjectId),
targetTemplateId: expect.any(ObjectId),
},
]);
});

it('should throw if target Template does not exist', async () => {
const { createExtractor } = setUpUseCase();
const targetTemplateId = new ObjectId().toString();

const promise = createExtractor.execute({
sourceTemplateId: sourceTemplate._id.toString(),
targetTemplateId,
});

await expect(promise).rejects.toMatchObject({
code: PXErrorCode.TARGET_TEMPLATE_NOT_FOUND,
});
});

it('should throw if source Template does not exist', async () => {
const { createExtractor } = setUpUseCase();
const sourceTemplateId = new ObjectId().toString();

const promise = createExtractor.execute({
targetTemplateId: sourceTemplate._id.toString(),
sourceTemplateId,
});

await expect(promise).rejects.toMatchObject({
code: PXErrorCode.SOURCE_TEMPLATE_NOT_FOUND,
});
});

it('should throw if target template is not valid for create an Extractor', async () => {
const { createExtractor } = setUpUseCase();

const promise = createExtractor.execute({
sourceTemplateId: sourceTemplate._id.toString(),
targetTemplateId: invalidTargetTemplate._id.toString(),
});

await expect(promise).rejects.toMatchObject({
code: PXErrorCode.TARGET_TEMPLATE_INVALID,
});

const dbPXExtractors = await testingEnvironment.db.getAllFrom(mongoPXExtractorsCollection);
expect(dbPXExtractors).toEqual([]);
});

it('should throw if target and source template are the same', async () => {
const { createExtractor } = setUpUseCase();

const promise = createExtractor.execute({
sourceTemplateId: targetTemplate._id.toString(),
targetTemplateId: targetTemplate._id.toString(),
});

await expect(promise).rejects.toMatchObject({
code: PXErrorCode.TARGET_SOURCE_TEMPLATE_EQUAL,
});

const dbPXExtractors = await testingEnvironment.db.getAllFrom(mongoPXExtractorsCollection);
expect(dbPXExtractors).toEqual([]);
});
});
40 changes: 40 additions & 0 deletions app/api/paragraphExtraction/domain/PXExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Template } from 'api/templates.v2/model/Template';
import { PXValidationError, PXErrorCode } from './PXValidationError';

export type PXExtractorProps = {
id: string;
sourceTemplate: Template;
targetTemplate: Template;
};

export class PXExtractor {
id: string;

targetTemplate: Template;

sourceTemplate: Template;

constructor(props: PXExtractorProps) {
this.id = props.id;
this.targetTemplate = props.targetTemplate;
this.sourceTemplate = props.sourceTemplate;

this.validate();
}

private validate() {
if (!this.targetTemplate.getPropertiesByType('markdown').length) {
throw new PXValidationError(
PXErrorCode.TARGET_TEMPLATE_INVALID,
`Target template with id ${this.targetTemplate.id} should have at least one rich text property`
);
}

if (this.targetTemplate.id === this.sourceTemplate.id) {
throw new PXValidationError(
PXErrorCode.TARGET_SOURCE_TEMPLATE_EQUAL,
'Target and Source template cannot be the same'
);
}
}
}
5 changes: 5 additions & 0 deletions app/api/paragraphExtraction/domain/PXExtractorDataSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PXExtractor } from './PXExtractor';

export interface PXExtractorsDataSource {
create(extractor: PXExtractor): Promise<void>;
}
17 changes: 17 additions & 0 deletions app/api/paragraphExtraction/domain/PXValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export enum PXErrorCode {
TARGET_TEMPLATE_NOT_FOUND = 'TARGET_TEMPLATE_NOT_FOUND',
SOURCE_TEMPLATE_NOT_FOUND = 'SOURCE_TEMPLATE_NOT_FOUND',
TARGET_TEMPLATE_INVALID = 'TARGET_TEMPLATE_INVALID',
TARGET_SOURCE_TEMPLATE_EQUAL = 'TARGET_SOURCE_TEMPLATE_EQUAL',
}

export class PXValidationError extends Error {
constructor(
public code: PXErrorCode,
message: string,
options?: ErrorOptions
) {
super(message, options);
this.name = 'PXValidationError';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ObjectId } from 'mongodb';

export type MongoPXExtractorDBO = {
_id: ObjectId;
targetTemplateId: ObjectId;
sourceTemplateId: ObjectId;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MongoDataSource } from 'api/common.v2/database/MongoDataSource';
import { ObjectId } from 'mongodb';
import { PXExtractor } from '../domain/PXExtractor';
import { PXExtractorsDataSource } from '../domain/PXExtractorDataSource';
import { MongoPXExtractorDBO } from './MongoPXExtractorDBO';

export const mongoPXExtractorsCollection = 'px_extractors';

export class MongoPXExtractorsDataSource
extends MongoDataSource<MongoPXExtractorDBO>
implements PXExtractorsDataSource
{
protected collectionName = mongoPXExtractorsCollection;

async create(extractor: PXExtractor): Promise<void> {
const mongoExtractor: MongoPXExtractorDBO = {
_id: new ObjectId(extractor.id),
sourceTemplateId: new ObjectId(extractor.sourceTemplate.id),
targetTemplateId: new ObjectId(extractor.targetTemplate.id),
};

await this.getCollection().insertOne(mongoExtractor);
}
}
1 change: 1 addition & 0 deletions app/api/templates.v2/contracts/TemplatesDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RelationshipProperty } from '../model/RelationshipProperty';
import { Template } from '../model/Template';

export interface TemplatesDataSource {
getAll(): ResultSet<Template>;
getAllTemplatesIds(): ResultSet<string>;
getAllRelationshipProperties(): ResultSet<RelationshipProperty>;
getAllProperties(): ResultSet<Property>;
Expand Down
4 changes: 4 additions & 0 deletions app/api/templates.v2/database/MongoTemplatesDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export class MongoTemplatesDataSource

private _nameToPropertyMap?: Record<string, Property>;

getAll() {
return new MongoResultSet(this.getCollection().find({}), TemplateMappers.toApp);
}

getAllRelationshipProperties() {
const cursor = this.getCollection().aggregate([
{
Expand Down
6 changes: 5 additions & 1 deletion app/api/templates.v2/model/Template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { objectIndex } from 'shared/data_utils/objectIndex';
import { Property, PropertyUpdateInfo } from './Property';
import { Property, PropertyTypes, PropertyUpdateInfo } from './Property';

class Template {
readonly id: string;
Expand Down Expand Up @@ -59,6 +59,10 @@ class Template {

return null;
}

getPropertiesByType(type: PropertyTypes) {
return this.properties.filter(p => p.type === type);
}
}

export { Template };
6 changes: 6 additions & 0 deletions app/api/utils/testingEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const testingEnvironment = {
async tearDown() {
await testingDB.disconnect();
},

db: {
async getAllFrom(collectionName: string) {
return testingDB.mongodb?.collection(collectionName).find().toArray();
},
},
};

export { testingEnvironment };
Loading
Loading