@things-factory/figure-service 10.1.6 → 10.1.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.6",
3
+ "version": "10.1.7",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -25,12 +25,12 @@
25
25
  "dependencies": {
26
26
  "@hatiolab/figure-model": "^0.1.15",
27
27
  "@operato/typeorm-history": "^10.0.0",
28
- "@things-factory/ai-client-base": "^10.1.6",
29
- "@things-factory/auth-base": "^10.1.6",
28
+ "@things-factory/ai-client-base": "^10.1.7",
29
+ "@things-factory/auth-base": "^10.1.7",
30
30
  "@things-factory/env": "^10.1.3",
31
- "@things-factory/shell": "^10.1.4",
31
+ "@things-factory/shell": "^10.1.7",
32
32
  "graphql-upload": "^15.0.2",
33
33
  "koa-router": "^7.4.0"
34
34
  },
35
- "gitHead": "963aeb9ac470be5aea05bb5842a0a92afcab6036"
35
+ "gitHead": "9a036f83f5751f9e4af9180e9a0018f716a4ba27"
36
36
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 분류와 태그가 **정말 써지는가** — sqlite 를 실제로 들고 확인한다.
3
+ *
4
+ * ── 왜 순수 시험으로는 부족한가 ─────────────────────────────────────────────
5
+ * 이 저장 경로는 읽어 온 엔티티를 펼쳐 `save` 한다(`{ ...figure, ...patch }`). 그 모양은 칸이
6
+ * **조용히 안 써지는** 자리로 알려져 있다 — `save` 는 정상 반환하고 로그도 성공이라고 말한다.
7
+ * 다른 레인이 도메인 시각대를 그렇게 잃었고, `select` 를 해 보기 전에는 못 찾았다.
8
+ *
9
+ * 그래서 여기서는 반환값을 믿지 않고 **저장소에서 다시 읽는다.**
10
+ *
11
+ * ── 이 하네스로 볼 수 없는 것 ───────────────────────────────────────────────
12
+ * sqlite 다. 동시성·격리·컬럼 타입 방언은 여기서 초록이어도 근거가 아니다. `simple-json` 이
13
+ * 드라이버마다 어떻게 저장되는지도 postgres 에서 따로 봐야 한다 — 여기서 보는 것은 **왕복**이다.
14
+ */
15
+ import type { DataSource } from 'typeorm'
16
+
17
+ const { DataSource: TypeOrmDataSource } = require('typeorm')
18
+ const shell = require('@things-factory/shell')
19
+ /* Figure 가 creator·updater 로 User 를 가리킨다 — 그 엔티티가 없으면 메타데이터가 안 선다. */
20
+ const authBase = require('@things-factory/auth-base')
21
+
22
+ let dataSource: DataSource | null = null
23
+ let Figure: any
24
+
25
+ /*
26
+ 엔티티는 컴파일된 산출물에서 든다.
27
+
28
+ `@Field({ nullable: true })` 처럼 명시 타입이 없는 type-graphql 데코레이터는
29
+ `emitDecoratorMetadata` 가 있어야 읽히는데 jest 의 변환기는 babel 이라 그것을 못 낸다.
30
+ 소스에서 들면 import 시점에 죽는다. auth-base 가 같은 이유로 같은 길을 간다(tests/compiled.ts).
31
+ */
32
+ function loadCompiled(modulePath: string): any {
33
+ const { existsSync } = require('fs')
34
+ const { join } = require('path')
35
+ const resolved = join(__dirname, '..', '..', '..', 'dist-server', `${modulePath}.js`)
36
+
37
+ if (!existsSync(resolved)) {
38
+ throw new Error(
39
+ `figure-service 산출물이 없다: ${resolved}\n` +
40
+ `이 시험은 tsc 산출물을 든다. 먼저 빌드할 것:\n` +
41
+ ` yarn workspace @things-factory/figure-service build:server`
42
+ )
43
+ }
44
+
45
+ return require(resolved)
46
+ }
47
+
48
+ beforeAll(async () => {
49
+ Figure = loadCompiled('service/figure/figure').Figure
50
+
51
+ dataSource = new TypeOrmDataSource({
52
+ type: 'sqlite',
53
+ database: ':memory:',
54
+ synchronize: true,
55
+ dropSchema: true,
56
+ namingStrategy: new shell.NamingStrategy(),
57
+ entities: [...shell.entities, ...authBase.entities, Figure],
58
+ logging: false
59
+ })
60
+
61
+ await dataSource.initialize()
62
+ shell.addDataSource('default', dataSource)
63
+ shell.addDataSource('tx', dataSource)
64
+ })
65
+
66
+ beforeEach(async () => {
67
+ await dataSource!.synchronize(true)
68
+ })
69
+
70
+ afterAll(async () => {
71
+ if (!dataSource) return
72
+ shell.removeDataSource('default')
73
+ shell.removeDataSource('tx')
74
+ await dataSource.destroy()
75
+ dataSource = null
76
+ })
77
+
78
+ /** 저장소에서 **다시 읽은** 것. 반환값이 아니라 이것이 근거다. */
79
+ async function readBack(id: string) {
80
+ return await dataSource!.getRepository(Figure).findOne({ where: { id } })
81
+ }
82
+
83
+ async function seed(over: Record<string, unknown> = {}) {
84
+ const repository = dataSource!.getRepository(Figure)
85
+ return await repository.save({
86
+ type: 'CONVEYOR',
87
+ name: '컨베이어',
88
+ source: '{}',
89
+ state: 'draft',
90
+ ...over
91
+ })
92
+ }
93
+
94
+ describe('태그가 정말 써진다', () => {
95
+ it('여럿 붙고, 다시 읽어도 그대로다', async () => {
96
+ const saved = await seed({ tags: ['반출', '자동'] })
97
+ const read = await readBack(saved.id)
98
+
99
+ expect(read!.tags).toEqual(['반출', '자동'])
100
+ })
101
+
102
+ /*
103
+ 이 저장 경로의 모양이다 — 읽어 온 것을 펼쳐 새 값을 얹는다. 여기서 칸이 조용히 빠지는 것이
104
+ 기억에 남은 그 함정이다.
105
+ */
106
+ it('읽어 온 것을 펼쳐 얹어도 써진다 — save 가 그 칸을 흘리지 않는다', async () => {
107
+ const saved = await seed({ tags: ['반출'] })
108
+ const before = await readBack(saved.id)
109
+
110
+ await dataSource!.getRepository(Figure).save({ ...before, tags: ['반출', '2층'] })
111
+
112
+ expect((await readBack(saved.id))!.tags).toEqual(['반출', '2층'])
113
+ })
114
+
115
+ it('빈 배열은 전부 지운다 — 지울 방법이 있어야 한다', async () => {
116
+ const saved = await seed({ tags: ['반출'] })
117
+ const before = await readBack(saved.id)
118
+
119
+ await dataSource!.getRepository(Figure).save({ ...before, tags: [] })
120
+
121
+ expect((await readBack(saved.id))!.tags).toEqual([])
122
+ })
123
+
124
+ it('안 준 도형은 태그가 없다 — 빈 배열로 갈음하지 않는다', async () => {
125
+ const saved = await seed()
126
+
127
+ expect((await readBack(saved.id))!.tags).toBeNull()
128
+ })
129
+ })
130
+
131
+ describe('분류는 하나다', () => {
132
+ it('써지고 다시 읽힌다', async () => {
133
+ const saved = await seed({ category: '컨베이어' })
134
+
135
+ expect((await readBack(saved.id))!.category).toBe('컨베이어')
136
+ })
137
+
138
+ it('분류와 태그는 서로 손대지 않는다', async () => {
139
+ const saved = await seed({ category: '컨베이어', tags: ['반출'] })
140
+ const before = await readBack(saved.id)
141
+
142
+ await dataSource!.getRepository(Figure).save({ ...before, tags: ['반출', '자동'] })
143
+ const read = await readBack(saved.id)
144
+
145
+ expect(read!.category).toBe('컨베이어')
146
+ expect(read!.tags).toEqual(['반출', '자동'])
147
+ })
148
+ })
149
+
150
+ describe('★ 이 시험이 실제로 DB 를 읽고 있다', () => {
151
+ it('없는 것을 물으면 없다고 한다 — 늘 초록이 아니다', async () => {
152
+ expect(await readBack('없는-id')).toBeNull()
153
+ })
154
+
155
+ it('표가 실제로 서 있다', async () => {
156
+ const saved = await seed()
157
+ const rows = await dataSource!.query('select count(*) as n from figures')
158
+
159
+ expect(Number(rows[0].n)).toBe(1)
160
+ expect(saved.id).toBeTruthy()
161
+ })
162
+ })
@@ -31,7 +31,14 @@ export class FigureQuery {
31
31
  params,
32
32
  domain,
33
33
  alias: 'figure',
34
- searchables: ['type', 'name', 'description', 'category']
34
+ /*
35
+ `tags` 도 찾을 수 있어야 한다 — 태그의 값은 붙이는 데 있지 않고 **찾는 데** 있다.
36
+
37
+ `simple-json` 이라 칸에는 `["반출","자동"]` 이 통째로 들어 있고, LIKE 는 그 글자를
38
+ 본다. 「반출」로 찾으면 걸린다. 정확히 그 태그만 걸리는 것은 아니지만(이름에 「반출」이
39
+ 든 도형도 걸린다) 찾기로는 그것이 오히려 낫다.
40
+ */
41
+ searchables: ['type', 'name', 'description', 'category', 'tags']
35
42
  })
