Skip to content

Commit 4f7495d

Browse files
authored
feat: Model のインスタンスを取るときに権限を戦わせるために Builder パターンを導入 (#42)
* wip: Variants まとめられてけっこう良い感じ * wip: いいぞ〜 あとは各モデルに実装するだけ! * feat: 全モデル実装 * feat: 複数取得 * feat: 更新系実装 * Squashed commit of the following: commit 3e402bd Author: tada ryuto <[email protected]> Date: Sun Mar 16 03:45:39 2025 +0900 lint (#36) commit 14028ab Author: tada ryuto <[email protected]> Date: Sun Mar 16 03:42:38 2025 +0900 Docs/append readme (#35) * docs: READMEを更新 * feat: VSCodeの推奨拡張機能を追加 commit 430a4e5 Author: tada ryuto <[email protected]> Date: Sun Mar 16 03:31:07 2025 +0900 feat: ヘッダーにログインボタン追加 (#34) commit 431dbdb Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 03:15:05 2025 +0900 change: テーマカラーを変更 commit 67bc573 Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 03:14:56 2025 +0900 fix: ログアウト機能 commit b926bd4 Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 03:00:34 2025 +0900 fix: ログアウト commit 6a15b4a Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 02:54:06 2025 +0900 feat: フッター commit 45b1b9e Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 02:46:37 2025 +0900 feat: ヘッダー commit b7d6349 Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 01:43:40 2025 +0900 feat: 管理者画面を追加 commit 8916fa5 Author: SatooRu65536 <[email protected]> Date: Sun Mar 16 01:41:42 2025 +0900 delete: フロントを削除 commit e79f22f Author: wappon28dev <[email protected]> Date: Sat Mar 15 23:46:33 2025 +0900 Revert "wip: ファクトリーパターンへの工事" This reverts commit d914fa3. * chore: モデルビルドのエラーハンドリングを `DatabaseError` へ統合 * fix: `Member` モデルに対する過剰な null チェック * chore: subject で部員を逆引きできるように * chore: log ほしいので追記 * merge: マージ後の手直し * chore: log ほしいので追記 (2 回目) * improve: ログ出し強化した
1 parent ce59bd2 commit 4f7495d

26 files changed

+2319
-913
lines changed

Diff for: .vscode/model.code-snippets

+96-61
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
{
2-
"Model Class": {
2+
"Model Class": {
33
"prefix": "@model",
44
"body": [
55
"import type { DatabaseResult } from '@/types/database';",
6-
"import type { ModelEntityOf, ModelGenerator, ModelMetadata, ModelMode, ModelSchemaRawOf, ModeWithResolved } from '@/types/model';",
7-
"import type { Override } from '@/types/utils';",
6+
"import type { BuildModelResult, Model, ModelBuilder, ModelBuilderInternal, ModelBuilderType, ModelGenerator, ModelInstances, ModelMetadata, ModelMode, ModelNormalizer, ModelRawData4build, ModelResolver, ModelSchemaRawOf, ModelUnwrappedInstances__DO_NOT_EXPOSE, ModeWithResolved } from '@/types/model';",
7+
"import type { ArrayElem, Brand, Nullable, Override } from '@/types/utils';",
88
"import type {",
99
" Prisma,",
1010
" PrismaClient,",
1111
" ${1:ModelName} as SchemaRaw,",
1212
"} from '@prisma/client';",
1313
"import { Database } from '@/services/database.server';",
14-
"import { includeKeys2select, matchWithResolved } from '@/utils/model';",
14+
"import { buildRawData, includeKeys2select, matchWithDefault, matchWithResolved, separateRawData } from '@/utils/model';",
15+
"import { err, ok } from 'neverthrow';",
16+
"import { match } from 'ts-pattern';",
17+
"import { z } from 'zod';",
1518
"",
1619
"/// Metadata ///",
1720
"",
@@ -23,107 +26,139 @@
2326
"",
2427
"/// Custom Types ///",
2528
"",
26-
"/* TODO */",
29+
"",
2730
"",
2831
"/// Model Types ///",
2932
"",
3033
"type Schema = Override<",
3134
" SchemaRaw,",
3235
" {",
33-
" /* TODO */",
3436
" }",
3537
">;",
3638
"",
3739
"type IncludeKey = keyof Prisma.${1:ModelName}Include;",
3840
"const includeKeys = [] as const satisfies IncludeKey[];",
3941
"",
4042
"interface SchemaResolvedRaw {",
41-
" /* TODO */",
4243
"}",
4344
"",
4445
"interface SchemaResolved {",
45-
" _parent: {",
46-
" /* TODO */",
47-
" };",
4846
"}",
4947
"",
48+
"/// ModelTypes ///",
49+
"",
50+
"type ModelGen = ModelGenerator<typeof metadata, SchemaRaw, Schema, SchemaResolvedRaw, SchemaResolved>;",
51+
"type ThisModelImpl<M extends ModelMode = 'DEFAULT'> = Model<M, ModelGen>;",
52+
"type ThisModel<M extends ModelMode = 'DEFAULT'> = $${1:ModelName}<M>;",
53+
"interface ThisModelVariants {",
54+
" DEFAULT: ThisModel;",
55+
" WITH_RESOLVED: ThisModel<'WITH_RESOLVED'>;",
56+
"}",
57+
"type RawData = ModelRawData4build<ThisModel>;",
58+
"",
59+
"/// Normalizer ///",
60+
"",
61+
"const normalizer = ((client, builder) => ({",
62+
" schema: (__raw) => ({",
63+
" }),",
64+
" schemaResolved: (__rawResolved) => {",
65+
" const { models } = new Database(client);",
66+
" const { } = __rawResolved;",
67+
" },",
68+
"})) satisfies ModelNormalizer<ThisModel>;",
69+
"",
5070
"/// Model ///",
5171
"",
52-
"export const __${1:ModelName} = (<M extends ModelMode = 'DEFAULT'>(client: PrismaClient) => class ${1:ModelName}<Mode extends ModelMode = M> {",
53-
" public static __prisma = client;",
72+
"export class $${1:ModelName}<Mode extends ModelMode = 'DEFAULT'> implements ThisModelImpl<Mode> {",
5473
" private dbError = Database.dbErrorWith(metadata);",
55-
" private models = new Database(client).models",
74+
" private client;",
75+
" public declare __struct: ThisModelImpl<Mode>;",
76+
" public declare __variants: ThisModelVariants;",
5677
"",
5778
" public __raw: SchemaRaw;",
5879
" public data: Schema;",
5980
" public __rawResolved: ModeWithResolved<Mode, SchemaResolvedRaw>;",
6081
" public dataResolved: ModeWithResolved<Mode, SchemaResolved>;",
6182
"",
62-
" public constructor(__raw: SchemaRaw, __rawResolved?: SchemaResolvedRaw) {",
63-
" this.__raw = __raw;",
64-
" this.data = {",
65-
" ...__raw,",
66-
" /* TODO */",
67-
" };",
68-
"",
69-
" const { rawResolved, dataResolved } = matchWithResolved<Mode, SchemaResolvedRaw, SchemaResolved>(",
70-
" __rawResolved,",
71-
" (r) => ({",
72-
" _parent: {",
73-
" /* TODO */",
74-
" },",
75-
" }),",
76-
" );",
83+
" private constructor(",
84+
" public __prisma: PrismaClient,",
85+
" { __raw, __rawResolved }: RawData,",
86+
" private builder: ModelBuilderType,",
87+
" ) {",
88+
" const n = normalizer(__prisma, this.builder);",
7789
"",
90+
" this.__raw = __raw;",
91+
" this.data = n.schema(__raw);",
92+
" const { rawResolved, dataResolved } = matchWithResolved<Mode, SchemaResolvedRaw, SchemaResolved>(__rawResolved, n.schemaResolved);",
7893
" this.__rawResolved = rawResolved;",
7994
" this.dataResolved = dataResolved;",
95+
" this.client = __prisma;",
8096
" }",
8197
"",
82-
" public static from(id /* TODO */): DatabaseResult<${1:ModelName}> {",
83-
" return Database.transformResult(",
84-
" client.${2:modelName}.findUniqueOrThrow({",
85-
" where: { ${3:primaryKey}: id },",
86-
" }),",
87-
" )",
88-
" .mapErr(Database.dbErrorWith(metadata).transform('from'))",
89-
" .map((data) => new ${1:ModelName}(data));",
98+
" public static with(client: PrismaClient) {",
99+
" const __toUnwrappedInstances = ((rawData, builder) => ({",
100+
" default: new $${1:ModelName}(client, rawData, builder),",
101+
" withResolved: new $${1:ModelName}<'WITH_RESOLVED'>(client, rawData, builder),",
102+
" })) satisfies ModelUnwrappedInstances__DO_NOT_EXPOSE<ThisModel>;",
103+
"",
104+
" const toInstances = ((rawData, builder) => match(builder)",
105+
" .with({ type: 'ANONYMOUS' }, () => err({ type: 'PERMISSION_DENIED', detail: { builder }} as const))",
106+
" .with({ type: 'SELF' }, () => ok(__toUnwrappedInstances(rawData, builder)))",
107+
" .with({ type: 'MEMBER' }, () => ok(__toUnwrappedInstances(rawData, builder)))",
108+
" .exhaustive()",
109+
" ) satisfies ModelInstances<ThisModel>;",
110+
"",
111+
" const __build = {",
112+
" __with: toInstances,",
113+
" by: (rawData, memberAsBuilder) => toInstances(rawData, { type: 'MEMBER', member: memberAsBuilder }),",
114+
" bySelf: (rawData) => toInstances(rawData, { type: 'SELF' }),",
115+
" } satisfies ModelBuilderInternal<ThisModel>;",
116+
"",
117+
" return {",
118+
" __build,",
119+
" from: (${3:primaryKey}: never) => {",
120+
" const rawData = Database.transformResult(",
121+
" client.${2:modelName}.findUniqueOrThrow({",
122+
" where: { ${3:primaryKey} },",
123+
" }),",
124+
" )",
125+
" .mapErr(Database.dbErrorWith(metadata).transform('from'))",
126+
" .map(separateRawData<ThisModel, IncludeKey>(includeKeys).default);",
127+
"",
128+
" return rawData.map(buildRawData(__build).default);",
129+
" },",
130+
" fromWithResolved: (${3:primaryKey}: never) => {",
131+
" const rawData = Database.transformResult(",
132+
" client.${2:modelName}.findUniqueOrThrow({",
133+
" where: { ${3:primaryKey} },",
134+
" include: includeKeys2select(includeKeys),",
135+
" }),",
136+
" )",
137+
" .mapErr(Database.dbErrorWith(metadata).transform('fromWithResolved'))",
138+
" .map(separateRawData<ThisModel, IncludeKey>(includeKeys).withResolved);",
139+
"",
140+
" return rawData.map(buildRawData(__build).withResolved);",
141+
" },",
142+
" } satisfies ModelBuilder<ThisModel>;",
90143
" }",
91144
"",
92-
"public static fromWithResolved(id /* TODO */): DatabaseResult<Member<'WITH_RESOLVED'>> {",
93-
" return Database.transformResult(",
94-
" client.member.findUniqueOrThrow({",
95-
" where: { id },",
96-
" include: includeKeys2select(includeKeys),",
97-
" }),",
98-
" )",
99-
" .mapErr(Database.dbErrorWith(metadata).transform('fromWithResolved'))",
100-
" .map(/* TODO */)",
101-
"}",
102-
"",
103-
" public resolveRelation(): DatabaseResult<SchemaResolved> {",
145+
" public resolveRelation(): ModelResolver<Mode, ThisModel> {",
104146
" return matchWithDefault(",
105147
" this.__rawResolved,",
106-
" () => Database.transformResult(",
107-
" client.${2:modelName}.findUniqueOrThrow({",
108-
" where: { ${3:primaryKey}: this.data.id },",
109-
" include: includeKeys2select(includeKeys),",
110-
" }),",
111-
" )",
112-
" .mapErr(this.dbError.transform(this.dbError.transform('resolveRelation'))",
113-
" .map(/* TODO */)",
114-
" ));",
148+
" () => $${1:ModelName}.with(this.client).fromWithResolved(this.data.${3:primaryKey}),",
149+
" );",
115150
" }",
116151
"",
117-
" public update(_operator: ModelEntityOf<$$Member>, _data: Partial<Schema>): DatabaseResult<${1:ModelName}> {",
152+
" public update(_data: Partial<Schema>): DatabaseResult<ThisModel> {",
118153
" throw new Error('Method not implemented.');",
119154
" }",
120155
"",
121-
" public delete(_operator: ModelEntityOf<$$Member>): DatabaseResult<void> {",
156+
" public delete(_operator: ThisModel): DatabaseResult<void> {",
122157
" throw new Error('Method not implemented.');",
123158
" }",
124-
"}) satisfies ModelGenerator<any, typeof metadata, SchemaRaw, Schema, SchemaResolvedRaw, SchemaResolved>;",
125159
"",
126-
"export type $${1:ModelName}<M extends ModelMode = 'DEFAULT'> = ModelGenerator<M, typeof metadata, SchemaRaw, Schema, SchemaResolvedRaw, SchemaResolved>; & typeof __${1:ModelName}<M>",
160+
" public hoge() { }",
161+
"}",
127162
""
128163
],
129164
"description": ""

Diff for: app/examples/Member.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable unused-imports/no-unused-vars */
22

33
import type { $Member } from '@/models/member';
4-
import type { ModelEntityOf, ModelSchemaOf } from '@/types/model';
4+
import type { ModelSchemaOf } from '@/types/model';
55
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
66
import type { ReactElement } from 'react';
77
import { MemberId } from '@/models/member';
@@ -37,13 +37,13 @@ function MemberCardInfo({
3737
}
3838

3939
function MemberCard(
40-
Member: ModelEntityOf<$Member>, // ← 部員自身の操作が必要なので, ModelEntityOf<$Member>.
40+
Member: $Member, // ← 部員自身の操作が必要なので, `$Member` を使う.
4141
): ReactElement {
4242
const { update } = Member;
4343

4444
// ここは, SWR とかなんでも使ってどうぞ.
4545
const [member, updateMember, isPending] = useActionState(async () => {
46-
const newMember = await update(Member, { updatedAt: new Date() });
46+
const newMember = await update({ updatedAt: new Date() });
4747

4848
if (newMember.isErr()) {
4949
// エラーに対するインタラクションなど

0 commit comments

Comments
 (0)