@things-factory/figure-service 10.1.178 → 10.1.180
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/index.d.ts +2 -0
- package/dist-server/index.js +6 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/routers/figure-mesh-router.js +4 -1
- package/dist-server/routers/figure-mesh-router.js.map +1 -1
- package/dist-server/routers/figure-thumbnail-router.js +3 -7
- package/dist-server/routers/figure-thumbnail-router.js.map +1 -1
- package/dist-server/service/figure/figure-categories.d.ts +10 -0
- package/dist-server/service/figure/figure-categories.js +53 -0
- package/dist-server/service/figure/figure-categories.js.map +1 -0
- package/dist-server/service/figure/figure-format.d.ts +18 -1
- package/dist-server/service/figure/figure-format.js +28 -1
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.d.ts +8 -2
- package/dist-server/service/figure/figure-mutation.js +83 -34
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-namespace-entity.d.ts +24 -0
- package/dist-server/service/figure/figure-namespace-entity.js +87 -0
- package/dist-server/service/figure/figure-namespace-entity.js.map +1 -0
- package/dist-server/service/figure/figure-namespace-resolver.d.ts +5 -8
- package/dist-server/service/figure/figure-namespace-resolver.js +52 -36
- package/dist-server/service/figure/figure-namespace-resolver.js.map +1 -1
- package/dist-server/service/figure/figure-namespace.d.ts +28 -28
- package/dist-server/service/figure/figure-namespace.js +105 -63
- package/dist-server/service/figure/figure-namespace.js.map +1 -1
- package/dist-server/service/figure/figure-package-export.js +4 -2
- package/dist-server/service/figure/figure-package-export.js.map +1 -1
- package/dist-server/service/figure/figure-public.d.ts +19 -0
- package/dist-server/service/figure/figure-public.js +54 -0
- package/dist-server/service/figure/figure-public.js.map +1 -0
- package/dist-server/service/figure/figure-query.d.ts +7 -0
- package/dist-server/service/figure/figure-query.js +59 -5
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-rename.d.ts +25 -2
- package/dist-server/service/figure/figure-rename.js +21 -2
- package/dist-server/service/figure/figure-rename.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +1 -1
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +1 -0
- package/dist-server/service/figure/figure-type.js +4 -0
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/service/figure/figure.d.ts +1 -0
- package/dist-server/service/figure/figure.js +5 -0
- package/dist-server/service/figure/figure.js.map +1 -1
- package/dist-server/service/figure/index.d.ts +2 -1
- package/dist-server/service/figure/index.js +2 -1
- package/dist-server/service/figure/index.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/index.ts +2 -0
- package/server/routers/figure-mesh-router.ts +5 -1
- package/server/routers/figure-thumbnail-router.ts +3 -5
- package/server/service/figure/figure-categories.ts +50 -0
- package/server/service/figure/figure-category-db.test.ts +127 -0
- package/server/service/figure/figure-deprecation-db.test.ts +110 -6
- package/server/service/figure/figure-format.ts +43 -1
- package/server/service/figure/figure-mesh-db.test.ts +4 -2
- package/server/service/figure/figure-mutation.ts +79 -37
- package/server/service/figure/figure-namespace-db.test.ts +140 -74
- package/server/service/figure/figure-namespace-entity.ts +67 -0
- package/server/service/figure/figure-namespace-resolver.ts +39 -26
- package/server/service/figure/figure-namespace.ts +122 -81
- package/server/service/figure/figure-package-export.ts +6 -3
- package/server/service/figure/figure-proposal-db.test.ts +5 -3
- package/server/service/figure/figure-public-db.test.ts +180 -0
- package/server/service/figure/figure-public.ts +60 -0
- package/server/service/figure/figure-query.ts +48 -5
- package/server/service/figure/figure-release-check.test.ts +63 -0
- package/server/service/figure/figure-release-record-db.test.ts +7 -5
- package/server/service/figure/figure-rename-db.test.ts +50 -4
- package/server/service/figure/figure-rename.ts +37 -3
- package/server/service/figure/figure-tools.ts +1 -1
- package/server/service/figure/figure-type.ts +3 -0
- package/server/service/figure/figure.ts +8 -0
- package/server/service/figure/index.ts +2 -1
- package/translations/en.json +12 -3
- package/translations/es.json +12 -3
- package/translations/ja.json +12 -3
- package/translations/ko.json +12 -3
- package/translations/ms.json +12 -3
- package/translations/zh.json +12 -3
|
@@ -13,8 +13,9 @@ import { revertKeepingDraft, undoRevert } from './figure-revert.js'
|
|
|
13
13
|
import { recordProposals } from './figure-proposal.js'
|
|
14
14
|
import { FigureProposal, ProposeRequest } from './figure-propose-type.js'
|
|
15
15
|
import { inspectSource, releaseRecordOf } from './figure-format.js'
|
|
16
|
-
import { renameDraftType, renameFacts } from './figure-rename.js'
|
|
17
|
-
import { namespaceForNewFigure, namespaceOf } from './figure-namespace.js'
|
|
16
|
+
import { releasedBeforeMessage, renameDraftType, renameFacts, typeReleasedBefore } from './figure-rename.js'
|
|
17
|
+
import { assertReleasableNamespace, namespaceForNewFigure, namespaceOf } from './figure-namespace.js'
|
|
18
|
+
import { assertFigureCategory, assertReleasableCategory } from './figure-categories.js'
|
|
18
19
|
import { parseSource } from './figure-format.js'
|
|
19
20
|
import { ProposeFigureFailure, proposalFailureForPerson, proposeFigureSteps } from './figure-propose-steps.js'
|
|
20
21
|
import { prepareFigureWrite, compareAndSwapFigure, pictureOf } from './figure-write.js'
|
|
@@ -48,6 +49,19 @@ export function assertNames(names: unknown): asserts names is Record<string, str
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The figure a write may touch: one of this domain's own (LV figure-server design G3). A domain reads its parent's
|
|
54
|
+
* figures but does not change them -- updating, releasing or retiring one from a child domain is refused as not found,
|
|
55
|
+
* the same answer saving its draft has always given.
|
|
56
|
+
*/
|
|
57
|
+
async function ownFigure(repository: { findOne(options: any): Promise<Figure | null> }, domain: { id: string }, id: string): Promise<Figure> {
|
|
58
|
+
const figure = await repository.findOne({ where: { domain: { id: domain.id }, id } })
|
|
59
|
+
if (!figure) {
|
|
60
|
+
throw new Error(`Figure ${id} 를 찾을 수 없다`)
|
|
61
|
+
}
|
|
62
|
+
return figure
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
@Resolver(Figure)
|
|
52
66
|
@UseMiddleware(SpeakRefusals)
|
|
53
67
|
export class FigureMutation {
|
|
@@ -67,9 +81,11 @@ export class FigureMutation {
|
|
|
67
81
|
nullable: true,
|
|
68
82
|
description: "Keys of the AI proposals whose steps this source holds: recorded with the save, in its transaction."
|
|
69
83
|
})
|
|
70
|
-
proposalKeys?: string[]
|
|
84
|
+
proposalKeys?: string[],
|
|
85
|
+
@Arg('namespace', { nullable: true, description: "The domain's namespace a new figure is created under; its default when none. Ignored for an existing one." })
|
|
86
|
+
namespace?: string
|
|
71
87
|
): Promise<Figure> {
|
|
72
|
-
const saved = await this.writeSource(context, source, id, expectedSource, creationId, thumbnail, type)
|
|
88
|
+
const saved = await this.writeSource(context, source, id, expectedSource, creationId, thumbnail, type, namespace)
|
|
73
89
|
await recordProposals(context.state.tx, context.state.domain, saved.id!, proposalKeys)
|
|
74
90
|
return saved
|
|
75
91
|
}
|
|
@@ -81,7 +97,8 @@ export class FigureMutation {
|
|
|
81
97
|
expectedSource?: string,
|
|
82
98
|
creationId?: string,
|
|
83
99
|
thumbnail?: string,
|
|
84
|
-
type?: string
|
|
100
|
+
type?: string,
|
|
101
|
+
chosenNamespace?: string
|
|
85
102
|
): Promise<Figure> {
|
|
86
103
|
const { domain, user, tx } = context.state
|
|
87
104
|
const repository = tx.getRepository(Figure)
|
|
@@ -107,7 +124,7 @@ export class FigureMutation {
|
|
|
107
124
|
name or not at all -- a taken name refuses the save, rather than saving under the temporary one and failing a
|
|
108
125
|
rename after. Without a name (a caller that has none to give) it is the temporary one, renamed later.
|
|
109
126
|
*/
|
|
110
|
-
const namespace = await namespaceForNewFigure(tx, domain)
|
|
127
|
+
const namespace = await namespaceForNewFigure(tx, domain, chosenNamespace, user)
|
|
111
128
|
const named = type?.trim()
|
|
112
129
|
if (named) {
|
|
113
130
|
const facts = await renameFacts(
|
|
@@ -179,7 +196,8 @@ export class FigureMutation {
|
|
|
179
196
|
'{type} is not a figure type. use lower-case letters, digits and single hyphens, {min} to {max} characters, starting with a letter.'
|
|
180
197
|
)
|
|
181
198
|
}
|
|
182
|
-
|
|
199
|
+
assertFigureCategory(figure.category)
|
|
200
|
+
const namespace = await namespaceForNewFigure(tx, domain, figure.namespace, user)
|
|
183
201
|
|
|
184
202
|
// 타입 이름은 도메인 안에서 유일하다. 유니크 인덱스가 있지만 여기서 먼저 본다 —
|
|
185
203
|
// DB 오류 문구는 저작자에게 아무 말도 해 주지 못한다.
|
|
@@ -187,6 +205,9 @@ export class FigureMutation {
|
|
|
187
205
|
if (taken) {
|
|
188
206
|
throw new Error(`타입 이름 '${figure.type}' 은 이미 쓰이고 있다. 만든 뒤에는 고칠 수 없으므로 다른 이름을 고른다`)
|
|
189
207
|
}
|
|
208
|
+
if (await typeReleasedBefore({ figures: repository, histories: tx.getRepository(FigureHistory) }, domain, figure.type!)) {
|
|
209
|
+
throw new Error(releasedBeforeMessage(figure.type!))
|
|
210
|
+
}
|
|
190
211
|
|
|
191
212
|
// 형식을 어기면 여기서 던진다. 파생값은 서버가 계산한다.
|
|
192
213
|
const { source, derived, violations } = parseSource(figure.source)
|
|
@@ -219,12 +240,7 @@ export class FigureMutation {
|
|
|
219
240
|
const { domain, user, tx } = context.state
|
|
220
241
|
const repository = tx.getRepository(Figure)
|
|
221
242
|
|
|
222
|
-
const figure = await repository
|
|
223
|
-
where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }
|
|
224
|
-
})
|
|
225
|
-
if (!figure) {
|
|
226
|
-
throw new Error(`Figure ${id} 를 찾을 수 없다`)
|
|
227
|
-
}
|
|
243
|
+
const figure = await ownFigure(repository, domain, id)
|
|
228
244
|
|
|
229
245
|
/*
|
|
230
246
|
**그림만 바꾸면 발행이 풀리지 않는다.** 미리보기에서 사람이 돌려 놓은 각도로 찍은 그림이 그렇게 온다.
|
|
@@ -264,6 +280,8 @@ export class FigureMutation {
|
|
|
264
280
|
행이 다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다.
|
|
265
281
|
*/
|
|
266
282
|
if (patch.names !== undefined) assertNames(patch.names)
|
|
283
|
+
/* An app's category vocabulary (G5): a category outside it is refused, and one once set does not change. */
|
|
284
|
+
assertFigureCategory(patch.category, figure.category)
|
|
267
285
|
/* The picture is kept by its content; the patch's data URL is never written to the row. */
|
|
268
286
|
const { thumbnail: picturePatch, ...patchRest } = patch
|
|
269
287
|
const picture = await pictureOf(tx, domain, user, picturePatch, figure)
|
|
@@ -360,7 +378,8 @@ export class FigureMutation {
|
|
|
360
378
|
* `inspectFigure` 로 미리 물어 이유를 보여 준다.
|
|
361
379
|
*/
|
|
362
380
|
@Directive('@transaction')
|
|
363
|
-
|
|
381
|
+
/* Releasing, retiring and making public are their own gate (LV figure-server design G6): an author who edits drafts need not be one who sends releases out. */
|
|
382
|
+
@Directive('@privilege(category: "figure-release", privilege: "mutation", domainOwnerGranted: true)')
|
|
364
383
|
@Mutation(returns => Figure, { description: 'Releases a figure — raises its version and keeps the released state as history.' })
|
|
365
384
|
async releaseFigure(
|
|
366
385
|
@Arg('id') id: string,
|
|
@@ -371,27 +390,16 @@ export class FigureMutation {
|
|
|
371
390
|
const repository = tx.getRepository(Figure)
|
|
372
391
|
const historyRepository = tx.getRepository(FigureHistory)
|
|
373
392
|
|
|
374
|
-
const figure = await repository
|
|
375
|
-
where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }
|
|
376
|
-
})
|
|
377
|
-
if (!figure) {
|
|
378
|
-
throw new Error(`Figure ${id} 를 찾을 수 없다`)
|
|
379
|
-
}
|
|
393
|
+
const figure = await ownFigure(repository, domain, id)
|
|
380
394
|
|
|
381
395
|
if (figure.state === 'released') {
|
|
382
396
|
throw new Error(`Figure '${figure.type}' 은 이미 발행되어 있다. 고쳐서 다시 내려면 초안으로 되돌린 뒤 발행한다`)
|
|
383
397
|
}
|
|
384
398
|
|
|
385
|
-
/* A release promises boards the full name {namespace}/{type}
|
|
399
|
+
/* A release promises boards the full name {namespace}/{type}: it goes out only under a namespace the domain owns and has active. */
|
|
386
400
|
const namespace = namespaceOf(figure)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
throw new Refusal(
|
|
390
|
-
'figure-namespace-changed',
|
|
391
|
-
{ setting: 'figure-namespace', value: current, namespace },
|
|
392
|
-
'the figure namespace of this domain is {namespace} and cannot change. set {setting} back to {namespace}.'
|
|
393
|
-
)
|
|
394
|
-
}
|
|
401
|
+
await assertReleasableNamespace(tx, domain, namespace, user)
|
|
402
|
+
assertReleasableCategory(figure.category, figure.type)
|
|
395
403
|
|
|
396
404
|
/*
|
|
397
405
|
── 게이트는 여기 있어야 한다 ────────────────────────────────────────────
|
|
@@ -412,7 +420,7 @@ export class FigureMutation {
|
|
|
412
420
|
.limit(1)
|
|
413
421
|
.getOne()
|
|
414
422
|
|
|
415
|
-
const inspection = inspectSource(figure.source, released?.source, context.t, figure.type)
|
|
423
|
+
const inspection = inspectSource(figure.source, released?.source, context.t, figure.type, figure.category)
|
|
416
424
|
if (inspection.blocked) {
|
|
417
425
|
const reasons = inspection.findings
|
|
418
426
|
.filter(finding => finding.blocking)
|
|
@@ -503,12 +511,24 @@ export class FigureMutation {
|
|
|
503
511
|
|
|
504
512
|
@Directive('@transaction')
|
|
505
513
|
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
506
|
-
@Mutation(returns => Boolean, { description: 'Soft-deletes a figure.' })
|
|
514
|
+
@Mutation(returns => Boolean, { description: 'Soft-deletes a figure that has never been released. A released figure is refused: it is retired instead.' })
|
|
507
515
|
async deleteFigure(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
508
516
|
const { domain, tx } = context.state
|
|
509
517
|
|
|
510
|
-
|
|
511
|
-
|
|
518
|
+
/*
|
|
519
|
+
A figure once released is not deleted, only retired (LV figure-server design G4, fmsim design 01 D5). Its releases
|
|
520
|
+
have gone out in packages as `ns/type@n`; deleting it would let the same type be made again and numbered from 1,
|
|
521
|
+
and the same reference would then name other content, which an importing server refuses.
|
|
522
|
+
*/
|
|
523
|
+
const figure = await tx.getRepository(Figure).findOne({ where: { domain: { id: domain.id }, id } })
|
|
524
|
+
if (figure) {
|
|
525
|
+
const releases = await tx.getRepository(FigureHistory).count({ where: { originalId: figure.id } })
|
|
526
|
+
if (releases > 0 || Number(figure.version) > 0) {
|
|
527
|
+
throw new Refusal('figure-released-not-deletable', { type: figure.type }, '{type} has been released and cannot be deleted.')
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 소프트 삭제다 — 행을 지우지 않고 되살릴 길을 남긴다.
|
|
512
532
|
await tx.getRepository(Figure).softDelete({ domain: { id: domain.id }, id })
|
|
513
533
|
|
|
514
534
|
return true
|
|
@@ -519,10 +539,10 @@ export class FigureMutation {
|
|
|
519
539
|
*
|
|
520
540
|
* 표시일 뿐이다. 발행판은 그대로 남고 받는 쪽은 그대로 받는다 — 발행판은 지우지 않는다. 그래서 **발행을 풀지 않는다**:
|
|
521
541
|
* 행을 엔티티로 저장하지 않고 질의로 고친다. 엔티티로 저장하면 발행된 행이라 판본 구독자가 같은 번호로 판본을 또
|
|
522
|
-
* 뜬다. 대신 쓸 것(`supersededBy`)은 같은 도메인(또는 상위)에 있는 다른 type 이어야 한다.
|
|
542
|
+
* 뜬다. 대신 쓸 것(`supersededBy`)은 같은 도메인(또는 상위)에 있는 다른 type 이어야 한다. 퇴역은 소유 도메인의 figure 만 한다.
|
|
523
543
|
*/
|
|
524
544
|
@Directive('@transaction')
|
|
525
|
-
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
545
|
+
@Directive('@privilege(category: "figure-release", privilege: "mutation", domainOwnerGranted: true)')
|
|
526
546
|
@Mutation(returns => Figure, { description: 'Retires a figure or brings it back. It stays released and served; supersededBy names the type to use instead.' })
|
|
527
547
|
async setFigureDeprecation(
|
|
528
548
|
@Arg('id') id: string,
|
|
@@ -532,9 +552,9 @@ export class FigureMutation {
|
|
|
532
552
|
): Promise<Figure> {
|
|
533
553
|
const { domain, tx } = context.state
|
|
534
554
|
const repository = tx.getRepository(Figure)
|
|
555
|
+
const figure = await ownFigure(repository, domain, id)
|
|
556
|
+
/* What to use instead may be the parent domain's: a domain reads its parent's figures, it writes only its own. */
|
|
535
557
|
const scope = { id: In([domain.id, domain.parentId].filter(Boolean)) }
|
|
536
|
-
const figure = await repository.findOne({ where: { domain: scope, id } })
|
|
537
|
-
if (!figure) throw new Error(`Figure ${id} 를 찾을 수 없다`)
|
|
538
558
|
const instead = deprecated ? supersededBy?.trim() || null : null
|
|
539
559
|
if (instead) {
|
|
540
560
|
if (instead === figure.type) throw new Error(`Figure '${figure.type}' 는 자기 자신으로 대신할 수 없다`)
|
|
@@ -545,6 +565,28 @@ export class FigureMutation {
|
|
|
545
565
|
return await repository.findOneOrFail({ where: { id } })
|
|
546
566
|
}
|
|
547
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Makes a released figure public, or private again (LV figure-server design G2). Public, other domains read its
|
|
570
|
+
* releases, package them and draw it by full name; its draft stays this domain's, and only this domain changes it.
|
|
571
|
+
* Like retiring, it is a mark on the row: no release is written and the release state is not undone.
|
|
572
|
+
*/
|
|
573
|
+
@Directive('@transaction')
|
|
574
|
+
@Directive('@privilege(category: "figure-release", privilege: "mutation", domainOwnerGranted: true)')
|
|
575
|
+
@Mutation(returns => Figure, { description: 'Makes a released figure public to other domains, or private again. Its draft is never shown to them.' })
|
|
576
|
+
async setFigurePublic(@Arg('id') id: string, @Arg('public') isPublic: boolean, @Ctx() context: ResolverContext): Promise<Figure> {
|
|
577
|
+
const { domain, tx } = context.state
|
|
578
|
+
const repository = tx.getRepository(Figure)
|
|
579
|
+
const figure = await ownFigure(repository, domain, id)
|
|
580
|
+
if (isPublic) {
|
|
581
|
+
const releases = await tx.getRepository(FigureHistory).count({ where: { originalId: figure.id } })
|
|
582
|
+
if (!releases && !(Number(figure.version) > 0)) {
|
|
583
|
+
throw new Refusal('figure-public-unreleased', { type: figure.type }, '{type} has no release yet: only released figures are made public.')
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
await repository.createQueryBuilder().update(Figure).set({ public: isPublic }).where('id = :id', { id }).callListeners(false).execute()
|
|
587
|
+
return await repository.findOneOrFail({ where: { id } })
|
|
588
|
+
}
|
|
589
|
+
|
|
548
590
|
/**
|
|
549
591
|
* 저작 보조에게 제안을 시킨다(ADR-0094).
|
|
550
592
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Full figure names on the authoring server (ADR-0114): the
|
|
2
|
+
* Full figure names on the authoring server (ADR-0114, LV figure-server design G1): the namespaces a domain owns and the one a figure is created under, the batch door
|
|
3
3
|
* `figureBlueprints`, and the package a board server imports. sqlite, history subscriber on.
|
|
4
4
|
*/
|
|
5
5
|
import type { DataSource } from 'typeorm'
|
|
@@ -18,6 +18,7 @@ let FigureMutation: any
|
|
|
18
18
|
let FigureQuery: any
|
|
19
19
|
let figurePackageOf: any
|
|
20
20
|
let FigureHistory: any
|
|
21
|
+
let FigureNamespace: any
|
|
21
22
|
|
|
22
23
|
function loadCompiled(modulePath: string): any {
|
|
23
24
|
const { existsSync } = require('fs')
|
|
@@ -31,6 +32,7 @@ function loadCompiled(modulePath: string): any {
|
|
|
31
32
|
beforeAll(async () => {
|
|
32
33
|
Figure = loadCompiled('service/figure/figure').Figure
|
|
33
34
|
FigureHistory = loadCompiled('service/figure/figure-history').FigureHistory
|
|
35
|
+
FigureNamespace = loadCompiled('service/figure/figure-namespace-entity').FigureNamespace
|
|
34
36
|
const Subscriber = loadCompiled('service/figure/event-subscriber').FigureHistoryEntitySubscriber
|
|
35
37
|
;({ FigureMutation } = loadCompiled('service/figure/figure-mutation'))
|
|
36
38
|
;({ FigureQuery } = loadCompiled('service/figure/figure-query'))
|
|
@@ -41,7 +43,7 @@ beforeAll(async () => {
|
|
|
41
43
|
synchronize: true,
|
|
42
44
|
dropSchema: true,
|
|
43
45
|
namingStrategy: new shell.NamingStrategy(),
|
|
44
|
-
entities: [...shell.entities, ...authBase.entities, ...settingBase.entities, Figure, FigureHistory],
|
|
46
|
+
entities: [...shell.entities, ...authBase.entities, ...settingBase.entities, Figure, FigureHistory, FigureNamespace],
|
|
45
47
|
subscribers: [Subscriber],
|
|
46
48
|
logging: false
|
|
47
49
|
})
|
|
@@ -62,119 +64,160 @@ const releasable = (label = 'Conveyor') => {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
let seq = 0
|
|
67
|
+
/** A domain owning one namespace, its default (or none). Namespaces are unique on the server, so each test names its own. */
|
|
65
68
|
async function domainWith(namespace: string | null) {
|
|
66
69
|
const name = `ns-${++seq}`
|
|
67
70
|
const domain = await dataSource!.getRepository(shell.Domain).save({ name, subdomain: name })
|
|
68
71
|
if (namespace !== null) {
|
|
69
|
-
await dataSource!.getRepository(
|
|
72
|
+
await dataSource!.getRepository(FigureNamespace).save({ domain, name: namespace, state: 'active', isDefault: true })
|
|
70
73
|
}
|
|
71
74
|
const context = { state: { domain, user: undefined, tx: dataSource!.manager } }
|
|
72
|
-
return { domain, context }
|
|
75
|
+
return { domain, context, namespace: namespace ?? '' }
|
|
73
76
|
}
|
|
77
|
+
const unique = (stem: string) => `${stem}-${++seq}.hatiolab.com`
|
|
74
78
|
|
|
75
79
|
const creationId = () => require('crypto').randomUUID()
|
|
76
|
-
const
|
|
77
|
-
|
|
80
|
+
const save = (context: any, type: string, namespace?: string) =>
|
|
81
|
+
new FigureMutation().saveFigure(releasable(type), context, undefined, undefined, creationId(), undefined, type, undefined, namespace)
|
|
78
82
|
|
|
79
83
|
async function releasedFigure(context: any, type: string) {
|
|
80
|
-
const figure = await
|
|
84
|
+
const figure = await save(context, type)
|
|
81
85
|
await new FigureMutation().releaseFigure(figure.id, 'first', context)
|
|
82
86
|
return figure
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
const resolver = () => new (loadCompiled('service/figure/figure-namespace-resolver').FigureNamespaceResolver)()
|
|
90
|
+
|
|
85
91
|
describe('the namespace a figure is created under', () => {
|
|
86
|
-
it(
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
expect(figure.type).
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('with no setting, or an empty one, is the subdomain', async () => {
|
|
94
|
-
for (const value of [null, '', ' ']) {
|
|
95
|
-
const { domain, context } = await domainWith(value)
|
|
96
|
-
const figure = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
|
|
97
|
-
expect(figure.namespace).toBe(domain.subdomain)
|
|
98
|
-
}
|
|
92
|
+
it("is the domain's default", async () => {
|
|
93
|
+
const ns = unique('default')
|
|
94
|
+
const { context } = await domainWith(ns)
|
|
95
|
+
const figure = await save(context, 'conveyor')
|
|
96
|
+
expect([figure.namespace, figure.type]).toEqual([ns, 'conveyor'])
|
|
99
97
|
})
|
|
100
98
|
|
|
101
|
-
it('
|
|
102
|
-
const { context } = await domainWith(
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
).
|
|
99
|
+
it('in a domain owning none, the first figure makes the subdomain its default namespace', async () => {
|
|
100
|
+
const { domain, context } = await domainWith(null)
|
|
101
|
+
const figure = await save(context, 'conveyor')
|
|
102
|
+
expect(figure.namespace).toBe(domain.subdomain)
|
|
103
|
+
expect((await resolver().figureNamespaces(context)).map((n: any) => [n.name, n.isDefault, n.state])).toEqual([[domain.subdomain, true, 'active']])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('is refused by name when the domain owns none and its subdomain is not a namespace', async () => {
|
|
106
107
|
const odd = await dataSource!.getRepository(shell.Domain).save({ name: `odd-${++seq}`, subdomain: 'Odd_Sub' })
|
|
107
|
-
await expect(
|
|
108
|
-
new FigureMutation().saveFigure(releasable(), { state: { domain: odd, user: undefined, tx: dataSource!.manager } }, undefined, undefined, creationId(), undefined, 'conveyor')
|
|
109
|
-
).rejects.toMatchObject({ code: 'figure-namespace-missing' })
|
|
108
|
+
await expect(save({ state: { domain: odd, user: undefined, tx: dataSource!.manager } }, 'conveyor')).rejects.toMatchObject({ code: 'figure-namespace-missing' })
|
|
110
109
|
})
|
|
111
110
|
|
|
112
|
-
it('
|
|
113
|
-
const {
|
|
114
|
-
const
|
|
115
|
-
await
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
it('may be another namespace the domain owns, named when it is created', async () => {
|
|
112
|
+
const { context } = await domainWith(unique('first'))
|
|
113
|
+
const second = unique('second')
|
|
114
|
+
await resolver().createFigureNamespace(second, context)
|
|
115
|
+
expect((await save(context, 'lifter', second)).namespace).toBe(second)
|
|
116
|
+
const created = await new FigureMutation().createFigure({ type: 'shuttle', name: 's', source: releasable(), namespace: second }, context)
|
|
117
|
+
expect(created.figure.namespace).toBe(second)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("is refused when named and not the domain's own, or suspended", async () => {
|
|
121
|
+
const { context } = await domainWith(unique('mine'))
|
|
122
|
+
const theirs = unique('theirs')
|
|
123
|
+
await domainWith(theirs)
|
|
124
|
+
await expect(save(context, 'conveyor', theirs)).rejects.toMatchObject({ code: 'figure-namespace-not-owned' })
|
|
125
|
+
const resting = unique('resting')
|
|
126
|
+
await resolver().createFigureNamespace(resting, context)
|
|
127
|
+
await resolver().setFigureNamespaceState(resting, 'suspended', context)
|
|
128
|
+
await expect(save(context, 'conveyor', resting)).rejects.toMatchObject({ code: 'figure-namespace-suspended' })
|
|
129
|
+
})
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
it('a release goes out only under an active namespace of the domain', async () => {
|
|
132
|
+
const { context } = await domainWith(unique('main'))
|
|
133
|
+
const side = unique('side')
|
|
134
|
+
await resolver().createFigureNamespace(side, context)
|
|
135
|
+
const draft = await save(context, 'conveyor', side)
|
|
136
|
+
await resolver().setFigureNamespaceState(side, 'suspended', context)
|
|
137
|
+
await expect(new FigureMutation().releaseFigure(draft.id, 'c', context)).rejects.toMatchObject({ code: 'figure-namespace-suspended' })
|
|
138
|
+
await resolver().setFigureNamespaceState(side, 'active', context)
|
|
122
139
|
await expect(new FigureMutation().releaseFigure(draft.id, 'c', context)).resolves.toMatchObject({ version: 1 })
|
|
123
140
|
})
|
|
124
141
|
|
|
125
142
|
it('refuses an upper-case type when creating directly', async () => {
|
|
126
|
-
const { context } = await domainWith('
|
|
143
|
+
const { context } = await domainWith(unique('upper'))
|
|
127
144
|
await expect(
|
|
128
145
|
new FigureMutation().createFigure({ type: 'CONVEYOR', name: 'c', source: releasable() }, context)
|
|
129
146
|
).rejects.toMatchObject({ code: 'figure-type-malformed' })
|
|
130
147
|
})
|
|
131
148
|
})
|
|
132
149
|
|
|
133
|
-
describe('the
|
|
134
|
-
|
|
150
|
+
describe('the namespaces a domain owns', () => {
|
|
151
|
+
it('adds one: not a namespace, or another domain\'s, is refused; the first is the default and the list starts with it', async () => {
|
|
152
|
+
const { context } = await domainWith(null)
|
|
153
|
+
await expect(resolver().createFigureNamespace('Hatiolab.COM', context)).rejects.toMatchObject({ code: 'figure-namespace-invalid' })
|
|
154
|
+
const taken = unique('taken')
|
|
155
|
+
await domainWith(taken)
|
|
156
|
+
await expect(resolver().createFigureNamespace(taken, context)).rejects.toMatchObject({ code: 'figure-namespace-taken' })
|
|
157
|
+
const a = unique('a')
|
|
158
|
+
const b = unique('b')
|
|
159
|
+
expect(await resolver().createFigureNamespace(` ${a} `, context, 'first one')).toMatchObject({ name: a, isDefault: true, state: 'active', description: 'first one' })
|
|
160
|
+
expect(await resolver().createFigureNamespace(b, context)).toMatchObject({ name: b, isDefault: false })
|
|
161
|
+
expect((await resolver().figureNamespaces(context)).map((n: any) => n.name)).toEqual([a, b])
|
|
162
|
+
})
|
|
135
163
|
|
|
136
|
-
it('
|
|
137
|
-
const
|
|
138
|
-
|
|
164
|
+
it('moves the default when a new one asks for it, or when another is made the default', async () => {
|
|
165
|
+
const a = unique('a')
|
|
166
|
+
const { context } = await domainWith(a)
|
|
167
|
+
const b = unique('b')
|
|
168
|
+
await resolver().createFigureNamespace(b, context, undefined, true)
|
|
169
|
+
expect((await resolver().figureNamespaces(context)).map((n: any) => [n.name, n.isDefault])).toEqual([[b, true], [a, false]])
|
|
170
|
+
await resolver().setDefaultFigureNamespace(a, context)
|
|
171
|
+
expect((await resolver().figureNamespaces(context)).map((n: any) => [n.name, n.isDefault])).toEqual([[a, true], [b, false]])
|
|
172
|
+
expect((await save(context, 'conveyor')).namespace).toBe(a)
|
|
139
173
|
})
|
|
140
174
|
|
|
141
|
-
it('
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await resolver().
|
|
146
|
-
|
|
147
|
-
expect(
|
|
148
|
-
expect(await resolver().
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await expect(resolver().
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
it('suspends and activates one; the default is not suspended, nor made default while suspended; another domain\'s is not touched', async () => {
|
|
176
|
+
const a = unique('a')
|
|
177
|
+
const { context } = await domainWith(a)
|
|
178
|
+
const b = unique('b')
|
|
179
|
+
await resolver().createFigureNamespace(b, context)
|
|
180
|
+
await expect(resolver().setFigureNamespaceState(a, 'suspended', context)).rejects.toMatchObject({ code: 'figure-namespace-default-suspended' })
|
|
181
|
+
await expect(resolver().setFigureNamespaceState(b, 'resting', context)).rejects.toMatchObject({ code: 'figure-namespace-state' })
|
|
182
|
+
expect(await resolver().setFigureNamespaceState(b, 'suspended', context)).toMatchObject({ name: b, state: 'suspended' })
|
|
183
|
+
await expect(resolver().setDefaultFigureNamespace(b, context)).rejects.toMatchObject({ code: 'figure-namespace-suspended' })
|
|
184
|
+
const theirs = unique('theirs')
|
|
185
|
+
await domainWith(theirs)
|
|
186
|
+
await expect(resolver().setFigureNamespaceState(theirs, 'suspended', context)).rejects.toMatchObject({ code: 'figure-namespace-not-owned' })
|
|
187
|
+
await expect(resolver().setDefaultFigureNamespace(theirs, context)).rejects.toMatchObject({ code: 'figure-namespace-not-owned' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('reading them is authoring; changing them is figure-namespace:mutation', () => {
|
|
191
|
+
const { readFileSync } = require('fs')
|
|
192
|
+
const { join } = require('path')
|
|
193
|
+
const source = readFileSync(join(__dirname, 'figure-namespace-resolver.ts'), 'utf-8')
|
|
194
|
+
const declarationOf = (method: string) => {
|
|
195
|
+
const preceding = source.slice(0, source.indexOf(`async ${method}(`))
|
|
196
|
+
return preceding.slice(preceding.lastIndexOf("@Directive('@privilege("))
|
|
197
|
+
}
|
|
198
|
+
expect(declarationOf('figureNamespaces')).toContain('category: "figure", privilege: "query"')
|
|
199
|
+
for (const method of ['createFigureNamespace', 'setDefaultFigureNamespace', 'setFigureNamespaceState'])
|
|
200
|
+
expect(declarationOf(method)).toContain('category: "figure-namespace", privilege: "mutation"')
|
|
159
201
|
})
|
|
160
202
|
})
|
|
161
203
|
|
|
162
204
|
describe('figureBlueprints answers each full name, an item or a refusal', () => {
|
|
163
205
|
it('gives released figures by full name and refuses the rest by cause, none taking another away', async () => {
|
|
164
|
-
const
|
|
206
|
+
const ns = unique('blue')
|
|
207
|
+
const { context } = await domainWith(ns)
|
|
165
208
|
await releasedFigure(context, 'conveyor')
|
|
166
|
-
await
|
|
209
|
+
await save(context, 'draft-only')
|
|
167
210
|
|
|
168
211
|
const answer = await new FigureQuery().figureBlueprints(
|
|
169
|
-
[
|
|
212
|
+
[`${ns}/conveyor`, `${ns}/draft-only`, 'other.com/conveyor', 'CONVEYOR', `${ns}/conveyor`],
|
|
170
213
|
context
|
|
171
214
|
)
|
|
172
215
|
|
|
173
216
|
expect(Object.keys(answer).sort()).toEqual(['items', 'refused'])
|
|
174
|
-
expect(answer.items.map((item: any) => [item.type, item.ref])).toEqual([[
|
|
217
|
+
expect(answer.items.map((item: any) => [item.type, item.ref])).toEqual([[`${ns}/conveyor`, `${ns}/conveyor@1`]])
|
|
175
218
|
expect(answer.items[0].asset.version).toBe('figure-asset-1')
|
|
176
219
|
expect(answer.refused.map((r: any) => [r.type, r.code])).toEqual([
|
|
177
|
-
[
|
|
220
|
+
[`${ns}/draft-only`, 'figure-not-released'],
|
|
178
221
|
['other.com/conveyor', 'figure-not-released'],
|
|
179
222
|
['CONVEYOR', 'figure-name-malformed']
|
|
180
223
|
])
|
|
@@ -185,17 +228,18 @@ describe('the figure package', () => {
|
|
|
185
228
|
const sha256 = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex')
|
|
186
229
|
|
|
187
230
|
it('carries the release a board server reads back whole, and names the server it came from', async () => {
|
|
188
|
-
const
|
|
231
|
+
const ns = unique('pkg')
|
|
232
|
+
const { domain, context } = await domainWith(ns)
|
|
189
233
|
const figure = await releasedFigure(context, 'trailer-22')
|
|
190
234
|
|
|
191
235
|
const { name, bytes } = await figurePackageOf({
|
|
192
|
-
refs: [
|
|
236
|
+
refs: [`${ns}/trailer-22@1`],
|
|
193
237
|
domain,
|
|
194
238
|
exportedBy: 'author',
|
|
195
239
|
sourceUrl: 'https://figure.hatiolab.com',
|
|
196
240
|
files: { put: async () => ({}), send: async () => {}, read: async () => Buffer.alloc(0) }
|
|
197
241
|
})
|
|
198
|
-
expect(name).toBe(
|
|
242
|
+
expect(name).toBe(`${ns}_trailer-22_1.figpkg`)
|
|
199
243
|
|
|
200
244
|
const zip = await JSZip.loadAsync(bytes)
|
|
201
245
|
const files = new Map<string, Uint8Array>()
|
|
@@ -205,13 +249,14 @@ describe('the figure package', () => {
|
|
|
205
249
|
expect(read.problems).toEqual([])
|
|
206
250
|
expect(read.refused).toEqual([])
|
|
207
251
|
expect(read.manifest.source).toEqual({ url: 'https://figure.hatiolab.com', domain: domain.subdomain })
|
|
208
|
-
expect(read.releases.map((r: any) => r.ref)).toEqual([
|
|
252
|
+
expect(read.releases.map((r: any) => r.ref)).toEqual([`${ns}/trailer-22@1`])
|
|
209
253
|
const stored = await dataSource!.getRepository(Figure).findOneOrFail({ where: { id: figure.id } })
|
|
210
254
|
expect(read.releases[0].source).toBe(stored.source)
|
|
211
255
|
})
|
|
212
256
|
|
|
213
257
|
it('dates each release when it was released, not when the figure was made, and carries its note', async () => {
|
|
214
|
-
const
|
|
258
|
+
const ns = unique('pkg')
|
|
259
|
+
const { domain, context } = await domainWith(ns)
|
|
215
260
|
const figure = await releasedFigure(context, 'trailer-23')
|
|
216
261
|
/* sqlite keeps these times to the second */
|
|
217
262
|
await new Promise(resolve => setTimeout(resolve, 1100))
|
|
@@ -220,7 +265,7 @@ describe('the figure package', () => {
|
|
|
220
265
|
await new FigureMutation().releaseFigure(figure.id, 'wider body', context)
|
|
221
266
|
|
|
222
267
|
const { bytes } = await figurePackageOf({
|
|
223
|
-
refs: [
|
|
268
|
+
refs: [`${ns}/trailer-23@1`, `${ns}/trailer-23@2`],
|
|
224
269
|
domain,
|
|
225
270
|
exportedBy: 'author',
|
|
226
271
|
sourceUrl: 'x',
|
|
@@ -239,11 +284,32 @@ describe('the figure package', () => {
|
|
|
239
284
|
})
|
|
240
285
|
|
|
241
286
|
it('refuses a release this domain cannot read, and a malformed reference', async () => {
|
|
242
|
-
const
|
|
287
|
+
const ns = unique('pkg')
|
|
288
|
+
const { domain, context } = await domainWith(ns)
|
|
243
289
|
await releasedFigure(context, 'conveyor')
|
|
244
290
|
const ask = (refs: string[]) => figurePackageOf({ refs, domain, exportedBy: null, sourceUrl: 'x' })
|
|
245
|
-
await expect(ask([
|
|
291
|
+
await expect(ask([`${ns}/conveyor@2`])).rejects.toMatchObject({ code: 'figure-package-release-not-found' })
|
|
246
292
|
await expect(ask(['other.com/conveyor@1'])).rejects.toMatchObject({ code: 'figure-package-release-not-found' })
|
|
247
293
|
await expect(ask(['CONVEYOR@1'])).rejects.toMatchObject({ code: 'figure-ref-malformed' })
|
|
248
294
|
})
|
|
249
295
|
})
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
* A domain whose figures were made before namespaces were rows (the setting days): it owns no namespace row, and its
|
|
299
|
+
* figures carry its subdomain. Releasing one is the first need of a namespace, so the subdomain becomes its first, as
|
|
300
|
+
* creating a figure does -- seen on :3300 after 10.1.179, where the system domain owned none.
|
|
301
|
+
*/
|
|
302
|
+
describe('a domain that owns no namespace row yet', () => {
|
|
303
|
+
it('releases a figure carrying its subdomain, and the subdomain becomes its default', async () => {
|
|
304
|
+
const { domain, context } = await domainWith(null)
|
|
305
|
+
const figure = await dataSource!.getRepository(Figure).save({ namespace: domain.subdomain, type: 'older', name: 'older', source: releasable(), state: 'draft', version: 0, domain })
|
|
306
|
+
await expect(new FigureMutation().releaseFigure(figure.id, 'first', context)).resolves.toMatchObject({ version: 1 })
|
|
307
|
+
expect((await resolver().figureNamespaces(context)).map((n: any) => [n.name, n.isDefault])).toEqual([[domain.subdomain, true]])
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('a figure carrying another namespace than its subdomain is still refused by name', async () => {
|
|
311
|
+
const { domain, context } = await domainWith(null)
|
|
312
|
+
const figure = await dataSource!.getRepository(Figure).save({ namespace: unique('elsewhere'), type: 'stray', name: 'stray', source: releasable(), state: 'draft', version: 0, domain })
|
|
313
|
+
await expect(new FigureMutation().releaseFigure(figure.id, 'first', context)).rejects.toMatchObject({ code: 'figure-namespace-not-owned' })
|
|
314
|
+
})
|
|
315
|
+
})
|