Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
aec3aa4
refac: move DTOs dir up one level
Jan 16, 2025
412a0e9
feat: port over and split up UserDetailsDTO
Jan 16, 2025
b795e48
feat: add UserResponseDTO interface
Jan 16, 2025
6b1d60f
doc: adjust JDoc to reflect reusable method
Jan 16, 2025
2614558
feat!: digest fetch Response in Repository directly
Jan 16, 2025
85ee582
doc: add throw tag to base repo
Jan 16, 2025
61f7029
feat: add helper to extract SimpleResponse from fetch Response
Jan 16, 2025
100df0d
feat!: incorporate new helper
Jan 16, 2025
5dc75a3
feat!: adjust remaining existing User Repo methods to new helper
Jan 16, 2025
cafaedb
fix: correct check for json content type
Jan 16, 2025
7b941b3
docs(changeset): Repository method read json data themselves and retu…
Jan 16, 2025
746e477
test: add test for SimpleResponseDTO helper
Jan 16, 2025
8a675cf
feat: extend and enhance content type differentiation
Jan 16, 2025
65ffe5a
refac: rename module private helper with underscore
Jan 17, 2025
d1739e4
test: extend SimpleResponse Helper Test Coverage
Jan 17, 2025
5e6392c
refac: put helpers on object instead of module scope
Jan 17, 2025
4edb96d
doc: add info to jsdoc about content type
Jan 17, 2025
1046af3
feat: add content type constants
Jan 17, 2025
da903b7
refac: use smarter Headers constructor
Jan 17, 2025
404b8df
feat: add default application/json content type
Jan 17, 2025
c3a5011
refac: sort exports
Jan 17, 2025
5cec4b7
refac: rename to RegistrationDTO
Jan 17, 2025
cc6f78c
refac: transform ResponseDTO into interface and add helper Object
Jan 17, 2025
c6cfab8
refac: extract buildRequest Param to type
Jan 18, 2025
f043134
refac: rename type
Jan 18, 2025
5f0dde3
feat: export missing files from index.js
Jan 18, 2025
dff8a98
fix: make type internal
Jan 18, 2025
62dcd24
feat: add buildRequest variant for JSON bodies
Jan 18, 2025
58c9d3d
refac: rename to SimpleResponseDTO
Jan 18, 2025
ef3079b
feat: add Group Message DTOs for new and existing messages
Jan 18, 2025
eb17af1
feat: add GroupRepository
Jan 18, 2025
47596c2
fix: export missing DTOs
Jan 18, 2025
558ba1e
docs(changeset): Ported over GroupRepository
Jan 18, 2025
3b03696
chore: add changeset script
Jan 18, 2025
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: 5 additions & 0 deletions .changeset/funny-games-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"motidata": minor
---

Ported over GroupRepository
6 changes: 6 additions & 0 deletions .changeset/wild-rats-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"motidata": major
---

Repository methods read json-data themselves and return it as part of the `SimpleResponseDTO` return value.
Remove calls to `Response.json` and instead handle the new return Type `SimpleResponseDTO`.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"watch": "vitest",
"test": "vitest run",
"ci": "npm run lint && npm run check-format && npm run test && npm run check-exports && npm run build",
"changeset": "changeset",
"local-release": "changeset version && changeset publish",
"release": "changeset publish",
"prepublishOnly": "npm run ci"
Expand Down
7 changes: 7 additions & 0 deletions src/DTOs/GroupDetailsDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { GroupMemberProfileDTO } from "./GroupMemberDTO.js";

export interface GroupDetailsDTO {
groupName: string;
members: GroupMemberProfileDTO[];
inviteCode: string;
}
5 changes: 5 additions & 0 deletions src/DTOs/GroupMemberDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface GroupMemberProfileDTO {
userId: string;
username: string;
profileImageUri: string;
}
14 changes: 14 additions & 0 deletions src/DTOs/GroupMessageDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface BaseGroupMessageDTO {
timestamp: string;
content: string;
//TODO: Research which Image Types are returned by Expo and Browser Camera, e.g. Blob or a Stream;
type: "TEXT" | "IMAGE";
isMotiMateMessage: boolean;
}