36
43
 
37
44
  const [items, total] = await queryBuilder.getManyAndCount()
@@ -22,6 +22,9 @@ export class NewFigure {
22
22
  @Field({ nullable: true, description: 'Classification code used to group figures in the palette.' })
23
23
  category?: string
24
24
 
25
+ @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
26
+ tags?: string[]
27
+
25
28
  @Field({ description: 'The canonical FigureSource JSON.' })
26
29
  source: string
27
30
 
@@ -52,6 +55,13 @@ export class FigurePatch {
52
55
  @Field({ nullable: true, description: 'The new classification code.' })
53
56
  category?: string
54
57
 
58
+ /*
59
+ 빈 배열과 안 준 것을 가른다. `undefined` 는 「건드리지 않는다」, `[]` 는 「전부 지운다」다.
60
+ 둘을 뭉개면 태그를 다 지우려는 저작자가 지울 방법을 잃는다.
61
+ */
62
+ @Field(type => [String], { nullable: true, description: 'The new tags. An empty list clears them; omitting the field leaves them.' })
63
+ tags?: string[]
64
+
55
65
  @Field({ nullable: true, description: 'The new FigureSource JSON.' })
56
66
  source?: string
57
67
 
@@ -89,11 +89,31 @@ export class Figure {
89
89
  @Field({ nullable: true, description: 'A detailed description for the figure.' })
90
90
  description?: string
91
91
 
92
- /** 분류 코드. 팔레트에서 묶어 보이는 기준. */
92
+ /**
93
+ * 분류 하나. **팔레트에서 묶어 보이는 기준이다.**
94
+ *
95
+ * 도형 하나에 하나뿐이다 — 팔레트는 목록을 한 벌로 나눠 보여 주므로, 한 도형이 두 묶음에
96
+ * 동시에 있으면 저작자가 같은 것을 두 번 만난다.
97
+ */
93
98
  @Column({ nullable: true })
94
- @Field({ nullable: true, description: 'Classification code used to group figures in the palette.' })
99
+ @Field({ nullable: true, description: 'Classification code used to group figures in the palette. One per figure.' })
95
100
  category?: string
96
101
 
102
+ /**
103
+ * 태그. **여럿 붙는다.**
104
+ *
105
+ * 분류와 다른 일을 한다. 분류는 「이것이 무엇인가」를 한 번 정하는 것이고, 태그는 「어디에
106
+ * 걸리는가」를 여러 갈래로 적는 것이다 — 컨베이어이면서 반출 구역의 것일 수 있다. 그래서
107
+ * 분류로는 못 하는 찾기가 된다.
108
+ *
109
+ * `simple-json` 으로 담는다. 사내 선례가 그것이고(`ops-master` · `organization` ·
110
+ * `personalization`), 드라이버마다 배열 타입이 다른 것을 피한다.
111
+ */
112
+ /* `simple-json` 과 `default: null` 은 사내 선례 그대로다 — board-template · attachment 가 같은 모양이다. */
113
+ @Column('simple-json', { nullable: true, default: null })
114
+ @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
115
+ tags?: string[]
116
+
97
117
  @Column({ nullable: true, default: 'draft' })
98
118
  @Field({ nullable: true, description: "Lifecycle state — 'draft' figures do not appear in the board palette." })
99
119
  state?: 'draft' | 'released'