@things-factory/figure-service 10.1.116 → 10.1.121

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 (50) hide show
  1. package/dist-server/routers/figure-package-router.d.ts +7 -0
  2. package/dist-server/routers/figure-package-router.js +48 -0
  3. package/dist-server/routers/figure-package-router.js.map +1 -0
  4. package/dist-server/routes.js +3 -0
  5. package/dist-server/routes.js.map +1 -1
  6. package/dist-server/service/figure/figure-history.d.ts +1 -0
  7. package/dist-server/service/figure/figure-history.js +5 -0
  8. package/dist-server/service/figure/figure-history.js.map +1 -1
  9. package/dist-server/service/figure/figure-mutation.js +17 -2
  10. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  11. package/dist-server/service/figure/figure-namespace.d.ts +17 -0
  12. package/dist-server/service/figure/figure-namespace.js +48 -0
  13. package/dist-server/service/figure/figure-namespace.js.map +1 -0
  14. package/dist-server/service/figure/figure-package-export.d.ts +26 -0
  15. package/dist-server/service/figure/figure-package-export.js +90 -0
  16. package/dist-server/service/figure/figure-package-export.js.map +1 -0
  17. package/dist-server/service/figure/figure-query.d.ts +8 -0
  18. package/dist-server/service/figure/figure-query.js +61 -0
  19. package/dist-server/service/figure/figure-query.js.map +1 -1
  20. package/dist-server/service/figure/figure-rename.js +1 -1
  21. package/dist-server/service/figure/figure-rename.js.map +1 -1
  22. package/dist-server/service/figure/figure-thumbnail-store.d.ts +2 -0
  23. package/dist-server/service/figure/figure-thumbnail-store.js +2 -1
  24. package/dist-server/service/figure/figure-thumbnail-store.js.map +1 -1
  25. package/dist-server/service/figure/figure.d.ts +5 -0
  26. package/dist-server/service/figure/figure.js +5 -0
  27. package/dist-server/service/figure/figure.js.map +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +8 -6
  30. package/recipes/duct-fan/models/default.json +1 -1
  31. package/scripts/rename-figures-to-full-names.mjs +211 -0
  32. package/server/routers/figure-package-router.ts +53 -0
  33. package/server/routes.ts +3 -0
  34. package/server/service/figure/figure-deprecation-db.test.ts +13 -6
  35. package/server/service/figure/figure-history.ts +4 -0
  36. package/server/service/figure/figure-mutation.ts +28 -4
  37. package/server/service/figure/figure-namespace-db.test.ts +181 -0
  38. package/server/service/figure/figure-namespace.ts +59 -0
  39. package/server/service/figure/figure-package-export.ts +116 -0
  40. package/server/service/figure/figure-proposal-db.test.ts +9 -2
  41. package/server/service/figure/figure-query.ts +74 -1
  42. package/server/service/figure/figure-release-record-db.test.ts +13 -6
  43. package/server/service/figure/figure-rename-db.test.ts +19 -11
  44. package/server/service/figure/figure-rename.test.ts +12 -12
  45. package/server/service/figure/figure-rename.ts +1 -1
  46. package/server/service/figure/figure-thumbnail-db.test.ts +3 -0
  47. package/server/service/figure/figure-thumbnail-store.ts +4 -1
  48. package/server/service/figure/figure.ts +8 -0
  49. package/translations/en.json +10 -1
  50. package/translations/ko.json +10 -1
