@things-factory/figure-service 10.1.3 → 10.1.6

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 (67) hide show
  1. package/coverage/clover.xml +6 -0
  2. package/coverage/coverage-final.json +1 -0
  3. package/coverage/lcov-report/base.css +224 -0
  4. package/coverage/lcov-report/block-navigation.js +87 -0
  5. package/coverage/lcov-report/favicon.png +0 -0
  6. package/coverage/lcov-report/index.html +101 -0
  7. package/coverage/lcov-report/prettify.css +1 -0
  8. package/coverage/lcov-report/prettify.js +2 -0
  9. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  10. package/coverage/lcov-report/sorter.js +210 -0
  11. package/coverage/lcov.info +0 -0
  12. package/dist-server/index.d.ts +2 -0
  13. package/dist-server/index.js +13 -0
  14. package/dist-server/index.js.map +1 -1
  15. package/dist-server/routers/figure-thumbnail-router.d.ts +24 -0
  16. package/dist-server/routers/figure-thumbnail-router.js +93 -0
  17. package/dist-server/routers/figure-thumbnail-router.js.map +1 -0
  18. package/dist-server/routes.d.ts +1 -0
  19. package/dist-server/routes.js +21 -0
  20. package/dist-server/routes.js.map +1 -0
  21. package/dist-server/service/figure/event-subscriber.d.ts +21 -0
  22. package/dist-server/service/figure/event-subscriber.js +51 -0
  23. package/dist-server/service/figure/event-subscriber.js.map +1 -0
  24. package/dist-server/service/figure/figure-gate.d.ts +55 -0
  25. package/dist-server/service/figure/figure-gate.js +64 -0
  26. package/dist-server/service/figure/figure-gate.js.map +1 -0
  27. package/dist-server/service/figure/figure-history.d.ts +43 -0
  28. package/dist-server/service/figure/figure-history.js +155 -0
  29. package/dist-server/service/figure/figure-history.js.map +1 -0
  30. package/dist-server/service/figure/figure-mutation.d.ts +45 -0
  31. package/dist-server/service/figure/figure-mutation.js +174 -1
  32. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  33. package/dist-server/service/figure/figure-query.d.ts +24 -1
  34. package/dist-server/service/figure/figure-query.js +75 -0
  35. package/dist-server/service/figure/figure-query.js.map +1 -1
  36. package/dist-server/service/figure/figure-tools.d.ts +10 -0
  37. package/dist-server/service/figure/figure-tools.js +304 -0
  38. package/dist-server/service/figure/figure-tools.js.map +1 -0
  39. package/dist-server/service/figure/figure-type.d.ts +25 -1
  40. package/dist-server/service/figure/figure-type.js +58 -5
  41. package/dist-server/service/figure/figure-type.js.map +1 -1
  42. package/dist-server/service/figure/figure.d.ts +9 -0
  43. package/dist-server/service/figure/figure.js +15 -3
  44. package/dist-server/service/figure/figure.js.map +1 -1
  45. package/dist-server/service/figure/index.d.ts +4 -1
  46. package/dist-server/service/figure/index.js +5 -2
  47. package/dist-server/service/figure/index.js.map +1 -1
  48. package/dist-server/service/index.d.ts +6 -2
  49. package/dist-server/service/index.js +17 -2
  50. package/dist-server/service/index.js.map +1 -1
  51. package/dist-server/tsconfig.tsbuildinfo +1 -0
  52. package/package.json +9 -6
  53. package/server/index.ts +13 -0
  54. package/server/routers/figure-thumbnail-router.ts +110 -0
  55. package/server/routes.ts +24 -0
  56. package/server/service/figure/event-subscriber.ts +49 -0
  57. package/server/service/figure/figure-gate.ts +119 -0
  58. package/server/service/figure/figure-history.ts +130 -0
  59. package/server/service/figure/figure-mutation.ts +180 -1
  60. package/server/service/figure/figure-propose.test.ts +136 -4
  61. package/server/service/figure/figure-query.ts +63 -1
  62. package/server/service/figure/figure-tools.test.ts +284 -0
  63. package/server/service/figure/figure-tools.ts +320 -0
  64. package/server/service/figure/figure-type.ts +49 -2
  65. package/server/service/figure/figure.ts +15 -3
  66. package/server/service/figure/index.ts +4 -1
  67. 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.6",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -23,11 +23,14 @@
