Skip to content

Commit 3da1441

Browse files
Johan BookJohan Book
Johan Book
authored and
Johan Book
committed
feat(api): add organization journal
1 parent 01970ca commit 3da1441

14 files changed

+187
-44
lines changed

services/api/scripts/generate-migration

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
set -e
55

66
DATA_SOURCE=dist/core/database/data-source.js
7-
MIGRATION_DIRECTORY=./src/core/database/migrations
7+
MIGRATION_DIRECTORY=./src/core/database/infrastructure/migrations
88
MIGRATION_NAME=$1
99

1010
# Check that `MIGRATION_NAME` is defined

services/api/scripts/run-migrations

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ set -e
66
sh ./scripts/create-database-if-not-exists
77

88
DATA_SOURCE=./dist/core/database/data-source.js
9-
MIGRATION_DIRECTORY=./dist/core/database/migrations
9+
MIGRATION_DIRECTORY=./dist/core/database/infrastructure/migrations
1010

1111
# Check that `MIGRATION_DIRECTORY` exists
1212
if [ ! -d "$MIGRATION_DIRECTORY" ]; then

services/api/src/core/journal/application/contracts/dtos/journal-entry-details.dto.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ export class JournalEntryDetails {
55
@Length(0, 1024)
66
public readonly commandName!: string;
77

8-
public readonly created!: Date;
8+
public readonly createdAt!: Date;
99

10-
public readonly id!: number;
10+
public readonly id!: string;
1111

1212
public readonly payload!: unknown;
1313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Type } from "class-transformer";
2+
import { IsDate } from "class-validator";
3+
4+
import { BaseQuery } from "src/core/query";
5+
6+
export class GetCurrentOrganizationJournalQuery extends BaseQuery {
7+
@IsDate()
8+
@Type(() => Date)
9+
from!: Date;
10+
11+
@IsDate()
12+
@Type(() => Date)
13+
to!: Date;
14+
}

services/api/src/core/journal/application/contracts/queries/get-journal.query.ts renamed to services/api/src/core/journal/application/contracts/queries/get-profile-journal.query.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IsDate } from "class-validator";
33

44
import { BaseQuery } from "src/core/query";
55

