Skip to content

Commit

Permalink
update tag crud
Browse files Browse the repository at this point in the history
  • Loading branch information
orlein committed Nov 25, 2024
1 parent 2729e39 commit a27e8ff
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 18 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@oz-adv/backend",
"version": "0.0.2 (2024-11-25.002)",
"version": "0.0.2 (2024-11-25.003)",
"description": "Backend for the Oz-Adv project",
"type": "module",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/api-live.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommentApiLive } from './comment/comment-api-live.mjs';
import { FileApiLive } from './file/file-api-live.mjs';
import { PostApiLive } from './post/post-api-live.mjs';
import { RootApiLive } from './root-api-live.mjs';
import { TagApiLive } from './tag/tag-api-live.mjs';

export const ApiLive = HttpApiBuilder.api(Api).pipe(
Layer.provide([
Expand All @@ -18,5 +19,6 @@ export const ApiLive = HttpApiBuilder.api(Api).pipe(
FileApiLive,
ChallengeApiLive,
ChallengeEventApiLive,
TagApiLive,
]),
);
7 changes: 5 additions & 2 deletions src/api.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommentApi } from './comment/comment-api.mjs';
import { FileApi } from './file/file-api.mjs';
import { PostApi } from './post/post-api.mjs';
import { RootApi } from './root-api.mjs';
import { TagApi } from './tag/tag-api.mjs';