23
23
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
24
24
  },
25
25
  "dependencies": {
26
- "@hatiolab/figure-model": "^0.1.12",
27
- "@things-factory/ai-client-base": "^10.1.3",
28
- "@things-factory/auth-base": "^10.1.3",
26
+ "@hatiolab/figure-model": "^0.1.15",
27
+ "@operato/typeorm-history": "^10.0.0",
28
+ "@things-factory/ai-client-base": "^10.1.6",
29
+ "@things-factory/auth-base": "^10.1.6",
29
30
  "@things-factory/env": "^10.1.3",
30
- "graphql-upload": "^15.0.2"
31
+ "@things-factory/shell": "^10.1.4",
32
+ "graphql-upload": "^15.0.2",
33
+ "koa-router": "^7.4.0"
31
34
  },
32
- "gitHead": "8407b1e9d1f47f6676af144d4a33c8ac01cb49c4"
35
+ "gitHead": "963aeb9ac470be5aea05bb5842a0a92afcab6036"
33
36
  }
package/server/index.ts CHANGED
@@ -1,2 +1,15 @@
1
+ import './routes.js'
2
+
3
+ /*
4
+ * 저작 도구를 어시스턴트 레지스트리에 등록한다.
5
+ *
6
+ * 이 패키지가 적재되면 대화면(도크)이 저작 중인 Figure 의 정본·비용·발행 판정을 읽고 후보를
7
+ * 요청할 수 있다. 능력을 복제하지 않고 figure-service 가 이미 가진 진입점을 투영한다 —
8
+ * 노출 범위는 대화면이 `toolCategories` 로 고른다(twin-ai · board-import 와 같은 방식).
9
+ */
10
+ import { registerFigureToolCategories } from './service/figure/figure-tools.js'
11
+ registerFigureToolCategories()
12
+
1
13
  export * from './service/index.js'
2
14
  export * from './migrations/index.js'
