generated from SatooRu65536/tpl-remix-vite
-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
1eb216c
commit 00d643a
Showing
15 changed files
with
715 additions
and
24 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,10 @@ | |
"words": [ | ||
"cloudflare", | ||
"datasource", | ||
"neverthrow", | ||
"prisma", | ||
"sqlite", | ||
"Uncapitalize", | ||
"uuid" | ||
] | ||
} |
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,32 @@ | ||
import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; | ||
import { MemberId } from '@/utils/models/member'; | ||
import { useLoaderData } from '@remix-run/react'; | ||
|
||
export async function loader({ context }: LoaderFunctionArgs) { | ||
const { db } = context; | ||
const { Member } = db.models; | ||
const member = (await Member.factories.from( | ||
MemberId.from('0188c0f2-8e47-11ec-b909-0242ac120002')._unsafeUnwrap(), | ||
))._unsafeUnwrap(); | ||
|
||
return { member }; | ||
} | ||
|
||
export default function Index() { | ||
const { member } = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<div> | ||
<div>test</div> | ||
<textarea | ||
defaultValue={JSON.stringify(member, null, 2)} | ||
style={{ | ||
width: '100%', | ||
minHeight: '10lh', | ||
// @ts-expect-error お NEW なプロパティ | ||
fieldSizing: 'content', | ||
}} | ||
/> | ||
</div> | ||
); | ||
} |
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,69 @@ | ||
import type { DatabaseError, PrismaClientError } from '@/types/database'; | ||
import type { ModelMetadata } from '@/types/model'; | ||
import { clientKnownErrorCode } from '@/types/database'; | ||
import { getEntries } from '@/utils'; | ||
import { __Member } from '@/utils/models/member'; | ||
import { Prisma, type PrismaClient } from '@prisma/client'; | ||
import { ResultAsync } from 'neverthrow'; | ||
import { match } from 'ts-pattern'; | ||
|
||
export class Database { | ||
public models; | ||
|
||
public constructor(protected client: PrismaClient) { | ||
this.models = { | ||
Member: __Member(client), | ||
}; | ||
} | ||
|
||
public static transformError( | ||
metadata: ModelMetadata<any, 'CATCH_ALL'>, | ||
caller: string, | ||
error: unknown, | ||
{ message, hint }: Partial<{ message: string; hint: string }> = {}, | ||
): DatabaseError { | ||
const detail | ||
= match(error) | ||
.when( | ||
(e) => e instanceof Prisma.PrismaClientKnownRequestError, | ||
(e) => { | ||
const type = getEntries(clientKnownErrorCode).find(([, code]) => code === e.code)?.[0] ?? 'UNKNOWN_REQUEST_ERROR' as const; | ||
return { type, _raw: e }; | ||
}, | ||
) | ||
.when( | ||
(e) => e instanceof Prisma.PrismaClientValidationError, | ||
(e) => ({ type: 'DATA_VALIDATION_FAILED' as const, _raw: e }), | ||
) | ||
.when( | ||
(e) => e instanceof Prisma.PrismaClientKnownRequestError || e instanceof Prisma.PrismaClientUnknownRequestError, | ||
(e) => ({ type: 'UNKNOWN_REQUEST_ERROR' as const, _raw: e }), | ||
) | ||
.when( | ||
(e) => e instanceof Prisma.PrismaClientRustPanicError || e instanceof Prisma.PrismaClientInitializationError, | ||
(e) => ({ type: 'UNKNOWN_ERROR' as const, _raw: e }), | ||
) | ||
.otherwise( | ||
() => ({ type: 'UNEXPECTED_ERROR' as const, _raw: new Error('Unknown error', { cause: error }) }), | ||
); | ||
|
||
const _message | ||
= match(detail.type) | ||
.with('NO_ROWS_FOUND', () => `この${metadata.displayName}は見つかりませんでした`) | ||
.with('ALREADY_EXISTS', () => `この${metadata.displayName}は既に存在します`) | ||
.with('DATA_VALIDATION_FAILED', () => `この${metadata.displayName}のデータが不正です`) | ||
.otherwise(() => `${metadata.displayName}の処理中にエラーが発生しました`); | ||
|
||
return { | ||
metadata, | ||
caller, | ||
message: message ?? _message, | ||
hint, | ||
...detail, | ||
}; | ||
} | ||
|
||
public static transformResult<S>(fn: Promise<S>): ResultAsync<S, PrismaClientError> { | ||
return ResultAsync.fromPromise(fn, (e) => e as PrismaClientError); | ||
} | ||
} |
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,48 @@ | ||
import type { ModelMetadata } from '@/types/model'; | ||
import type { Prisma } from '@prisma/client'; | ||
import type { ResultAsync } from 'neverthrow'; | ||
|
||
export const clientKnownErrorCode = { | ||
ALREADY_EXISTS: 'P2002', | ||
NO_ROWS_FOUND: 'P2025', | ||
} as const satisfies Record<string, string>; | ||
|
||
// ref: https://www.prisma.io/docs/orm/reference/error-reference | ||
export type PrismaClientError = | ||
| Prisma.PrismaClientKnownRequestError | ||
| Prisma.PrismaClientValidationError | ||
| Prisma.PrismaClientUnknownRequestError | ||
| Prisma.PrismaClientRustPanicError | ||
| Prisma.PrismaClientInitializationError | ||
| Error; | ||
|
||
export type DatabaseErrorDetail = | ||
| { | ||
type: keyof typeof clientKnownErrorCode | 'UNKNOWN_REQUEST_ERROR'; | ||
_raw: Prisma.PrismaClientKnownRequestError; | ||
} | ||
| { | ||
type: 'DATA_VALIDATION_FAILED'; | ||
_raw: Prisma.PrismaClientValidationError; | ||
} | ||
| { | ||
type: 'UNKNOWN_REQUEST_ERROR'; | ||
_raw: Prisma.PrismaClientKnownRequestError | Prisma.PrismaClientUnknownRequestError; | ||
} | ||
| { | ||
type: 'UNKNOWN_ERROR'; | ||
_raw: Prisma.PrismaClientRustPanicError | Prisma.PrismaClientInitializationError; | ||
} | ||
| { | ||
type: 'UNEXPECTED_ERROR'; | ||
_raw: Error; | ||
}; | ||
|
||
export type DatabaseError = { | ||
metadata: ModelMetadata<any>; | ||
caller: string; | ||
message: string; | ||
hint?: string; | ||
} & DatabaseErrorDetail; | ||
|
||
export type DatabaseResult<S> = ResultAsync<S, DatabaseError>; |
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,43 @@ | ||
import type { Model } from '@/utils/model'; | ||
import type { Prisma, PrismaClient } from '@prisma/client'; | ||
|
||
export interface ModelMetadata< | ||
M extends Prisma.ModelName, | ||
V extends 'CATCH_ALL' | 'DEFAULT' = 'DEFAULT', | ||
> { | ||
modelName: M; | ||
displayName: string; | ||
primaryKeyName: V extends 'CATCH_ALL' ? any : keyof PrismaClient[Uncapitalize<M>]['fields']; | ||
} | ||
|
||
// __M = (client) => M のときの M | ||
export type ModelGenerator< | ||
Metadata extends ModelMetadata<any, 'CATCH_ALL'>, | ||
Schema extends object, | ||
> = (client: PrismaClient) => new (data: Schema) => Model<Metadata, Schema>; | ||
|
||
export type AnyModelGenerator = ModelGenerator<ModelMetadata<any, 'CATCH_ALL'>, any>; | ||
export type AnyModel = Model<any, any>; | ||
|
||
// __M = (client) => M のときの M | ||
// ただし, __M = (client) => M のときの M に対しても適用可能 | ||
export type ModelEntityOf<T extends AnyModelGenerator | AnyModel> | ||
= T extends AnyModelGenerator | ||
? InstanceType<ReturnType<T>> | ||
: T extends AnyModel | ||
? T | ||
: never; | ||
|
||
// M: Model<Metadata, Schema> のときの Metadata | ||
// ただし, __M = (client) => M のときの M に対しても適用可能 | ||
export type ModelMetadataOf<T extends AnyModelGenerator | AnyModel> | ||
= ModelEntityOf<T> extends Model<infer M, any> | ||
? M | ||
: never; | ||
|
||
// M: Model<Metadata, Schema> のときの Metadata | ||
// ただし, __M = (client) => M のときの M に対しても適用可能 | ||
export type ModelSchemaOf<T extends AnyModelGenerator | AnyModel> | ||
= ModelEntityOf<T> extends Model<any, infer S> | ||
? S | ||
: never; |
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,25 @@ | ||
export type Override<T, U extends { [Key in keyof T]?: unknown }> = Omit< | ||
T, | ||
keyof U | ||
> & | ||
U; | ||
|
||
export type Entries<T> = Array< | ||
keyof T extends infer U ? (U extends keyof T ? [U, T[U]] : never) : never | ||
>; | ||
|
||
export type ArrayElem<ArrayType extends readonly unknown[]> = | ||
ArrayType extends ReadonlyArray<infer ElementType> ? ElementType : never; | ||
|
||
export type OmitStrict<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; | ||
|
||
export type Nullable<T> = T | null | undefined; | ||
|
||
// ref: https://zenn.dev/ourly_tech_blog/articles/branded_type_20240726#3.-branded-type%E3%81%AE%E5%AE%9F%E8%A3%85%E6%96%B9%E6%B3%95 | ||
declare const __brand: unique symbol; | ||
export type Brand<K, T> = K & { [__brand]: T }; | ||
|
||
// ref: https://www.totaltypescript.com/concepts/the-prettify-helper | ||
export type Prettify<T> = { | ||
[K in keyof T]: T[K]; | ||
} & {}; |
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 |
---|---|---|
@@ -1,6 +1,53 @@ | ||
import type { Brand, Entries } from '@/types/utils'; | ||
import type { Result } from 'neverthrow'; | ||
import type { ZodError } from 'zod'; | ||
import { TYPES } from '@/consts/member'; | ||
import { err, ok } from 'neverthrow'; | ||
import { string } from 'zod'; | ||
|
||
export function toTypeName(key: string) { | ||
const type = TYPES.find((t) => t.key === key); | ||
return type?.name; | ||
} | ||
|
||
export function toUncapitalize<T extends string>(str: T): Uncapitalize<T> { | ||
return str.charAt(0).toLowerCase() + str.slice(1) as Uncapitalize<T>; | ||
} | ||
|
||
export function parseUuid<K extends string>(uuid: string): Result<Brand<K, string>, ZodError<string>> { | ||
const zUuid = string().uuid(); | ||
const { success, data, error } = zUuid.safeParse(uuid); | ||
|
||
if (!success) { | ||
return err(error); | ||
} | ||
|
||
return ok(data as Brand<K, string>); | ||
} | ||
|
||
export async function waitMs(ms: number): Promise<void> { | ||
await new Promise((resolve) => { | ||
setTimeout(resolve, ms); | ||
}); | ||
} | ||
|
||
export function getKeys<T extends Record<string, unknown>>( | ||
obj: T, | ||
): Array<keyof T> { | ||
return Object.keys(obj); | ||
} | ||
export function getValues<T extends Record<string, any>>( | ||
obj: T, | ||
): Array<T[keyof T]> { | ||
return Object.values(obj); | ||
} | ||
export function getEntries<T extends Record<string, unknown>>( | ||
obj: T, | ||
): Entries<T> { | ||
return Object.entries(obj) as Entries<T>; | ||
} | ||
export function fromEntries<T extends Record<string, unknown>>( | ||
entries: Entries<T>, | ||
): T { | ||
return Object.fromEntries(entries) as T; | ||
} |
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,63 @@ | ||
import type { DatabaseResult } from '@/types/database'; | ||
import type { | ||
AnyModel, | ||
ModelMetadata, | ||
ModelMetadataOf, | ||
ModelSchemaOf, | ||
} from '@/types/model'; | ||
import type { Brand } from '@/types/utils'; | ||
import type { Prisma, PrismaClient } from '@prisma/client'; | ||
import { Database } from '@/services/database.server'; | ||
import { toUncapitalize } from '@/utils'; | ||
|
||
export abstract class Model< | ||
Metadata extends ModelMetadata<any, 'CATCH_ALL'>, | ||
Schema extends object, | ||
> { | ||
public constructor(public data: Schema, protected metadata: Metadata) {} | ||
|
||
/** | ||
* {@link Database.transformError} の部分適用. | ||
* `Model` を継承しているので, 最初の引数を省略できる. | ||
*/ | ||
protected transformError(caller: string) { | ||
return ( | ||
...rest: Parameters<typeof Database.transformError> extends [ | ||
any, | ||
any, | ||
...infer R, | ||
] | ||
? R | ||
: never | ||
) => Database.transformError(this.metadata, caller, ...rest); | ||
} | ||
|
||
protected transformResult = Database.transformResult; | ||
|
||
protected static getFactories< | ||
B extends Brand<string, unknown>, | ||
M extends AnyModel, | ||
>( | ||
client: PrismaClient, | ||
) { | ||
return ( | ||
Model: new (data: ModelSchemaOf<M>) => M, | ||
metadata: ModelMetadataOf<M>, | ||
) => ({ | ||
from: (id: B): DatabaseResult<M> => { | ||
const { modelName, primaryKeyName } = metadata as ModelMetadata<Prisma.ModelName, 'CATCH_ALL'>; | ||
const modelNameUnCapitalize = toUncapitalize (modelName); | ||
|
||
const result = Database.transformResult<ModelSchemaOf<M>>( | ||
// @ts-expect-error: `primaryKeyName` が `any` になってしまうため, 型エラーが発生する | ||
// eslint-disable-next-line ts/no-unsafe-argument | ||
client[modelNameUnCapitalize].findUniqueOrThrow({ where: { [primaryKeyName]: id } }), | ||
); | ||
|
||
return result | ||
.map((data) => new Model(data)) | ||
.mapErr((e) => Database.transformError(metadata, 'from', e)); | ||
}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.