@things-factory/figure-service 10.1.49 → 10.1.50

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.49",
3
+ "version": "10.1.50",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -23,7 +23,7 @@
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.55",
26
+ "@hatiolab/figure-model": "^0.1.57",
27
27
  "@operato/typeorm-history": "^10.0.0",
28
28
  "@things-factory/ai-client-base": "^10.1.39",
29
29
  "@things-factory/auth-base": "^10.1.47",
@@ -33,5 +33,5 @@
33
33
  "graphql-upload": "^15.0.2",
34
34
  "koa-router": "^7.4.0"
35
35
  },
36
- "gitHead": "f9b29dcbdfecbfdd72e33bbcf74aea05331da75f"
36
+ "gitHead": "4700d39d70eb5fae4ad56cf47ea0b966c8516ae2"
37
37
  }
@@ -25,7 +25,9 @@ export class FigureMutation {
25
25
  @Arg('id', { nullable: true }) id?: string,
26
26
  @Arg('expectedSource', { nullable: true }) expectedSource?: string,
27
27
  @Arg('creationId', { nullable: true }) creationId?: string,
28
- @Arg('thumbnail', { nullable: true }) thumbnail?: string
28
+ @Arg('thumbnail', { nullable: true }) thumbnail?: string,
29
+ @Arg('type', { nullable: true, description: 'The type name a new figure is created under. Ignored for an existing one.' })
30
+ type?: string
29
31
  ): Promise<Figure> {
30
32
  const { domain, user, tx } = context.state
31
33
  const repository = tx.getRepository(Figure)
@@ -44,8 +46,23 @@ export class FigureMutation {
44
46
  not match, so a new V3 figure could be published under a machine name that then froze (ADR-0063 decision 4)
45
47
  — seen on :3300, 2026-09-24. A retry is still safe: the row id is the creation id, not the name.
46
48
  */
49
+ /*
50
+ The author names it on the first save, as V2 asked (lost in the move to V3, 2026-09-25): created under that
51
+ name or not at all -- a taken name refuses the save, rather than saving under the temporary one and failing a
52
+ rename after. Without a name (a caller that has none to give) it is the temporary one, renamed later.
53
+ */
54
+ const named = type?.trim()
55
+ if (named) {
56
+ const facts = await renameFacts(
57
+ { figures: repository, histories: tx.getRepository(FigureHistory) },
58
+ { id: creationId },
59
+ domain,
60
+ named
61
+ )
62
+ renameDraftType({ type: named, source: prepared.source }, named, facts)
63
+ }
47
64
  // insert, not save: a concurrent retry must never turn into an unchecked overwrite.
48
- await repository.insert({ ...prepared, id: creationId, type: placeholderType(),
65
+ await repository.insert({ ...prepared, id: creationId, type: named || placeholderType(),
49
66
  domain, creator: user, updater: user })
50
67
  return repository.findOneOrFail({ where: { id: creationId, domain: { id: domain.id } } })
51
68
  }
@@ -136,3 +136,48 @@ describe('이름을 바꾸기 전에 읽는 사실', () => {
136
136
  expect((await facts(figure, ' ')).takenByAnother).toBe(false)
137
137
  })
138
138
  })
139
+
140
+ /*
141
+ * The first save names the figure (V2 asked for the type on the first save; the V3 modeller lost it, 2026-09-25).
142
+ * Through the door itself, on sqlite: created under that name or not at all.
143
+ */
144
+ describe('처음 저장에 타입 이름을 받는다', () => {
145
+ let FigureMutation: any
146
+ let V3_SOURCE: string
147
+ beforeAll(() => {
148
+ FigureMutation = loadCompiled('service/figure/figure-mutation').FigureMutation
149
+ const model = require('@hatiolab/figure-model')
150
+ V3_SOURCE = model.serializeV3Asset(
151
+ model.addV3Box(model.createV3Asset('상자'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
152
+ )
153
+ })
154
+
155
+ const save = (creationId: string, type?: string) =>
156
+ new FigureMutation().saveFigureV3(
157
+ V3_SOURCE,
158
+ { state: { domain: DOMAIN, user: undefined, tx: dataSource!.manager } },
159
+ undefined,
160
+ undefined,
161
+ creationId,
162
+ undefined,
163
+ type
164
+ )
165
+ const ids = ['8a1f2c3d-4b5e-4f60-8a71-92b3c4d5e6f7', '9b2e3d4c-5a6f-4e71-9b82-a3c4d5e6f708']
166
+
167
+ it('준 이름으로 만든다 — 앞뒤 빈칸은 뺀다', async () => {
168
+ const saved = await save(ids[0], ' ROBOT_ARM ')
169
+ expect(saved.type).toBe('ROBOT_ARM')
170
+ })
171
+
172
+ it('★ 이미 쓰는 이름이면 저장하지 않는다 — 임시 이름으로 남기지 않는다', async () => {
173
+ await seedFigure({ type: 'CONVEYOR', name: '컨베이어' })
174
+ await expect(save(ids[1], 'CONVEYOR')).rejects.toThrow(/이미 쓰고 있다/)
175
+ expect(await dataSource!.getRepository(Figure).count({ where: { id: ids[1] } })).toBe(0)
176
+ })
177
+
178
+ it('이름 없이 부르면 종전대로 임시 이름 — 나중에 고친다', async () => {
179
+ const model = require('@hatiolab/figure-model')
180
+ const saved = await save(ids[0])
181
+ expect(model.isPlaceholderType(saved.type)).toBe(true)
182
+ })
183
+ })
@@ -110,13 +110,7 @@ describe('문이 이 규칙을 지난다', () => {
110
110
  V3 저장이 임시 이름을 스스로 지었다(`FIGURE_<32 hex>`). 형식의 `isPlaceholderType` 이 그것을 못 알아봐서
111
111
  발행 판정이 기계 이름을 통과시켰다. 짓는 쪽과 알아보는 쪽이 한 규칙이어야 한다.
112
112
  */
113
- it('새 V3 도형의 임시 이름은 형식의 규칙으로 짓는다 — 판정이 알아보는 그 규칙', () => {
114
- const mutation = readFileSync(join(__dirname, 'figure-mutation.ts'), 'utf8')
115
- const save = mutation.slice(mutation.indexOf('async saveFigureV3'), mutation.indexOf('async createFigure'))
116
-
117
- expect(save).toContain('type: placeholderType()')
118
- expect(save).not.toMatch(/type: `FIGURE_/)
119
- })
113
+ /* 새 V3 도형의 임시 이름이 판정이 알아보는 그 규칙인지는 문을 실제로 불러 본다 — figure-rename-db.test.ts 「이름 없이 부르면」. */
120
114
 
121
115
  it('문이 type 을 받는 칸을 갖는다', () => {
122
116
  const type = source('figure-type.ts')
@@ -92,7 +92,7 @@ export function renameDraftType(
92
92
  */
93
93
  export async function renameFacts(
94
94
  repositories: {
95
- figures: { findOne(options: any): Promise<{ id: string } | null> }
95
+ figures: { findOne(options: any): Promise<{ id?: string } | null> }
96
96
  histories: { count(options: any): Promise<number> }
97
97
  },
98
98
  figure: { id?: string; version?: number },