-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
382 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}), | ||
) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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에 해당하는 태그가 존재하지 않습니다.', | ||
}), | ||
) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
Oops, something went wrong.