15
+ export { registerFigureToolCategories } from './service/figure/figure-tools.js'
@@ -0,0 +1,110 @@
1
+ import Router from 'koa-router'
2
+ import { In } from 'typeorm'
3
+
4
+ import { checkPermission } from '@things-factory/auth-base'
5
+ import { getRepository } from '@things-factory/shell'
6
+
7
+ import { Figure } from '../service/figure/figure.js'
8
+
9
+ /**
10
+ * 썸네일을 **그림으로** 내보내는 길.
11
+ *
12
+ * ## 왜 필요한가
13
+ *
14
+ * 썸네일은 base64 문자열로 칸에 들어 있다(`Board.thumbnail` 과 같은 방식). 그것을 목록 질의가
15
+ * 함께 실어 오면 카탈로그를 열 때마다 그림 전부가 JSON 안에 담겨 다시 온다 — data URL 은
16
+ * 브라우저가 캐시할 수 없다. [확인함] 표본 14개에 227KB, 한 장 최대 20KB.
17
+ *
18
+ * 그림으로 내보내면 브라우저가 캐시하고, 부르는 쪽이 `?v=thumbnailUpdatedAt` 을 붙이므로 그림이
19
+ * 다시 만들어진 것만 다시 받는다. 목록 질의에서는 `thumbnail` 을 아예 고르지 않는다.
20
+ *
21
+ * ## board 를 베끼면서 고친 것 셋
22
+ *
23
+ * `standalone-board-service-router` 의 `/thumbnail/:id` 가 본이지만 그대로 두면 안 되는 것들이 있다.
24
+ *
25
+ * 1. **길 이름이 겹친다.** 이 앱은 board 도 함께 싣는다(메뉴가 board-ui 를 끌어온다).
26
+ * `/thumbnail/:id` 로 두면 먼저 등록된 쪽이 이기고, 남의 표에서 id 를 찾아 404 를 낸다.
27
+ * 2. **행 전체를 읽는다.** board 는 `findOneBy` 로 행을 다 읽는다. figure 행에는 정본(`source`)이
28
+ * 붙어 있어, 그림 한 장에 정본까지 읽게 된다. 필요한 칸만 고른다.
29
+ * 3. **png 로 못 박는다.** figure 썸네일은 webp 다(모델러가 그렇게 만든다). data URL 이 스스로
30
+ * 말하는 형식을 쓴다 — 못 박으면 브라우저가 확장자와 다른 바이트를 받는다.
31
+ */
32
+ export const figureThumbnailRouter = new Router()
33
+
34
+ /** `data:image/webp;base64,....` → 형식과 바이트. 아니면 undefined. */
35
+ function decodeDataUrl(dataUrl: string): { type: string; buffer: Buffer } | undefined {
36
+ const matched = /^data:([^;,]+);base64,(.*)$/s.exec(dataUrl)
37
+ if (!matched) {
38
+ return undefined
39
+ }
40
+
41
+ return { type: matched[1], buffer: Buffer.from(matched[2], 'base64') }
42
+ }
43
+
44
+ figureThumbnailRouter.get('/figure-thumbnail/:id', async (context, next) => {
45
+ const { domain, user, unsafeIP, prohibitedPrivileges } = context.state
46
+
47
+ /*
48
+ **질의와 같은 문을 쓴다.**
49
+
50
+ `User.hasPrivilege('query','figure',…)` 만 보면 안 된다 — figure 질의는
51
+ `@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)` 로 선언되어 있고,
52
+ 도메인 소유자는 `figure:query` 권한 줄이 없어도 통과한다. 그 허용을 여기서 빼면 목록은 보이는데
53
+ 그림만 403 이 되어, 화면은 「그림이 아직 없다」고 거짓말한다. [확인함] 실제로 카드 14장이 전부
54
+ 403 이었다.
55
+
56
+ 조건을 손으로 다시 적지 않고 지시자가 쓰는 함수를 그대로 쓴다. 선언이 바뀌면 여기도 함께 바뀐다.
57
+ */
58
+ const allowed = await checkPermission(
59
+ { category: 'figure', privilege: 'query', owner: true },
60
+ user,
61
+ domain,
62
+ unsafeIP,
63
+ prohibitedPrivileges
64
+ )
65
+
66
+ if (!allowed) {
67
+ context.status = 403
68
+ return
69
+ }
70
+
71
+ const { id } = context.params
72
+
73
+ /*
74
+ 읽기는 부모 도메인까지 본다 — `figure` 질의와 같은 범위다. 물려받은 형상을 목록에는 보여 주고
75
+ 그림만 404 를 내면, 화면은 「그림이 아직 없다」고 거짓말하게 된다.
76
+ */
77
+ const found = await getRepository(Figure).findOne({
78
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id },
79
+ select: { id: true, thumbnail: true, thumbnailUpdatedAt: true }
80
+ })
81
+
82
+ const decoded = found?.thumbnail ? decodeDataUrl(found.thumbnail) : undefined
83
+
84
+ if (!decoded) {
85
+ context.status = 404
86
+ return
87
+ }
88
+
89
+ /*
90
+ **개인 캐시만 허락한다.** 이 그림은 권한을 본 뒤에 내주는 것이므로 공용 캐시(프록시)에 남으면
91
+ 안 된다. 부르는 쪽이 `?v=` 로 판을 가리므로 오래 두어도 낡은 그림이 남지 않고, `v` 없이 부른
92
+ 경우를 위해 시각을 함께 실어 다시 물어볼 수 있게 한다.
93
+ */
94
+ const stamp = found!.thumbnailUpdatedAt
95
+
96
+ context.type = decoded.type
97
+ context.set('Cache-Control', 'private, max-age=31536000')
98
+ if (stamp) {
99
+ context.set('Last-Modified', new Date(stamp).toUTCString())
100
+ context.set('ETag', `"${new Date(stamp).getTime()}"`)
101
+
102
+ /* 브라우저가 가진 것이 아직 그것이면 바이트를 보내지 않는다. */
103
+ if (context.fresh) {
104
+ context.status = 304
105
+ return
106
+ }
107
+ }
108
+
109
+ context.body = decoded.buffer
110
+ })
@@ -0,0 +1,24 @@
1
+ import { config } from '@things-factory/env'
2
+ import { getRoutePrefixForDomainType } from '@things-factory/shell'
3
+
4
+ import { figureThumbnailRouter } from './routers/figure-thumbnail-router.js'
5
+
6
+ /**
7
+ * 도메인 경로가 주소에 들어가는 배치인가.
8
+ *
9
+ * `subdomain` 설정이 있으면 도메인은 호스트 이름으로 갈리므로 경로에 도메인이 없다. 없으면
10
+ * `/domain/{subdomain}/...` 로 갈린다 — board-service 가 같은 판단을 같은 방식으로 한다.
11
+ */
12
+ const isPathBaseDomain = !config.get('subdomain')
13
+
14
+ process.on('bootstrap-module-domain-private-route' as any, (app, domainPrivateRouter) => {
15
+ if (isPathBaseDomain) {
16
+ domainPrivateRouter.use(
17
+ `/${getRoutePrefixForDomainType()}/:domain`,
18
+ figureThumbnailRouter.routes(),
19
+ figureThumbnailRouter.allowedMethods()
20
+ )
21
+ } else {
22
+ domainPrivateRouter.use('', figureThumbnailRouter.routes(), figureThumbnailRouter.allowedMethods())
23
+ }
24
+ })
@@ -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,119 @@
1
+ import { compile, sizingReport, validate } from '@hatiolab/figure-model'
2
+ import type { FigureSource } from '@hatiolab/figure-model'
3
+
4
+ /**
5
+ * 발행해도 되나 — **서버가 판정한다.**
6
+ *
7
+ * ## 왜 서버인가
8
+ *
9
+ * 이 검사는 브라우저 하네스에만 있었다(`test/browser/check-figure-scaling.mjs`). 사람이 돌릴 때만
10
+ * 돌았고, 발행 경로에는 아무 검사도 없었다 — 형식만 맞으면 무엇이든 발행되었다. 발행은 소비처에게
11
+ * 하는 약속이므로, 그 약속을 지킬 수 있는지는 약속하는 자리가 재야 한다.
12
+ *
13
+ * ## 무엇을 막고 무엇을 알리나
14
+ *
15
+ * 판정은 `figure-model` 의 `gate.ts` 가 이미 문서로 정해 두었다. 여기서 새로 정하지 않는다.
16
+ *
17
+ * sizing-splits 크기를 바꾸면 조각이 갈라진다 **막는다**
18
+ * sizing-loose 1배에서 이미 여러 조각이다 알린다
19
+ * violations 형식이 권하지 않는 것 알린다 (저장 때 이미 보여 준 것들)
20
+ *
21
+ * 갈라짐을 막는 이유는 사람 눈에 「부서졌다」로 보이기 때문이다 — 시그널타워를 3배로 세우니 램프가
22
+ * 기둥 위 공중에 떴던 그것이고, 앵커 규칙이 생긴 이유다. 소비처가 형상을 늘려 쓰는 것은 막을 수
23
+ * 없으니(그것이 이 형식의 값이다), 늘려도 견디는지를 발행 전에 본다.
24
+ *
25
+ * ## 재 본 값
26
+ *
27
+ * [확인함] 2026-09-09 · 표본 15개(시드 14 + 저작 1)
28
+ * 형식 오류 0 · 1배 분리 0 · **갈라짐 1**(MIXER, z·y-z 조합) · 조용함 14
29
+ *
30
+ * 게이트가 벽이 아니라는 뜻이다. 열넷은 그대로 발행되고, 하나는 고칠 것이 있다.
31
+ */
32
+
33
+ /** 게이트가 낸 한 가지. `blocking` 이 참이면 발행이 안 된다. */
34
+ export interface GateFinding {
35
+ code: string
36
+ message: string
37
+ /**
38
+ * **왜 그것이 문제인가.** 잰 것만 보여 주면 읽는 사람이 「그래서?」에서 멈춘다 — 화면에
39
+ * 「1 조각이 2 조각으로 갈라진다」만 떴을 때 실제로 그랬다.
40
+ */
41
+ why?: string
42
+ /** **무엇을 바꾸면 되나.** 부품 이름과 값으로. 형식이 만든 문장을 그대로 옮긴다. */
43
+ how?: string
44
+ /** 어디를 가리키나 — 갈라짐은 갈라진 부품 이름들, 형식 위반은 정본 안의 경로. */
45
+ at?: string
46
+ blocking: boolean
47
+ }
48
+
49
+ export interface Inspection {
50
+ findings: GateFinding[]
51
+ /** 막는 것이 하나라도 있나. 화면이 이것으로 단추를 잠근다. */
52
+ blocked: boolean
53
+ }
54
+
55
+ /**
56
+ * 정본 하나를 판정한다.
57
+ *
58
+ * 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를
59
+ * 조용히 통과시키면 빈 것이 발행된다.
60
+ */
61
+ export function inspectSource(sourceJson?: string | null): Inspection {
62
+ if (!sourceJson) {
63
+ return {
64
+ blocked: true,
65
+ findings: [
66
+ {
67
+ code: 'no-source',
68
+ message: '저장된 정본이 없다. 저장하면 발행할 수 있다',
69
+ blocking: true
70
+ }
71
+ ]
72
+ }
73
+ }
74
+
75
+ let parsed: unknown
76
+ try {
77
+ parsed = JSON.parse(sourceJson)
78
+ } catch (e) {
79
+ return {
80
+ blocked: true,
81
+ findings: [{ code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, blocking: true }]
82
+ }
83
+ }
84
+
85
+ const result = validate(parsed)
86
+ const findings: GateFinding[] = []
87
+
88
+ /*
89
+ 형식 오류는 **저장을 통과할 수 없다**(`parseSource` 가 거절한다). 그래도 여기서 다시 보는 이유는,
90
+ 이 판정이 저장 경로와 독립이어야 하기 때문이다 — 옛 정본이 남아 있거나 형식이 엄해진 뒤라면
91
+ 저장 때는 맞았던 것이 지금은 틀릴 수 있다. 그때 발행이 통과하면 안 된다.
92
+ */
93
+ for (const error of result.errors) {
94
+ findings.push({ code: error.code, message: error.message, at: error.path, blocking: true })
95
+ }
96
+
97
+ for (const violation of result.violations) {
98
+ findings.push({ code: violation.code, message: violation.message, blocking: false })
99
+ }
100
+
101
+ if (result.errors.length === 0) {
102
+ const report = sizingReport(compile(parsed as FigureSource))
103
+
104
+ for (const finding of report.findings) {
105
+ findings.push({
106
+ code: finding.code,
107
+ message: finding.message,
108
+ /* 왜·어떻게는 형식이 소유한다. 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다. */
109
+ why: finding.why,
110
+ how: finding.how,
111
+ /* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */
112
+ at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
113
+ blocking: finding.code === 'sizing-splits'
114
+ })
115
+ }
116
+ }
117
+
118
+ return { findings, blocked: findings.some(finding => finding.blocking) }
119
+ }
@@ -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
+ }