@things-factory/figure-service 10.1.35 → 10.1.36
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/dist-server/service/figure/figure-mutation.d.ts +1 -0
- package/dist-server/service/figure/figure-mutation.js +42 -0
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-v3-write.d.ts +21 -0
- package/dist-server/service/figure/figure-v3-write.js +30 -0
- package/dist-server/service/figure/figure-v3-write.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/figure/figure-mutation.ts +38 -0
- package/server/service/figure/figure-v3-write.ts +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.36",
|
|
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.
|
|
26
|
+
"@hatiolab/figure-model": "^0.1.35",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
28
|
"@things-factory/ai-client-base": "^10.1.31",
|
|
29
29
|
"@things-factory/auth-base": "^10.1.34",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"graphql-upload": "^15.0.2",
|
|
34
34
|
"koa-router": "^7.4.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "3942ac0aa4f004b4700e739889e12ffc35d78794"
|
|
37
37
|
}
|
|
@@ -11,9 +11,47 @@ import { inspectSource } from './figure-gate.js'
|
|
|
11
11
|
import { renameDraftType, renameFacts } from './figure-rename.js'
|
|
12
12
|
import { parseSource } from './figure-source.js'
|
|
13
13
|
import { ProposeFailure, proposeFigure } from './figure-propose.js'
|
|
14
|
+
import { prepareV3Write, compareAndSwapV3 } from './figure-v3-write.js'
|
|
14
15
|
|
|
15
16
|
@Resolver(Figure)
|
|
16
17
|
export class FigureMutation {
|
|
18
|
+
@Directive('@transaction')
|
|
19
|
+
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
20
|
+
@Mutation(returns => Figure, { description: 'Save a V3 asset draft using an exact-source concurrency check.' })
|
|
21
|
+
async saveFigureV3(
|
|
22
|
+
@Arg('source') source: string,
|
|
23
|
+
@Ctx() context: ResolverContext,
|
|
24
|
+
@Arg('id', { nullable: true }) id?: string,
|
|
25
|
+
@Arg('expectedSource', { nullable: true }) expectedSource?: string,
|
|
26
|
+
@Arg('creationId', { nullable: true }) creationId?: string
|
|
27
|
+
): Promise<Figure> {
|
|
28
|
+
const { domain, user, tx } = context.state
|
|
29
|
+
const repository = tx.getRepository(Figure)
|
|
30
|
+
if (!id) {
|
|
31
|
+
if (!creationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(creationId))
|
|
32
|
+
throw Error('새 모델 저장에는 생성 요청 식별자가 필요합니다.')
|
|
33
|
+
const prepared = prepareV3Write(source)
|
|
34
|
+
const previous = await repository.findOne({ where: { id: creationId, domain: { id: domain.id } } })
|
|
35
|
+
if (previous) {
|
|
36
|
+
if (previous.source !== prepared.source) throw Error('이 생성 요청은 이미 다른 내용으로 저장되었습니다. 저장된 모델을 다시 열어 주세요.')
|
|
37
|
+
return previous
|
|
38
|
+
}
|
|
39
|
+
// insert, not save: a concurrent retry must never turn into an unchecked overwrite.
|
|
40
|
+
await repository.insert({ ...prepared, id: creationId, type: `FIGURE_${creationId.replace(/-/g, '').toUpperCase()}`,
|
|
41
|
+
domain, creator: user, updater: user })
|
|
42
|
+
return repository.findOneOrFail({ where: { id: creationId, domain: { id: domain.id } } })
|
|
43
|
+
}
|
|
44
|
+
// Exact tenant ownership: an inherited catalog entry is not writable in the child domain.
|
|
45
|
+
const figure = await repository.findOne({ where: { id, domain: { id: domain.id } } })
|
|
46
|
+
if (!figure) throw Error('편집할 Figure를 찾을 수 없습니다.')
|
|
47
|
+
const prepared = prepareV3Write(source, figure, expectedSource)
|
|
48
|
+
// Compare-and-swap in the UPDATE itself: supported by SQLite as well as PostgreSQL.
|
|
49
|
+
// A pessimistic row lock is not supported by the product's SQLite deployment.
|
|
50
|
+
await compareAndSwapV3(criteria => repository.update(criteria, { ...prepared, updater: user }),
|
|
51
|
+
id, domain.id, expectedSource)
|
|
52
|
+
return repository.findOneOrFail({ where: { id, domain: { id: domain.id } } })
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
@Directive('@transaction')
|
|
18
56
|
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
19
57
|
@Mutation(returns => FigureSaveResult, { description: 'Creates a figure. Rejects when the source violates the format.' })
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseV3Asset, serializeV3Asset, compileV3Asset } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
export interface V3WriteCriteria {
|
|
4
|
+
id: string
|
|
5
|
+
domain: { id: string }
|
|
6
|
+
source: string
|
|
7
|
+
}
|
|
8
|
+
export async function compareAndSwapV3(
|
|
9
|
+
update: (criteria: V3WriteCriteria) => Promise<{ affected?: number | null }>,
|
|
10
|
+
id: string,
|
|
11
|
+
domainId: string,
|
|
12
|
+
expectedSource: string | undefined
|
|
13
|
+
) {
|
|
14
|
+
if (expectedSource === undefined) throw Error('수정 전 정본이 필요합니다.')
|
|
15
|
+
const result = await update({ id, domain: { id: domainId }, source: expectedSource })
|
|
16
|
+
if (result.affected !== 1) throw Error('다른 편집에서 모델이 변경되었습니다. 최신 모델을 다시 열어 주세요.')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Early validation only. The repository must also compare expectedSource in its atomic UPDATE. */
|
|
20
|
+
export function prepareV3Write(source: string, existing?: { source?: string }, expectedSource?: string) {
|
|
21
|
+
if (existing && (expectedSource === undefined || existing.source !== expectedSource)) {
|
|
22
|
+
throw Error('다른 편집에서 모델이 변경되었습니다. 최신 모델을 다시 연 뒤 적용해 주세요.')
|
|
23
|
+
}
|
|
24
|
+
const asset = parseV3Asset(source)
|
|
25
|
+
const evaluated = compileV3Asset(asset).evaluate()
|
|
26
|
+
return {
|
|
27
|
+
source: serializeV3Asset(asset),
|
|
28
|
+
name: asset.name,
|
|
29
|
+
// V2 scores are not V3 evidence. Clear them instead of carrying a previous model's score.
|
|
30
|
+
score: null,
|
|
31
|
+
triangles: null,
|
|
32
|
+
groups: evaluated.geometry.length,
|
|
33
|
+
state: 'draft' as const
|
|
34
|
+
}
|
|
35
|
+
}
|