@things-factory/figure-service 10.1.3 → 10.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist-server/service/figure/event-subscriber.d.ts +21 -0
  2. package/dist-server/service/figure/event-subscriber.js +51 -0
  3. package/dist-server/service/figure/event-subscriber.js.map +1 -0
  4. package/dist-server/service/figure/figure-history.d.ts +43 -0
  5. package/dist-server/service/figure/figure-history.js +155 -0
  6. package/dist-server/service/figure/figure-history.js.map +1 -0
  7. package/dist-server/service/figure/figure-mutation.d.ts +39 -0
  8. package/dist-server/service/figure/figure-mutation.js +153 -1
  9. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  10. package/dist-server/service/figure/figure-query.d.ts +15 -0
  11. package/dist-server/service/figure/figure-query.js +47 -0
  12. package/dist-server/service/figure/figure-query.js.map +1 -1
  13. package/dist-server/service/figure/figure-type.d.ts +0 -1
  14. package/dist-server/service/figure/figure-type.js +0 -4
  15. package/dist-server/service/figure/figure-type.js.map +1 -1
  16. package/dist-server/service/figure/figure.d.ts +9 -0
  17. package/dist-server/service/figure/figure.js +15 -3
  18. package/dist-server/service/figure/figure.js.map +1 -1
  19. package/dist-server/service/figure/index.d.ts +4 -1
  20. package/dist-server/service/figure/index.js +5 -2
  21. package/dist-server/service/figure/index.js.map +1 -1
  22. package/dist-server/service/index.d.ts +6 -2
  23. package/dist-server/service/index.js +17 -2
  24. package/dist-server/service/index.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -0
  26. package/package.json +4 -3
  27. package/server/service/figure/event-subscriber.ts +49 -0
  28. package/server/service/figure/figure-history.ts +130 -0
  29. package/server/service/figure/figure-mutation.ts +157 -1
  30. package/server/service/figure/figure-query.ts +37 -0
  31. package/server/service/figure/figure-type.ts +6 -2
  32. package/server/service/figure/figure.ts +15 -3
  33. package/server/service/figure/index.ts +4 -1
  34. package/server/service/index.ts +23 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.3",
3
+ "version": "10.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -24,10 +24,11 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@hatiolab/figure-model": "^0.1.12",
27
+ "@operato/typeorm-history": "^10.0.0",
27
28
  "@things-factory/ai-client-base": "^10.1.3",
28
- "@things-factory/auth-base": "^10.1.3",
29
+ "@things-factory/auth-base": "^10.1.4",
29
30
  "@things-factory/env": "^10.1.3",
30
31
  "graphql-upload": "^15.0.2"
31
32
  },
32
- "gitHead": "8407b1e9d1f47f6676af144d4a33c8ac01cb49c4"
33
+ "gitHead": "c653a2967daa76b3828f1eb7f4eb18934263d4b8"
33
34
  }