const program = Effect.provide(
Effect.gen(function* () {
Expand All @@ -30,12 +31,14 @@ export class Api extends HttpApi.empty
.add(FileApi)
.add(ChallengeApi)
.add(ChallengeEventApi)
.add(TagApi)
.annotateContext(
OpenApi.annotations({
title: '오즈 6기 심화반 챌린지 서비스를 위한 백엔드',
description: `최신변경점:
* 챌린지 이벤트에서 챌린지 참가자 대비 이벤트에 체크한 참가자 비율을 구하는 기능 추가 (2024-11-25.001)
* 챌린지 이벤트에 챌린지 참가자가 얼마나 참여하였는지 보는 기능 추가 (2024-11-25.001)
* 챌린지 태그 CRUD 추가 (2024-11-25.003)
* 챌린지 이벤트에서 챌린지 참가자 대비 이벤트에 체크한 참가자 비율을 구하는 기능 추가 (2024-11-25.002)
* 챌린지 이벤트에 챌린지 참가자가 얼마나 참여하였는지 보는 기능 추가 (2024-11-25.002)
* 챌린지 조회시 챌린지 이벤트가 얼마나 있는지, 또 그로 정렬하여 보는 기능 추가 (2024-11-25.001)
* 챌린지 조회시 챌린지 참가자가 얼마나 많이 참여하였는지, 또 그로 정렬하여 보는 기능 추가 (2024-11-25.001)
* 챌린지 조회시 챌린지 좋아요로 정렬하여 보는 기능 추가 (2024-11-25.001)
Expand Down
16 changes: 9 additions & 7 deletions src/sql/migrations/00001_create_base_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,19 +231,21 @@ create table challenge_event_participant (
constraint fk_challenge_event_participant_challenge_id foreign key (challenge_id) references challenge (id)
);
create table tag (
id uuid primary key default gen_random_uuid (),
name text,
description text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
is_deleted boolean default false
CREATE TABLE tag (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_deleted BOOLEAN DEFAULT FALSE,
CONSTRAINT unique_name UNIQUE (name)
);
create table account_interest_tag (
id uuid primary key default gen_random_uuid (),
account_id uuid not null,
tag_id uuid not null,
unique (account_id, tag_id),
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
is_deleted boolean default false,
Expand Down
1 change: 0 additions & 1 deletion src/tag/account-interest-tag.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export class AccountInterestTag extends Model.Class<AccountInterestTag>(
id: Model.Generated(AccountInterestTagId),
accountId: AccountId,
tagId: TagId,
isDeleted: Schema.Boolean,
createdAt: CustomDateTimeInsert,
updatedAt: CustomDateTimeUpdate,
}) {}
21 changes: 21 additions & 0 deletions src/tag/tag-api-live.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Api } from '@/api.mjs';
import { AuthenticationLive } from '@/auth/authentication.mjs';
import { HttpApiBuilder } from '@effect/platform';
import { Effect, Layer } from 'effect';
import { TagService } from './tag-service.mjs';

export const TagApiLive = HttpApiBuilder.group(Api, 'tag', (handlers) =>
Effect.gen(function* () {
const tagService = yield* TagService;

return handlers
.handle('findAll', ({ urlParams }) => tagService.findAll(urlParams))
.handle('findById', ({ path }) => tagService.findById(path.tagId))
.handle('findByName', ({ path }) => tagService.findByName(path.tagName))
.handle('create', ({ payload }) => tagService.getOrInsert(payload))
.handle('update', ({ path, payload }) =>
tagService.update(path.tagId, payload),
)
.handle('delete', ({ path }) => tagService.deleteById(path.tagId));
}),
).pipe(Layer.provide(AuthenticationLive), Layer.provide(TagService.Live));
123 changes: 123 additions & 0 deletions src/tag/tag-api.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { FindManyResultSchema } from '@/misc/find-many-result-schema.mjs';
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from '@effect/platform';
import { Tag, TagId } from './tag-schema.mjs';
import { FindManyUrlParams } from '@/misc/find-many-url-params-schema.mjs';
import { Schema } from 'effect';
import { TagNotFound } from './tag-error.mjs';
import { Authentication } from '@/auth/authentication.mjs';
import { Unauthenticated } from '@/auth/error-401.mjs';

export class TagApi extends HttpApiGroup.make('tag')
.add(
HttpApiEndpoint.get('findAll', '/')
.setUrlParams(FindManyUrlParams)
.addSuccess(FindManyResultSchema(Tag.json))
.annotateContext(
OpenApi.annotations({
description:
'태그 목록을 조회합니다. 페이지와 한 페이지당 태그 수를 지정할 수 있습니다.',
override: {
summary: '(사용가능) 태그 목록 조회',
},
}),
),
)
.add(
HttpApiEndpoint.get('findById', '/:tagId')
.setPath(
Schema.Struct({
tagId: TagId,
}),
)
.addError(TagNotFound)
.addSuccess(Tag.json)
.annotateContext(
OpenApi.annotations({
description:
'태그를 조회합니다. 태그가 존재하지 않는 경우 404를 반환합니다.',
override: {
summary: '(사용가능) 태그 ID 조회',
},
}),
),
)
.add(
HttpApiEndpoint.get('findByName', '/by-name/:tagName')
.setPath(
Schema.Struct({
tagName: Schema.String,
}),
)
.addError(TagNotFound)
.addSuccess(Tag.json)
.annotateContext(
OpenApi.annotations({
description:
'태그를 이름으로 조회합니다. 태그가 존재하지 않는 경우 404를 반환합니다.',
override: {
summary: '(사용가능) 태그 이름 조회',
},
}),
),
)
.add(
HttpApiEndpoint.post('create', '/')
.middleware(Authentication)
.setPayload(Tag.jsonCreate)
.addError(Unauthenticated)
.addSuccess(Tag.json)
.annotateContext(
OpenApi.annotations({
description: '관리자만 가능 태그를 생성합니다.',
override: {
summary: '(사용가능)(관리자 전용) 태그 생성',
},
}),
),
)
.add(
HttpApiEndpoint.patch('update', '/:tagId')
.middleware(Authentication)
.setPath(
Schema.Struct({
tagId: TagId,
}),
)
.setPayload(Schema.partialWith(Tag.jsonUpdate, { exact: true }))
.addError(Unauthenticated)
.addError(TagNotFound)
.addSuccess(Tag.json)
.annotateContext(
OpenApi.annotations({
description: '관리자만 가능 태그를 수정합니다.',
override: {
summary: '(사용가능)(관리자 전용) 태그 수정',
},
}),
),
)
.add(
HttpApiEndpoint.del('delete', '/:tagId')
.middleware(Authentication)
.setPath(
Schema.Struct({
tagId: TagId,
}),
)
.addError(Unauthenticated)
.addError(TagNotFound)
.annotateContext(
OpenApi.annotations({
description: '관리자만 가능 태그를 삭제합니다.',
override: {
summary: '(사용가능)(관리자 전용) 태그 삭제',
},
}),
),
)
.prefix('/api/tags')
.annotateContext(
OpenApi.annotations({
title: '(사용가능) 태그 API',
}),
) {}
13 changes: 13 additions & 0 deletions src/tag/tag-error.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Schema } from 'effect';
import { TagId } from './tag-schema.mjs';
import { HttpApiSchema } from '@effect/platform';

export class TagNotFound extends Schema.TaggedError<TagNotFound>()(
'TagNotFound',
{ id: TagId },
HttpApiSchema.annotations({
status: 404,
title: 'Tag Not Found',
description: 'ID에 해당하는 태그가 존재하지 않습니다.',
}),
) {}
137 changes: 137 additions & 0 deletions src/tag/tag-repo.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Model, SqlClient, SqlSchema } from '@effect/sql';
import { Effect, Layer, Option, pipe, Schema } from 'effect';
import { Tag, TagId } from './tag-schema.mjs';
import { TagNotFound } from './tag-error.mjs';
import { SqlLive } from '@/sql/sql-live.mjs';
import { FindManyUrlParams } from '@/misc/find-many-url-params-schema.mjs';

const TABLE_NAME = 'tag';

const make = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const repo = yield* Model.makeRepository(Tag, {
tableName: TABLE_NAME,
spanPrefix: 'TagRepo',
idColumn: 'id',
});

const findAll = (params: FindManyUrlParams) =>
Effect.gen(function* () {
const tags = yield* SqlSchema.findAll({
Request: FindManyUrlParams,
Result: Tag,
execute: (req) =>
sql`select * from ${sql(TABLE_NAME)} order by name limit ${params.limit} offset ${(params.page - 1) * params.limit}`,
})(params);
const { total } = yield* SqlSchema.single({
Request: FindManyUrlParams,
Result: Schema.Struct({
total: Schema.Number,
}),
execute: () => sql`select count(*) as total from ${sql(TABLE_NAME)}`,
})(params);

return {
data: tags,
meta: {
total,
page: params.page,
limit: params.limit,
isLastPage: params.page * params.limit + tags.length >= total,
},
};
}).pipe(Effect.orDie, Effect.withSpan('TagRepo.findAll'));

const findOne = (name: string) =>
SqlSchema.findOne({
Request: Schema.Struct({
name: Schema.String,
}),
Result: Tag,
execute: (req) => sql`
SELECT * FROM ${sql(TABLE_NAME)}
WHERE name = ${req.name}`,
})({ name }).pipe(Effect.orDie, Effect.withSpan('TagRepo.findOne'));

const getOrInsert = (payload: typeof Tag.jsonCreate.Type) =>
SqlSchema.single({
Request: Tag.jsonCreate,
Result: Tag,
execute: (req) => sql`
WITH inserted AS (
INSERT INTO ${sql(TABLE_NAME)} (name, description)
VALUES (${req.name}, ${req.description})
ON CONFLICT (name) DO NOTHING
RETURNING *
)
SELECT *
FROM inserted
UNION
SELECT *
FROM tag
WHERE name = 'tag_name'
LIMIT 1;`,
})(payload).pipe(Effect.orDie, Effect.withSpan('TagRepo.getOrInsert'));

const with_ = <A, E, R>(
id: TagId,
f: (tag: Tag) => Effect.Effect<A, E, R>,
): Effect.Effect<A, E | TagNotFound, R> => {
return pipe(
repo.findById(id),
Effect.flatMap(
Option.match({
onNone: () =>
new TagNotFound({
id,
}),
onSome: Effect.succeed,
}),
),
Effect.flatMap(f),
sql.withTransaction,
Effect.catchTag('SqlError', (err) => Effect.die(err)),
);
};

const withName_ = <A, E, R>(
name: string,
f: (tag: Tag) => Effect.Effect<A, E, R>,
): Effect.Effect<A, E | TagNotFound, R> => {
return pipe(
findOne(name),
Effect.flatMap(
Option.match({
onNone: () =>
new TagNotFound({
id: TagId.make(''),
}),
onSome: Effect.succeed,
}),
),
Effect.flatMap(f),
sql.withTransaction,
Effect.catchTag('SqlError', (err) => Effect.die(err)),
);
};

return {
...repo,
findAll,
findOne,
getOrInsert,
with: with_,
withName: withName_,
} as const;
});

export class TagRepo extends Effect.Tag('TagRepo')<
TagRepo,
Effect.Effect.Success<typeof make>
>() {
static layer = Layer.effect(TagRepo, make);

static Live = this.layer.pipe(Layer.provide(SqlLive));
}
Loading

0 comments on commit a27e8ff

Please sign in to comment.