export interface GroupMessageDTO extends BaseGroupMessageDTO {
messageId: string;
authorId: string | null;
content: string;
clapCount: number;
}
15 changes: 15 additions & 0 deletions src/DTOs/RegistrationDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface RegistrationDTO {
username: string;
email: string;
password: string;
}

export const RegistrationHelper = {
buildFromObject({
username = "",
email = "",
password = "",
}: RegistrationDTO) {
return { username, email, password };
},
};
46 changes: 46 additions & 0 deletions src/DTOs/SimpleResponseDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Serializable } from "child_process";

export interface SimpleResponseDTO<
BodyType extends Serializable | undefined = undefined,
> {
ok: boolean;
statusCode: number;
data?: BodyType;
}

/**
* Collection of {@link SimpleResponseDTO} Helpers
*/
export const SimpleResponseHelpers = {
/**
* Due to limitations of NextJs Server Actions, [this can not be a class](https://react.dev/reference/rsc/use-server#serializable-parameters-and-return-values).
* @throws any `Response.json()` related error
*/
async transformToSimpleResponse<
ResponseDTO extends Serializable | undefined = undefined,
>(fetchResponse: Response): Promise<SimpleResponseDTO<ResponseDTO>> {
return {
ok: fetchResponse.ok,
statusCode: fetchResponse.status,
data: (await this._extractDataBasedOnContentType(
fetchResponse,
)) as ResponseDTO,
};
},

/**
* @private
*/
async _extractDataBasedOnContentType(
fetchResponse: Response,
): Promise<unknown> {
const CONTENT_TYPE = fetchResponse.headers.get("Content-Type");
if (CONTENT_TYPE === null) {
return undefined;
} else if (CONTENT_TYPE.includes("application/json")) {
return await fetchResponse.json();
} else if (CONTENT_TYPE.includes("text/plain")) {
return await fetchResponse.text();
}
},
};
8 changes: 8 additions & 0 deletions src/DTOs/UserDetailsDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { GroupDetailsDTO } from "./GroupDetailsDTO.js";

export interface UserDetailsDTO {
userId: string;
personalGoal: number;
personalProgress: number;
groupInfo: GroupDetailsDTO;
}
57 changes: 57 additions & 0 deletions src/DTOs/__tests__/SimpleResponseDTO.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from "vitest";
import {
SimpleResponseHelpers,
type SimpleResponseDTO,
} from "../SimpleResponseDTO.js";

test("should transform to SimpleResponseDTO", async () => {
//GIVEN
const EXPECTED_STATUS = 200;
const EXPECTED_DATA = { aKey: "aValue" };
const RESPONSE = new Response(JSON.stringify(EXPECTED_DATA), {
status: EXPECTED_STATUS,
headers: new Headers({ "Content-Type": "application/json" }),
});

//WHEN
const ACTUAL =
await SimpleResponseHelpers.transformToSimpleResponse<
typeof EXPECTED_DATA
>(RESPONSE);

//THEN
expect(ACTUAL).toEqual<SimpleResponseDTO<typeof EXPECTED_DATA>>({
ok: RESPONSE.ok,
statusCode: RESPONSE.status,
data: EXPECTED_DATA,
});
});

test("should handle default, non json response", async () => {
//GIVEN
const EXPECTED_DATA = "12345";
const RESPONSE = new Response(EXPECTED_DATA);
//WHEN
const ACTUAL =
await SimpleResponseHelpers._extractDataBasedOnContentType(RESPONSE);

//THEN
expect(ACTUAL).toEqual(EXPECTED_DATA);
});

test("should handle Content-Type=application/json response", async () => {
//GIVEN
const EXPECTED_DATA = { testKey: "testValue" };
const RESPONSE = new Response(JSON.stringify(EXPECTED_DATA), {
headers: new Headers({
"Content-Type": "application/json; charset=utf-8",
}),
});

//WHEN
const ACTUAL =
await SimpleResponseHelpers._extractDataBasedOnContentType(RESPONSE);

//THEN
expect(ACTUAL).toEqual(EXPECTED_DATA);
});
5 changes: 5 additions & 0 deletions src/constants/ContentTypeHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ContentType = "Content-Type";
export const MimeTypes = {
textPlain: "text/plain",
applicationJson: "application/json",
};
24 changes: 0 additions & 24 deletions src/constants/DTOs/RegistrationDTO.ts