@@ -12,7 +12,7 @@ import { createFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hati
12
12
  import { FigureRenameRefused, renameDraftType } from './figure-rename'
13
13
 
14
14
  const draft = (over: Record<string, unknown> = {}) => ({
15
- type: 'FIGURE_MU6RJH47',
15
+ type: 'figure-mu6rjh47',
16
16
  source: serializeFigureAsset(createFigureAsset('새 Figure')),
17
17
  ...over
18
18
  })
@@ -26,26 +26,26 @@ describe('초안의 type 고치기', () => {
26
26
  */
27
27
  it('★ 행만 바뀌고, 원본은 그대로 다시 열린다', () => {
28
28
  const source = serializeFigureAsset(createFigureAsset('새 Figure'))
29
- const result = renameDraftType({ type: 'FIGURE_ABCDEF12', source }, 'ROLLER_CONVEYOR', never)
29
+ const result = renameDraftType({ type: 'figure-abcdef12', source }, 'roller-conveyor', never)
30
30
 
31
- expect(result.type).toBe('ROLLER_CONVEYOR')
31
+ expect(result.type).toBe('roller-conveyor')
32
32
  expect(result.source).toBe(source)
33
33
  expect(() => parseFigureAsset(result.source)).not.toThrow()
34
34
  })
35
35
 
36
36
  it('앞뒤 공백을 뗀다. 모양은 모델러가 묻는 규칙 그대로 — 대문자로 적지 않으면 거절한다(Q-D3, 2026-09-27)', () => {
37
- expect(renameDraftType(draft(), ' ROBOT_ARM ', never).type).toBe('ROBOT_ARM')
37
+ expect(renameDraftType(draft(), ' robot-arm ', never).type).toBe('robot-arm')
38
38
  /* It was kept as written; the modeller always sent it upper-cased, and every stored type already is (56/56). */
39
39
  expect(() => renameDraftType(draft(), 'Robot_Arm', never)).toThrow(expect.objectContaining({ code: 'malformed-type' }))
40
40
  })
41
41
 
42
42
  it('★ 한 번이라도 발행됐으면 거절한다 — 판 번호로', () => {
43
- expect(() => renameDraftType(draft(), 'ROBOT_ARM', { ...never, version: 1 })).toThrow(FigureRenameRefused)
43
+ expect(() => renameDraftType(draft(), 'robot-arm', { ...never, version: 1 })).toThrow(FigureRenameRefused)
44
44
  })
45
45
 
46
46
  it('★ 판 번호가 도로 0 이어도 이력이 있으면 거절한다 — 되돌리기가 그 자리를 만든다', () => {
47
47
  try {
48
- renameDraftType(draft(), 'ROBOT_ARM', { ...never, hasHistory: true })
48
+ renameDraftType(draft(), 'robot-arm', { ...never, hasHistory: true })
49
49
  throw new Error('거절하지 않았다')
50
50
  } catch (e) {
51
51
  expect((e as FigureRenameRefused).code).toBe('already-released')
@@ -54,11 +54,11 @@ describe('초안의 type 고치기', () => {
54
54
 
55
55
  it('같은 도메인에서 쓰는 이름이면 거절하고, 어느 이름인지 말한다', () => {
56
56
  try {
57
- renameDraftType(draft(), 'CONVEYOR', { ...never, takenByAnother: true })
57
+ renameDraftType(draft(), 'conveyor', { ...never, takenByAnother: true })
58
58
  throw new Error('거절하지 않았다')
59
59
  } catch (e) {
60
60
  expect((e as FigureRenameRefused).code).toBe('type-taken')
61
- expect((e as Error).message).toContain('CONVEYOR')
61
+ expect((e as Error).message).toContain('conveyor')
62
62
  }
63
63
  })
64
64
 
@@ -108,10 +108,10 @@ describe('문이 이 규칙을 지난다', () => {
108
108
 
109
109
  describe('a type being set keeps the shape the modeller asks for (Q-D3, designer 2026-09-27)', () => {
110
110
  const free = { version: 0, hasHistory: false, takenByAnother: false }
111
- it.each(['lower_case', 'X1', 'A'.repeat(33), '1ABC', 'AB-CD', 'AB__CD', '한글'])('refuses %s as malformed', wanted => {
112
- expect(() => renameDraftType({ type: 'OLD', source: '{}' }, wanted, free)).toThrow(expect.objectContaining({ code: 'malformed-type' }))
111
+ it.each(['UPPER', 'snake_case', 'x', 'a'.repeat(65), '1abc', 'ab--cd', 'ab-', '한글'])('refuses %s as malformed', wanted => {
112
+ expect(() => renameDraftType({ type: 'old', source: '{}' }, wanted, free)).toThrow(expect.objectContaining({ code: 'malformed-type' }))
113
113
  })
114
- it.each(['FIG_K3F9A2', 'CONVEYOR', 'LIFT_TABLE_2'])('takes %s', wanted => {
115
- expect(renameDraftType({ type: 'OLD', source: '{}' }, wanted, free).type).toBe(wanted)
114
+ it.each(['fig-k3f9a2', 'conveyor', 'lift-table-2'])('takes %s', wanted => {
115
+ expect(renameDraftType({ type: 'old', source: '{}' }, wanted, free).type).toBe(wanted)
116
116
  })
117
117
  })
@@ -65,7 +65,7 @@ export function renameDraftType(
65
65
  if (!TYPE_PATTERN.test(next) || next.length < TYPE_LENGTH.min || next.length > TYPE_LENGTH.max) {
66
66
  throw new FigureRenameRefused(
67
67
  'malformed-type',
68
- `type 「${next}」 는 대문자로 시작하고 대문자·숫자·밑줄만 쓰며 ${TYPE_LENGTH.min}~${TYPE_LENGTH.max}자여야 한다`
68
+ `type 「${next}」 는 소문자로 시작하고 소문자·숫자·하이픈 하나씩만 쓰며 ${TYPE_LENGTH.min}~${TYPE_LENGTH.max}자여야 한다`
69
69
  )
70
70
  }
71
71
 
@@ -42,6 +42,9 @@ function mapFiles() {
42
42
  koa.set('ETag', '"storage-md5"')
43
43
  koa.set('Cache-Control', 'public, max-age=31536000')
44
44
  koa.body = kept.get(path)
45
+ },
46
+ async read(path: string) {
47
+ return kept.get(path)!
45
48
  }
46
49
  }
47
50
  }
@@ -33,6 +33,8 @@ export const sha256Of = (bytes: Buffer) => createHash('sha256').update(bytes).di
33
33
  export interface ThumbnailFiles {
34
34
  put(bytes: Buffer, mimetype: string, name: string, context: any): Promise<{ id: string; path: string; size: number; contents?: Buffer }>
35
35
  send(koa: any, path: string, next: () => Promise<any>): Promise<void>
36
+ /** The bytes kept at a path -- what a figure package carries. */
37
+ read(path: string): Promise<Buffer>
36
38
  }
37
39
 
38
40
  export const attachmentFiles: ThumbnailFiles = {
@@ -46,7 +48,8 @@ export const attachmentFiles: ThumbnailFiles = {
46
48
  const { id, path, size, contents } = await STORAGE.uploadFile({ file, context })
47
49
  return { id, path, size, contents }
48
50
  },
49
- send: (koa, path, next) => STORAGE.sendFile(koa, path, next)
51
+ send: (koa, path, next) => STORAGE.sendFile(koa, path, next),
52
+ read: async path => Buffer.from(await STORAGE.readFile(path))
50
53
  }
51
54
 
52
55
  /*
@@ -77,6 +77,14 @@ export class Figure {
77
77
  @Field({ description: 'Component type name. A stored identifier — unique per domain and immutable once created.' })
78
78
  type?: string
79
79
 
80
+ /**
81
+ * The namespace the figure is published under, copied from the domain setting `figure-namespace` when the figure is
82
+ * made. With the type it forms the full name `{namespace}/{type}` boards store (ADR-0114). Never changes.
83
+ */
84
+ @Column({ nullable: true })
85
+ @Field({ nullable: true, description: 'Namespace of the full name {namespace}/{type}. Copied from the domain setting when created; never changes.' })
86
+ namespace?: string
87
+
80
88
  @Column()
81
89
  @Field({ description: 'Display name shown to authors. Safe to rename.' })
82
90
  name?: string
@@ -89,5 +89,14 @@
89
89
  "error.figure-release-not-found": "{type} has no release {version}. its latest release is {latest}.",
90
90
  "figure.gate.origin-off-centre.message": "The origin is off the middle of the declared room: {off}.",
91
91
  "figure.gate.origin-off-centre.why": "A drawing that places figures by the middle of their room puts this one that far from where it is drawn.",
92
- "figure.gate.origin-off-centre.how": "Keep it when the figure is fastened there, as a door at its hinge. Move the origin to the middle to place it by its room."
92
+ "figure.gate.origin-off-centre.how": "Keep it when the figure is fastened there, as a door at its hinge. Move the origin to the middle to place it by its room.",
93
+ "error.figure-namespace-missing": "this domain has no valid figure namespace ({value}). set {setting} first, for example hatiolab.com.",
94
+ "error.figure-namespace-changed": "the figure namespace of this domain is {namespace} and cannot change. set {setting} back to {namespace}.",
95
+ "error.figure-type-malformed": "{type} is not a figure type. use lower-case letters, digits and single hyphens, {min} to {max} characters, starting with a letter.",
96
+ "error.figure-name-malformed": "{type} is not a full figure name of the form namespace/type.",
97
+ "error.figure-not-released": "no release is published here.",
98
+ "error.figure-names-too-many": "at most {max} figure names can be asked at once, not {count}.",
99
+ "error.figure-package-size": "a package carries 1 to {max} releases, not {count}.",
100
+ "error.figure-ref-malformed": "{ref} is not a release reference of the form namespace/type@version.",
101
+ "error.figure-package-release-not-found": "{ref} is not a release this domain can read."
93
102
  }
@@ -89,5 +89,14 @@
89
89
  "error.figure-release-not-found": "{type} 에는 {version} 번 발행판이 없습니다. 최신 발행판은 {latest} 번입니다.",
90
90
  "figure.gate.origin-off-centre.message": "원점이 차지 공간 가운데에서 비켜 있습니다: {off}.",
91
91
  "figure.gate.origin-off-centre.why": "공간 가운데로 도형을 놓는 도면에서는 그린 자리에서 그만큼 떨어져 놓입니다.",
92
- "figure.gate.origin-off-centre.how": "문의 경첩처럼 붙이는 자리라면 그대로 둡니다. 공간 가운데로 놓이게 하려면 원점을 가운데로 옮깁니다."
92
+ "figure.gate.origin-off-centre.how": "문의 경첩처럼 붙이는 자리라면 그대로 둡니다. 공간 가운데로 놓이게 하려면 원점을 가운데로 옮깁니다.",
93
+ "error.figure-namespace-missing": "이 도메인에 Figure namespace 가 없거나 형식이 맞지 않습니다({value}). 설정 {setting} 에 예를 들어 hatiolab.com 을 먼저 넣어 주세요.",
94
+ "error.figure-namespace-changed": "이 도메인의 Figure namespace 는 {namespace} 이며 바꿀 수 없습니다. 설정 {setting} 을 {namespace} 로 되돌려 주세요.",
95
+ "error.figure-type-malformed": "{type} 은 Figure type 이 아닙니다. 소문자로 시작하고 소문자·숫자·하이픈 하나씩만 써서 {min}~{max}자로 정해 주세요.",
96
+ "error.figure-name-malformed": "{type} 은 namespace/type 형식의 정식 이름이 아닙니다.",
97
+ "error.figure-not-released": "이 도메인에 발행판이 없습니다.",
98
+ "error.figure-names-too-many": "Figure 이름은 한 번에 {max}개까지 물을 수 있습니다({count}개).",
99
+ "error.figure-package-size": "꾸러미에는 발행판을 1~{max}개 담을 수 있습니다({count}개).",
100
+ "error.figure-ref-malformed": "{ref} 은 namespace/type@버전 형식의 발행판 참조가 아닙니다.",
101
+ "error.figure-package-release-not-found": "{ref} 은 이 도메인에서 읽을 수 있는 발행판이 아닙니다."
93
102
  }