@@ -0,0 +1,49 @@
1
+ import { EventSubscriber } from 'typeorm'
2
+
3
+ import { HistoryEntitySubscriber } from '@operato/typeorm-history'
4
+
5
+ import { Figure } from './figure.js'
6
+ import { FigureHistory } from './figure-history.js'
7
+
8
+ /**
9
+ * 판본을 남긴다 — **발행된 저장에만.**
10
+ *
11
+ * `@operato/typeorm-history` 는 원래 모든 저장을 베낀다. 그런데 도형은 저작 중에 수십 번 저장되고,
12
+ * 그 전부를 남기면 판본 목록이 「내가 오늘 몇 번 눌렀나」가 된다. 사람이 판이라고 부르는 것은
13
+ * **발행한 것**이므로 그때만 베낀다. board 가 같은 규칙으로 돈다(`board-service` 의 같은 이름 파일).
14
+ *
15
+ * 발행 메모는 Figure 의 칸이 아니다. `releaseFigure` 가 저장할 엔티티에 `__releaseComment` 로 얹어
16
+ * 두면 여기서 판본의 `comment` 로 옮긴다 — 메모는 그 판의 것이지 도형의 것이 아니다.
17
+ */
18
+ @EventSubscriber()
19
+ export class FigureHistoryEntitySubscriber extends HistoryEntitySubscriber<Figure, FigureHistory> {
20
+ public get entity() {
21
+ return Figure
22
+ }
23
+
24
+ public get historyEntity() {
25
+ return FigureHistory
26
+ }
27
+
28
+ public beforeInsertHistory(history: FigureHistory, entity: Figure): FigureHistory {
29
+ ;(history as any).comment = (entity as any).__releaseComment ?? null
30
+ return history
31
+ }
32
+
33
+ public beforeUpdateHistory(history: FigureHistory, entity: Figure): FigureHistory {
34
+ ;(history as any).comment = (entity as any).__releaseComment ?? null
35
+ return history
36
+ }
37
+
38
+ public async afterInsert(event: any): Promise<void> {
39
+ if (event.entity?.state == 'released') {
40
+ await super.afterInsert(event)
41
+ }
42
+ }
43
+
44
+ public async afterUpdate(event: any): Promise<void> {
45
+ if (event.entity?.state == 'released') {
46
+ await super.afterUpdate(event)
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,130 @@
1
+ import { Field, ID, ObjectType, Float, Int } from 'type-graphql'
2
+ import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'
3
+
4
+ import {
5
+ HistoryActionColumn,
6
+ HistoryActionType,
7
+ HistoryEntityInterface,
8
+ HistoryOriginalIdColumn
9
+ } from '@operato/typeorm-history'
10
+ import { User } from '@things-factory/auth-base'
11
+ import { Domain, longTextColumn } from '@things-factory/shell'
12
+
13
+ import { Figure } from './figure.js'
14
+
15
+ /**
16
+ * 발행된 판 하나 — **발행할 때만 남는다.**
17
+ *
18
+ * 저작 중 저장은 여기 오지 않는다. 스냅샷을 뜨는 subscriber 가 `state === 'released'` 인 저장에만
19
+ * 움직인다(`event-subscriber.ts`) — board 가 같은 규칙으로 돈다.
20
+ *
21
+ * ## 왜 통째로 베끼나
22
+ *
23
+ * 판을 되세우려면 그때의 정본만으로는 모자란다. 이름·분류·그림·비용까지 함께 있어야 「그 판이
24
+ * 무엇이었나」를 보여 주고 되돌릴 수 있다. 파생값(점수·삼각형·묶음)도 같이 둔다 — 다시 계산하면
25
+ * 형식이 바뀐 오늘의 값이 나오고, 그것은 그 판의 값이 아니다.
26
+ */
27
+ @Entity()
28
+ @Index('ix_figure_history_0', (history: FigureHistory) => [history.originalId, history.version], { unique: true })
29
+ @ObjectType({ description: 'A released version of a figure — one row per release.' })
30
+ export class FigureHistory implements HistoryEntityInterface<Figure> {
31
+ @PrimaryGeneratedColumn('uuid')
32
+ @Field(type => ID, { description: 'Unique identifier for this history record.' })
33
+ readonly id: string
34
+
35
+ @Column({ nullable: true, default: 1 })
36
+ @Field(type => Int, { nullable: true, description: 'The published version number this record holds.' })
37
+ version?: number = 1
38
+
39
+ @ManyToOne(type => Domain)
40
+ @Field(type => Domain, { nullable: true, description: 'The domain the figure belonged to.' })
41
+ domain?: Domain
42
+
43
+ @RelationId((history: FigureHistory) => history.domain)
44
+ domainId?: string
45
+
46
+ @Column({ nullable: true })
47
+ @Field({ nullable: true, description: 'The stored identifier of the figure. It never changes.' })
48
+ type?: string
49
+
50
+ @Column({ nullable: true })
51
+ @Field({ nullable: true, description: 'The display name at this version.' })
52
+ name?: string
53
+
54
+ @Column({ nullable: true })
55
+ @Field({ nullable: true, description: 'The description at this version.' })
56
+ description?: string
57
+
58
+ @Column({ nullable: true })
59
+ @Field({ nullable: true, description: 'The classification code at this version.' })
60
+ category?: string
61
+
62
+ @Column({ nullable: true })
63
+ @Field({ nullable: true, description: 'The lifecycle state recorded with this version.' })
64
+ state?: 'draft' | 'released'
65
+
66
+ @Column({ nullable: true, ...longTextColumn() })
67
+ @Field({ nullable: true, description: 'The canonical FigureSource JSON at this version.' })
68
+ source?: string
69
+
70
+ @Column({ nullable: true, ...longTextColumn() })
71
+ @Field({ nullable: true, description: 'The master-data field definition at this version.' })
72
+ properties?: string
73
+
74
+ @Column({ nullable: true, ...longTextColumn() })
75
+ @Field({ nullable: true, description: 'The thumbnail as it looked at this version.' })
76
+ thumbnail?: string
77
+
78
+ /*
79
+ * 그때의 비용이다. 오늘 다시 계산하면 오늘의 형식으로 나온다 — 그 판을 설명하지 못한다.
80
+ */
81
+ @Column({ type: 'float', nullable: true })
82
+ @Field(type => Float, { nullable: true, description: 'Reuse score as computed at this version.' })
83
+ score?: number
84
+
85
+ @Column({ nullable: true })
86
+ @Field(type => Int, { nullable: true, description: 'Triangle count at this version.' })
87
+ triangles?: number
88
+
89
+ @Column({ nullable: true })
90
+ @Field(type => Int, { nullable: true, description: 'Material group count at this version.' })
91
+ groups?: number
92
+
93
+ @Column({ nullable: true, ...longTextColumn() })
94
+ @Field({ nullable: true, description: 'The note left when this version was released.' })
95
+ comment?: string
96
+
97
+ @Column({ nullable: true })
98
+ @Field({ nullable: true, description: 'When the original figure was created.' })
99
+ createdAt?: Date
100
+
101
+ @Column({ nullable: true })
102
+ @Field({ nullable: true, description: 'When the figure was saved at this version.' })
103
+ updatedAt?: Date
104
+
105
+ @ManyToOne(type => User, { nullable: true })
106
+ @Field(type => User, { nullable: true, description: 'Who created the original figure.' })
107
+ creator?: User
108
+
109
+ @RelationId((history: FigureHistory) => history.creator)
110
+ creatorId?: string
111
+
112
+ @ManyToOne(type => User, { nullable: true })
113
+ @Field(type => User, { nullable: true, description: 'Who released this version.' })
114
+ updater?: User
115
+
116
+ @RelationId((history: FigureHistory) => history.updater)
117
+ updaterId?: string
118
+
119
+ @Column({ nullable: true })
120
+ @Field({ nullable: true, description: 'When the original figure was deleted, if it was.' })
121
+ deletedAt?: Date
122
+
123
+ @HistoryOriginalIdColumn()
124
+ @Field({ description: 'The id of the figure this version belongs to.' })
125
+ public originalId!: string
126
+
127
+ @HistoryActionColumn({ nullable: false, type: 'varchar', length: 10 })
128
+ @Field(type => String, { description: 'What produced this record (created · updated · deleted).' })
129
+ public action!: HistoryActionType
130
+ }
@@ -4,6 +4,7 @@ import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
4
4
  import { getRepository } from '@things-factory/shell'
5
5
 
6
6
  import { Figure } from './figure.js'
7
+ import { FigureHistory } from './figure-history.js'
7
8
  import { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js'
8
9
  import { FigureProposal, ProposeRequest } from './figure-propose-type.js'
9
10
  import { parseSource } from './figure-source.js'
@@ -76,17 +77,172 @@ export class FigureMutation {
76
77
  violations = parsed.violations
77
78
  }
78
79
 
80
+ /*
81
+ **판 번호는 여기서 손대지 않는다.** 저장은 저작이고, 판은 발행에서 생긴다(`releaseFigure`).
82
+ board 의 `updateBoard` 도 같다 — 저장 경로에는 version 이 없다.
83
+ */
79
84
  const saved = await repository.save({
85
+ /*
86
+ `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.
87
+
88
+ 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은
89
+ 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은
90
+ 자리에서 domain 을 함께 넣는다.
91
+ */
92
+ domain,
80
93
  ...figure,
81
94
  ...patch,
82
95
  source,
96
+ /*
97
+ **고치면 발행이 풀린다.**
98
+
99
+ 발행은 「이 판을 써도 된다」는 약속이고, 그 뒤에 정본이 바뀌면 약속한 것과 배포된 것이
100
+ 달라진다. 그래서 저장은 언제나 초안으로 되돌리고, 다시 내보내려면 발행을 한 번 더 한다
101
+ (판 번호가 하나 오르고 판본이 하나 더 남는다). board 의 `updateBoard` 가 같은 한 줄을 쓴다.
102
+ */
103
+ state: 'draft',
83
104
  ...(derived ?? {}),
84
105
  // 썸네일이 **썸네일로서** 바뀐 때만 시각을 갱신한다. 이름·설명 변경에는 안 건드린다.
85
106
  thumbnailUpdatedAt:
86
107
  patch.thumbnail !== undefined && patch.thumbnail !== figure.thumbnail
87
108
  ? new Date()
88
109
  : figure.thumbnailUpdatedAt,
89
- version: (figure.version ?? 1) + 1,
110
+ updater: user
111
+ })
112
+
113
+ return { figure: saved, violations }
114
+ }
115
+
116
+ /**
117
+ * 발행한다 — **판이 생기는 유일한 자리.**
118
+ *
119
+ * ## 무엇이 일어나나
120
+ *
121
+ * `state` 가 `released` 가 되고, 판 번호가 하나 오르고, 그 순간의 도형이 판본으로 남는다
122
+ * (`FigureHistory` — 스냅샷은 구독자가 뜬다). 저작 중 저장은 이 셋 중 아무것도 하지 않는다.
123
+ *
124
+ * ## 왜 이미 발행된 것을 거절하나
125
+ *
126
+ * 발행은 소비처에게 하는 약속이다. 같은 판을 두 번 발행하면 무엇이 약속인지 흐려진다. 고쳐서
127
+ * 다시 내려면 초안으로 되돌린 뒤 발행한다 — board 가 같은 규칙이다.
128
+ *
129
+ * ## 번호는 판본에서 센다
130
+ *
131
+ * 도형의 현재 번호가 아니라 **판본의 최대 번호 + 1** 이다. 되돌리기로 옛 판이 현재가 되어도
132
+ * 다음 발행이 이미 쓴 번호를 다시 쓰지 않는다.
133
+ */
134
+ @Directive('@transaction')
135
+ @Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
136
+ @Mutation(returns => Figure, { description: 'Releases a figure — raises its version and keeps the released state as history.' })
137
+ async releaseFigure(
138
+ @Arg('id') id: string,
139
+ @Arg('comment', { nullable: true }) comment: string,
140
+ @Ctx() context: ResolverContext
141
+ ): Promise<Figure> {
142
+ const { domain, user, tx } = context.state
143
+ const repository = tx.getRepository(Figure)
144
+ const historyRepository = tx.getRepository(FigureHistory)
145
+
146
+ const figure = await repository.findOne({
147
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }
148
+ })
149
+ if (!figure) {
150
+ throw new Error(`Figure ${id} 를 찾을 수 없다`)
151
+ }
152
+
153
+ if (figure.state === 'released') {
154
+ throw new Error(`Figure '${figure.type}' 은 이미 발행되어 있다. 고쳐서 다시 내려면 초안으로 되돌린 뒤 발행한다`)
155
+ }
156
+
157
+ const highest = await historyRepository
158
+ .createQueryBuilder('history')
159
+ .select('MAX(history.version)', 'max')
160
+ .where('history.originalId = :id', { id: figure.id })
161
+ .getRawOne()
162
+
163
+ const nextVersion = highest?.max ? Number(highest.max) + 1 : 1
164
+
165
+ return await repository.save({
166
+ /*
167
+ `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.
168
+
169
+ 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은
170
+ 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은
171
+ 자리에서 domain 을 함께 넣는다.
172
+ */
173
+ domain,
174
+ ...figure,
175
+ version: nextVersion,
176
+ state: 'released',
177
+ updater: user,
178
+ /* 판본 구독자가 이것을 판본의 comment 로 옮긴다. Figure 의 칸이 아니다. */
179
+ __releaseComment: comment ?? null
180
+ } as any)
181
+ }
182
+
183
+ /**
184
+ * 옛 판으로 되돌린다.
185
+ *
186
+ * ## 무엇이 돌아오나
187
+ *
188
+ * 그 판의 **정본과 속성 정의, 그리고 그림**이다. 이름·설명·분류는 그대로 둔다 — 그것들은 언제든
189
+ * 바꿔도 안전한 카탈로그의 말이고, 되돌리기가 개명까지 되돌리면 사람이 시키지 않은 일을 한다.
190
+ *
191
+ * ## 되돌려도 발행은 아니다
192
+ *
193
+ * 초안으로 돌아온다. 다시 내보내려면 발행을 한 번 더 하고, 그때 판 번호는 **이미 쓴 번호 다음**을
194
+ * 받는다 — 3판을 되살렸다고 다시 3판이 되지 않는다. board 도 같다.
195
+ *
196
+ * ## 비용은 다시 잰다
197
+ *
198
+ * 판본이 든 점수·삼각형은 **그때의 형식**으로 잰 값이다. 지금 이 도형이 그 내용이 되었으므로
199
+ * 오늘 기준으로 다시 계산한다. 옛 값은 판본에 그대로 남아 있다.
200
+ */
201
+ @Directive('@transaction')
202
+ @Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
203
+ @Mutation(returns => FigureSaveResult, { description: 'Reverts a figure to one of its released versions. It comes back as a draft.' })
204
+ async revertFigureVersion(
205
+ @Arg('id') id: string,
206
+ @Arg('version') version: number,
207
+ @Ctx() context: ResolverContext
208
+ ): Promise<FigureSaveResult> {
209
+ const { domain, user, tx } = context.state
210
+ const repository = tx.getRepository(Figure)
211
+ const historyRepository = tx.getRepository(FigureHistory)
212
+
213
+ const figure = await repository.findOne({
214
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }
215
+ })
216
+ if (!figure) {
217
+ throw new Error(`Figure ${id} 를 찾을 수 없다`)
218
+ }
219
+
220
+ const history = await historyRepository.findOne({
221
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id, version }
222
+ })
223
+ if (!history) {
224
+ throw new Error(`Figure '${figure.type}' 에 ${version} 판이 없다`)
225
+ }
226
+
227
+ /* 형식 검사도 다시 받는다 — 옛 판이 오늘의 형식을 어길 수 있고, 그것을 조용히 들이지 않는다. */
228
+ const { source, derived, violations } = parseSource(history.source)
229
+
230
+ const saved = await repository.save({
231
+ /*
232
+ `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.
233
+
234
+ 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은
235
+ 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은
236
+ 자리에서 domain 을 함께 넣는다.
237
+ */
238
+ domain,
239
+ ...figure,
240
+ source,
241
+ ...derived,
242
+ properties: history.properties,
243
+ thumbnail: history.thumbnail,
244
+ thumbnailUpdatedAt: history.thumbnail === figure.thumbnail ? figure.thumbnailUpdatedAt : new Date(),
245
+ state: 'draft',
90
246
  updater: user
91
247
  })
92
248
 
@@ -4,6 +4,7 @@ import { Args, Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'
4
4
  import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
5
5
 
6
6
  import { Figure } from './figure.js'
7
+ import { FigureHistory } from './figure-history.js'
7
8
  import { FigureList } from './figure-type.js'
8
9
 
9
10
  @Resolver(Figure)
@@ -45,6 +46,42 @@ export class FigureQuery {
45
46
  *
46
47
  * `state` 로 걸러지지 않는다 — `draft` 인 Figure 의 이름도 이미 점유된 것이다.
47
48
  */
49
+ /**
50
+ * 발행된 판들 — 최근 것부터.
51
+ *
52
+ * 저작 중 저장은 여기 없다. 판본은 발행에서만 생기므로(`releaseFigure`), 이 목록이 곧
53
+ * 「이 도형이 몇 번 약속되었나」다.
54
+ */
55
+ @Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
56
+ @Query(returns => [FigureHistory], { description: 'Released versions of a figure, newest first.' })
57
+ async figureVersions(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<FigureHistory[]> {
58
+ const { domain } = context.state
59
+
60
+ return await getRepository(FigureHistory).find({
61
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },
62
+ relations: ['updater'],
63
+ order: { version: 'DESC' },
64
+ take: 10
65
+ })
66
+ }
67
+
68
+ /**
69
+ * 지금 소비처가 쓰는 판.
70
+ *
71
+ * 도형의 현재 정본은 저작 중일 수 있다(초안). 배치된 것이 보는 것은 **마지막으로 발행된 판**이고,
72
+ * 그것을 이 자리가 답한다.
73
+ */
74
+ @Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
75
+ @Query(returns => FigureHistory, { nullable: true, description: 'The most recently released version of a figure.' })
76
+ async figureReleased(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<FigureHistory> {
77
+ const { domain } = context.state
78
+
79
+ return await getRepository(FigureHistory).findOne({
80
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },
81
+ order: { version: 'DESC' }
82
+ })
83
+ }
84
+
48
85
  @Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
49
86
  @Query(returns => [String], { description: 'All figure type names in scope — used to check name collisions before create.' })
50
87
  async figureTypeNames(@Ctx() context: ResolverContext): Promise<string[]> {
@@ -58,8 +58,12 @@ export class FigurePatch {
58
58
  @Field({ nullable: true, description: 'The new master-data field definition.' })
59
59
  properties?: string
60
60
 
61
- @Field({ nullable: true, description: 'The new lifecycle state.' })
62
- state?: 'draft' | 'released'
61
+ /*
62
+ `state` 는 여기 없다 — **상태는 저장이 정하지 않는다.**
63
+
64
+ 발행은 `releaseFigure` 가 한다. 저장으로 상태를 바꿀 수 있게 두면 발행된 판을 덮어쓰면서 같은
65
+ 판 번호로 판본을 또 뜨게 되고(유니크 충돌), 무엇이 약속된 판인지도 흐려진다. board 도 같다.
66
+ */
63
67
 
64
68
  @Field({ nullable: true, description: 'A base64 encoded thumbnail image.' })
65
69
  thumbnail?: string
@@ -45,9 +45,21 @@ export class Figure {
45
45
  @Field(type => ID, { nullable: true, description: 'Unique identifier for the figure.' })
46
46
  readonly id?: string
47
47
 
48
- @Column({ nullable: true, default: 1 })
49
- @Field(type => Int, { nullable: true, description: 'Revision counter, incremented on each save.' })
50
- version?: number = 1
48
+ /**
49
+ * 발행된 판의 번호 — **발행할 때만 올라간다.**
50
+ *
51
+ * 저장 횟수가 아니다. 저작 중에는 몇 번을 고쳐 저장하든 그대로이고, `draft` 가 `released` 가
52
+ * 되는 순간에만 하나 오른다. 소비처가 「몇 번째 판을 쓰고 있나」를 이 값으로 말한다.
53
+ *
54
+ * 한 번도 발행되지 않았으면 **0** 이다. 1 로 시작하면 아직 아무에게도 약속하지 않은 초안이
55
+ * 첫 판인 것처럼 보인다.
56
+ */
57
+ @Column({ nullable: true, default: 0 })
58
+ @Field(type => Int, {
59
+ nullable: true,
60
+ description: 'Published version. Raised when the figure is released, not when it is saved. 0 means never released.'
61
+ })
62
+ version?: number = 0
51
63
 
52
64
  @ManyToOne(type => Domain)
53
65
  @Field(type => Domain, { nullable: true, description: 'The domain this figure belongs to.' })
@@ -1,6 +1,9 @@
1
1
  import { Figure } from './figure.js'
2
+ import { FigureHistory } from './figure-history.js'
2
3
  import { FigureQuery } from './figure-query.js'
3
4
  import { FigureMutation } from './figure-mutation.js'
5
+ import { FigureHistoryEntitySubscriber } from './event-subscriber.js'
4
6
 
5
- export const entities = [Figure]
7
+ export const entities = [Figure, FigureHistory]
6
8
  export const resolvers = [FigureQuery, FigureMutation]
9
+ export const subscribers = [FigureHistoryEntitySubscriber]
@@ -2,9 +2,30 @@
2
2
  export * from './figure/figure.js'
3
3
  export * from './figure/figure-type.js'
4
4
  export * from './figure/figure-source.js'
5
+ export * from './figure/figure-history.js'
5
6
 
6
7
  // Entities, resolvers
7
- import { entities as FigureEntities, resolvers as FigureResolvers } from './figure/index.js'
8
+ import {
9
+ entities as FigureEntities,
10
+ resolvers as FigureResolvers,
11
+ subscribers as FigureSubscribers
12
+ } from './figure/index.js'
8
13
 
9
14
  export const entities = [...FigureEntities]
10
- export const resolvers = [...FigureResolvers]
15
+
16
+ /* 판본 스냅샷을 뜨는 구독자. 셸이 이 이름으로 모아 TypeORM 에 등록한다(board-service 와 같다). */
17
+ export const subscribers = [...FigureSubscribers]
18
+
19
+ /*
20
+ * The shell reads `schema.resolverClasses` from every module — nothing else.
21
+ *
22
+ * shell/dist-server/schema.js orderedModuleNames.map(dep => loader(dep).schema)
23
+ *
24
+ * This module used to export the classes as a bare `resolvers` array, a name the shell never looks
25
+ * at. The table was migrated and the resolver classes were compiled, and still every query failed
26
+ * with `Cannot query field "figures" on type "Query"` — the field was not in the schema at all.
27
+ * board-service, ops-master and font-base all export the shape below.
28
+ */
29
+ export const schema = {
30
+ resolverClasses: [...FigureResolvers]
31
+ }