6-
export class GetJournalQuery extends BaseQuery {
6+
export class GetProfileJournalQuery extends BaseQuery {
77
@IsDate()
88
@Type(() => Date)
99
from!: Date;

services/api/src/core/journal/application/handlers/command-handlers/create-journal-entry.handler.spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { Repository } from "typeorm";
22

33
import { UserIdService } from "src/core/authentication";
44
import { map } from "src/core/mapper";
5+
import { CurrentOrganizationService } from "src/core/organizations";
6+
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
7+
import { CurrentProfileService, Profile } from "src/core/profiles";
58
import { createMockRepository, createUserIdServiceMock } from "src/test/mocks";
69

710
import { JournalEntry } from "../../../infrastructure/entities/journal-entry.entity";
@@ -11,15 +14,24 @@ import { CreateJournalEntryHandler } from "./create-journal-entry.handler";
1114
describe(CreateJournalEntryHandler.name, () => {
1215
let commandHandler: CreateJournalEntryHandler;
1316
let journalEntries: Repository<JournalEntry>;
17+
let currentOrganizationService: CurrentOrganizationService;
18+
let currentProfileService: CurrentProfileService;
19+
let profiles: Repository<Profile>;
1420
let userIdService: UserIdService;
1521

1622
beforeEach(() => {
23+
profiles = createMockRepository<Profile>([{} as any]);
1724
journalEntries = createMockRepository<JournalEntry>();
1825
userIdService = createUserIdServiceMock();
1926

27+
currentOrganizationService = createCurrentOrganizationMock();
28+
29+
currentProfileService = new CurrentProfileService(profiles, userIdService);
30+
2031
commandHandler = new CreateJournalEntryHandler(
32+
currentOrganizationService,
33+
currentProfileService,
2134
journalEntries,
22-
userIdService,
2335
);
2436
});
2537

services/api/src/core/journal/application/handlers/command-handlers/create-journal-entry.handler.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
22
import { InjectRepository } from "@nestjs/typeorm";
33
import { Repository } from "typeorm";
44

5-
import { UserIdService } from "src/core/authentication";
65
import { MissingUserIdError } from "src/core/authentication";
6+
import { CurrentOrganizationService } from "src/core/organizations";
7+
import { CurrentProfileService } from "src/core/profiles";
78
import { redactBinaries } from "src/utils/object.helper";
89

910
import { JournalEntry } from "../../../infrastructure/entities/journal-entry.entity";
@@ -14,18 +15,22 @@ export class CreateJournalEntryHandler
1415
implements ICommandHandler<CreateJournalEntryCommand, void>
1516
{
1617
constructor(
18+
private readonly currentOrganizationService: CurrentOrganizationService,
19+
private readonly currentProfileService: CurrentProfileService,
1720
@InjectRepository(JournalEntry)
1821
private readonly journalEntries: Repository<JournalEntry>,
19-
private readonly userIdService: UserIdService,
2022
) {}
2123

2224
async execute(command: CreateJournalEntryCommand) {
23-
let userId: string;
25+
let organizationId;
26+
let profileId;
2427

2528
// User ID will not be available for system-issued commands (e.g. event handlers)
2629
// This skips logging any commands where user id cannot be found
2730
try {
28-
userId = this.userIdService.getUserId();
31+
organizationId =
32+
await this.currentOrganizationService.fetchCurrentOrganizationId();
33+
profileId = await this.currentProfileService.fetchCurrentProfileId();
2934
} catch (error) {
3035
if (error instanceof MissingUserIdError) {
3136
return;
@@ -37,8 +42,9 @@ export class CreateJournalEntryHandler
3742
const newJournalEntry = new JournalEntry();
3843

3944
newJournalEntry.commandName = command.commandName;
45+
newJournalEntry.organizationId = organizationId;
4046
newJournalEntry.payload = redactBinaries(command.payload);
41-
newJournalEntry.userId = userId;
47+
newJournalEntry.profileId = profileId;
4248

4349
await this.journalEntries.save(newJournalEntry);
4450
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { IQueryHandler, QueryHandler } from "@nestjs/cqrs";
2+
import { InjectRepository } from "@nestjs/typeorm";
3+
import { Between, Repository } from "typeorm";
4+
5+
import { map, mapArray } from "src/core/mapper";
6+
import { CurrentOrganizationService } from "src/core/organizations";
7+
import { QueryService } from "src/core/query";
8+
9+
import { JournalEntry } from "../../../infrastructure/entities/journal-entry.entity";
10+
import { JournalDetails } from "../../contracts/dtos/journal-details.dto";
11+
import { JournalEntryDetails } from "../../contracts/dtos/journal-entry-details.dto";
12+
import { GetCurrentOrganizationJournalQuery } from "../../contracts/queries/get-current-organization-journal.query";
13+
14+
function formatCommandName(commandName: string): string {
15+
return commandName.replace(/Command$/, "");
16+
}
17+
18+
@QueryHandler(GetCurrentOrganizationJournalQuery)
19+
export class GetCurrentOrganizationJournalHandler
20+
implements IQueryHandler<GetCurrentOrganizationJournalQuery, JournalDetails>
21+
{
22+
constructor(
23+
private readonly currentOrganizationService: CurrentOrganizationService,
24+
@InjectRepository(JournalEntry)
25+
private readonly journalEntries: Repository<JournalEntry>,
26+
private readonly queryService: QueryService<JournalEntry>,
27+
) {}
28+
29+
async execute(query: GetCurrentOrganizationJournalQuery) {
30+
const currentOrganizationId =
31+
await this.currentOrganizationService.fetchCurrentOrganizationId();
32+
33+
const foundJournalEntries = await this.queryService.find(
34+
this.journalEntries,
35+
{
36+
default: {
37+
order: { createdAt: "desc" },
38+
},
39+
query,
40+
required: {
41+
where: {
42+
createdAt: Between(query.from, query.to),
43+
organizationId: currentOrganizationId,
44+
},
45+
},
46+
},
47+
);
48+
49+
return map(JournalDetails, {
50+
entries: mapArray(JournalEntryDetails, foundJournalEntries, (entry) => ({
51+
commandName: formatCommandName(entry.commandName),
52+
createdAt: entry.createdAt,
53+
id: entry.id,
54+
payload: entry.payload,
55+
})),
56+
});
57+
}
58+
}

services/api/src/core/journal/application/handlers/query-handlers/get-journal.handler.ts renamed to services/api/src/core/journal/application/handlers/query-handlers/get-profile-journal.handler.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
11
import { IQueryHandler, QueryHandler } from "@nestjs/cqrs";
22
import { InjectRepository } from "@nestjs/typeorm";
3-
import { And, LessThan, MoreThan, Repository } from "typeorm";
3+
import { Between, Repository } from "typeorm";
44

5-
import { UserIdService } from "src/core/authentication";
65
import { map, mapArray } from "src/core/mapper";
6+
import { CurrentProfileService } from "src/core/profiles";
77
import { QueryService } from "src/core/query";
88

99
import { JournalEntry } from "../../../infrastructure/entities/journal-entry.entity";
1010
import { JournalDetails } from "../../contracts/dtos/journal-details.dto";
1111
import { JournalEntryDetails } from "../../contracts/dtos/journal-entry-details.dto";
12-
import { GetJournalQuery } from "../../contracts/queries/get-journal.query";
12+
import { GetProfileJournalQuery } from "../../contracts/queries/get-profile-journal.query";
1313

1414
function formatCommandName(commandName: string): string {
1515
return commandName.replace(/Command$/, "");
1616
}
1717

18-
@QueryHandler(GetJournalQuery)
19-
export class GetJournalHandler
20-
implements IQueryHandler<GetJournalQuery, JournalDetails>
18+
@QueryHandler(GetProfileJournalQuery)
19+
export class GetProfileJournalHandler
20+
implements IQueryHandler<GetProfileJournalQuery, JournalDetails>
2121
{
2222
constructor(
23+
private readonly currentProfileService: CurrentProfileService,
2324
@InjectRepository(JournalEntry)
2425
private readonly journalEntries: Repository<JournalEntry>,
2526
private readonly queryService: QueryService<JournalEntry>,
26-
private readonly userIdService: UserIdService,
2727
) {}
2828

29-
async execute(query: GetJournalQuery) {
30-
const userId = this.userIdService.getUserId();
29+
async execute(query: GetProfileJournalQuery) {
30+
const currentProfileId =
31+
await this.currentProfileService.fetchCurrentProfileId();
3132

3233
const foundJournalEntries = await this.queryService.find(
3334
this.journalEntries,
3435
{
3536
default: {
36-
order: { created: "desc" },
37+
order: { createdAt: "desc" },
3738
},
3839
query,
3940
required: {
4041
where: {
41-
created: And(LessThan(query.to), MoreThan(query.from)),
42-
userId,
42+
createdAt: Between(query.from, query.to),
43+
profileId: currentProfileId,
4344
},
4445
},
4546
},
@@ -48,7 +49,7 @@ export class GetJournalHandler
4849
return map(JournalDetails, {
4950
entries: mapArray(JournalEntryDetails, foundJournalEntries, (entry) => ({
5051
commandName: formatCommandName(entry.commandName),
51-
created: entry.created,
52+
createdAt: entry.createdAt,
5253
id: entry.id,
5354
payload: entry.payload,
5455
})),

services/api/src/core/journal/client/controllers/journal.controller.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ import { QueryBus } from "@nestjs/cqrs";
33
import { ApiTags } from "@nestjs/swagger";
44

55
import { JournalDetails } from "../../application/contracts/dtos/journal-details.dto";
6-
import { GetJournalQuery } from "../../application/contracts/queries/get-journal.query";
6+
import { GetCurrentOrganizationJournalQuery } from "../../application/contracts/queries/get-current-organization-journal.query";
7+
import { GetProfileJournalQuery } from "../../application/contracts/queries/get-profile-journal.query";
78

89
@Controller("journal")
910
@ApiTags("journal")
1011
export class JournalController {
1112
constructor(private queryBus: QueryBus) {}
1213

13-
@Get()
14-
async getJournal(@Query() query: GetJournalQuery): Promise<JournalDetails> {
14+
@Get("/current-organization")
15+
async getCurrentOrganizationJournal(
16+
@Query() query: GetCurrentOrganizationJournalQuery,
17+
): Promise<JournalDetails> {
18+
return await this.queryBus.execute(query);
19+
}
20+
21+
@Get("/profile")
22+
async getProfileJournal(
23+
@Query() query: GetProfileJournalQuery,
24+
): Promise<JournalDetails> {
1525
return await this.queryBus.execute(query);
1626
}
1727
}
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import {
2-
CreateDateColumn,
3-
Column,
4-
Entity,
5-
PrimaryGeneratedColumn,
6-
} from "typeorm";
1+
import { Column, Entity, ManyToOne } from "typeorm";
72

8-
@Entity()
9-
export class JournalEntry {
10-
@PrimaryGeneratedColumn()
11-
id!: number;
12-
13-
@CreateDateColumn()
14-
created!: Date;
3+
import { BaseEntity } from "src/core/database";
4+
import { Organization } from "src/core/organizations";
5+
import { Profile } from "src/core/profiles";
156

7+
@Entity()
8+
export class JournalEntry extends BaseEntity {
169
@Column("text")
1710
commandName!: string;
1811

12+
@ManyToOne(() => Organization)
13+
organization!: Organization;
14+
15+
@Column()
16+
organizationId!: number;
17+
1918
@Column("json")
2019
payload!: unknown;
2120

22-
@Column("uuid")
23-
userId!: string;
21+
@ManyToOne(() => Profile)
22+
profile!: Profile;
23+
24+
@Column()
25+
profileId!: number;
2426
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
import { MigrationError } from "src/core/error-handling";
4+
5+
export class DropJournalTable1699731481699 implements MigrationInterface {
6+
name = 'DropJournalTable1699731481699'
7+
8+
public async up(queryRunner: QueryRunner): Promise<void> {
9+
await queryRunner.query(`DROP TABLE "journal_entry"`);
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
throw new MigrationError("Not supported")
14+
}
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class CreateJournalEntryV21699732429511 implements MigrationInterface {
4+
name = 'CreateJournalEntryV21699732429511'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "journal_entry" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "commandName" text NOT NULL, "organizationId" integer NOT NULL, "payload" json NOT NULL, "profileId" integer NOT NULL, CONSTRAINT "PK_69167f660c807d2aa178f0bd7e6" PRIMARY KEY ("id"))`);
8+
await queryRunner.query(`ALTER TABLE "journal_entry" ADD CONSTRAINT "FK_8ab2ce48d25de7470897d4970f3" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
9+
await queryRunner.query(`ALTER TABLE "journal_entry" ADD CONSTRAINT "FK_6747365996c833749ba53dd39f8" FOREIGN KEY ("profileId") REFERENCES "profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query(`ALTER TABLE "journal_entry" DROP CONSTRAINT "FK_6747365996c833749ba53dd39f8"`);
14+
await queryRunner.query(`ALTER TABLE "journal_entry" DROP CONSTRAINT "FK_8ab2ce48d25de7470897d4970f3"`);
15+
await queryRunner.query(`DROP TABLE "journal_entry"`);
16+
}
17+
18+
}

services/api/src/core/journal/journal.module.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { QueryModule } from "src/core/query/query.module";
77

88
import { AuthenticationModule } from "../authentication/authentication.module";
99
import { CreateJournalEntryHandler } from "./application/handlers/command-handlers/create-journal-entry.handler";
10-
import { GetJournalHandler } from "./application/handlers/query-handlers/get-journal.handler";
10+
import { GetCurrentOrganizationJournalHandler } from "./application/handlers/query-handlers/get-current-organization-journal.handler";
11+
import { GetProfileJournalHandler } from "./application/handlers/query-handlers/get-profile-journal.handler";
1112
import { JournalController } from "./client/controllers/journal.controller";
1213
import { JournalEntry } from "./infrastructure/entities/journal-entry.entity";
1314
import { JournalLogger } from "./journal.listener";
@@ -21,6 +22,11 @@ import { JournalLogger } from "./journal.listener";
2122
QueryModule,
2223
],
2324
controllers: [JournalController],
24-
providers: [CreateJournalEntryHandler, GetJournalHandler, JournalLogger],
25+
providers: [
26+
CreateJournalEntryHandler,
27+
GetCurrentOrganizationJournalHandler,
28+
GetProfileJournalHandler,
29+
JournalLogger,
30+
],
2531
})
2632
export class JournalModule {}

0 commit comments

Comments
 (0)