This file was deleted.

19 changes: 18 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
export { UserRepository } from "./repositories/UserRepository.js";
// Repositories:
export type { SessionRepositoryInterface } from "./repositories/SessionRepositoryInterface.ts";
export { UserRepository } from "./repositories/UserRepository.js";
export { GroupRepository } from "./repositories/GroupRepository.js";
//DTOs:
export type { SimpleResponseDTO as SimpleResponse } from "./DTOs/SimpleResponseDTO.ts";
export type { RegistrationDTO } from "./DTOs/RegistrationDTO.ts";
export type { UserDetailsDTO } from "./DTOs/UserDetailsDTO.ts";
export type { GroupDetailsDTO } from "./DTOs/GroupDetailsDTO.ts";
export type { GroupMemberProfileDTO } from "./DTOs/GroupMemberDTO.ts";
export type {
GroupMessageDTO,
BaseGroupMessageDTO,
} from "./DTOs/GroupMessageDTO.ts";
//DTO-Helpers:
export { SimpleResponseHelpers } from "./DTOs/SimpleResponseDTO.js";
export { RegistrationHelper } from "./DTOs/RegistrationDTO.js";
// Miscellanieous Types:
export type { BaseRepositoryConstructorParam } from "./repositories/BaseRepository.ts";
54 changes: 38 additions & 16 deletions src/repositories/BaseRepository.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import type { Serializable } from "child_process";
import { ContentType, MimeTypes } from "../constants/ContentTypeHeader.js";
import { CustomHeadersNames } from "../constants/CustomHeaders.js";
import type { SessionRepositoryInterface } from "./SessionRepositoryInterface.js";

type ApiBaseUrlType =
(typeof BaseRepository.Api)[keyof typeof BaseRepository.Api];

export interface BaseRepositoryConstructorArgs {
export interface BaseRepositoryConstructorParam {
apiBaseUrl: ApiBaseUrlType;
publicApiKey: string;
sessionRepository: SessionRepositoryInterface;
}

interface RequestParams {
route: string;
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
queryParams?: URLSearchParams;
extraHeaders?: Headers;
body?: RequestInit["body"];
signal?: AbortSignal | null;
}

export abstract class BaseRepository {
static Api = { mockaroo: "https://my.api.mockaroo.com" };

Expand All @@ -28,20 +39,24 @@ export abstract class BaseRepository {
public sessionRepository: SessionRepositoryInterface,
) {}

/**
* @throws any {@link sessionRepository} related Error
*/
private async buildBaseHeaders() {
const HEADERS = new Headers();
HEADERS.append(
CustomHeadersNames.sessionId,
await this.sessionRepository.readSessionId(),
);

HEADERS.append(CustomHeadersNames.apiKey, this.publicApiKey);
const HEADERS = new Headers({
[CustomHeadersNames.sessionId]:
await this.sessionRepository.readSessionId(),
[CustomHeadersNames.apiKey]: this.publicApiKey,
[ContentType]: MimeTypes.applicationJson,
});

return HEADERS;
}

/**
* Sets the Content-Type to application/json by default. Can be overridden by settin {@link extraHeaders}.
* @protected
* @throws any {@link sessionRepository} related Error
*/
async _bulildRequest({
route,
Expand All @@ -50,14 +65,7 @@ export abstract class BaseRepository {
extraHeaders = new Headers(),
body,
signal,
}: {
route: string;
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
queryParams?: URLSearchParams;
extraHeaders?: Headers;
body?: RequestInit["body"];
signal?: AbortSignal | null;
}): Promise<Request> {
}: RequestParams): Promise<Request> {
const headers = await this.buildBaseHeaders();
extraHeaders.forEach((headerValue, headerName) => {
headers.append(headerName, headerValue);
Expand All @@ -77,6 +85,20 @@ export abstract class BaseRepository {
/**
* @protected
*/
async _buildJsonRequest(
unserializedBody: Serializable,
requestParams: Omit<RequestParams, "body">,
): Promise<Request> {
return this._bulildRequest({
...requestParams,
body: JSON.stringify(unserializedBody),
});
}

/**
* @protected
* @throws any {@link sessionRepository} related Error
*/
async _handleResponseAfterAuthentication(
response: Response,
): Promise<void> {
Expand Down
Loading
Loading