@things-factory/figure-service 10.1.68 → 10.1.69
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.js +14 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/figure-recipe/figure-recipe-resolver.d.ts +5 -0
- package/dist-server/service/figure-recipe/figure-recipe-resolver.js +41 -0
- package/dist-server/service/figure-recipe/figure-recipe-resolver.js.map +1 -0
- package/dist-server/service/figure-recipe/figure-recipe-store.d.ts +30 -0
- package/dist-server/service/figure-recipe/figure-recipe-store.js +79 -0
- package/dist-server/service/figure-recipe/figure-recipe-store.js.map +1 -0
- package/dist-server/service/figure-recipe/figure-recipe.d.ts +19 -0
- package/dist-server/service/figure-recipe/figure-recipe.js +67 -0
- package/dist-server/service/figure-recipe/figure-recipe.js.map +1 -0
- package/dist-server/service/figure-recipe/index.d.ts +4 -0
- package/dist-server/service/figure-recipe/index.js +8 -0
- package/dist-server/service/figure-recipe/index.js.map +1 -0
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +10 -3
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/index.ts +14 -0
- package/server/service/figure-recipe/figure-recipe-db.test.ts +103 -0
- package/server/service/figure-recipe/figure-recipe-resolver.ts +26 -0
- package/server/service/figure-recipe/figure-recipe-store.ts +75 -0
- package/server/service/figure-recipe/figure-recipe.ts +51 -0
- package/server/service/figure-recipe/index.ts +5 -0
- package/server/service/index.ts +5 -2
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.69",
|
|
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.82",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
28
|
"@things-factory/ai-client-base": "^10.1.53",
|
|
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": "
|
|
36
|
+
"gitHead": "7f3c2321cb348a3980d79a57e9f9316dffc31937"
|
|
37
37
|
}
|
package/server/index.ts
CHANGED
|
@@ -10,6 +10,20 @@ import './routes.js'
|
|
|
10
10
|
import { registerFigureToolCategories } from './service/figure/figure-tools.js'
|
|
11
11
|
registerFigureToolCategories()
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* The built-in recipes into the system domain, as auth-base fills its privileges: once, and again gives the same result
|
|
15
|
+
* (ADR-0095 condition 2). A recipe that fails its check is said here, not half put in.
|
|
16
|
+
*/
|
|
17
|
+
import { seedBuiltInRecipes } from './service/figure-recipe/figure-recipe-store.js'
|
|
18
|
+
process.on('bootstrap-module-start' as any, async () => {
|
|
19
|
+
try {
|
|
20
|
+
const { added, skipped, refused } = await seedBuiltInRecipes()
|
|
21
|
+
console.log(`[figure-service:bootstrap] recipes added ${added.length}, already there ${skipped.length}${refused.length ? `, refused: ${refused.join(' | ')}` : ''}`)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[figure-service:bootstrap] recipes were not seeded:', error)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
13
27
|
export * from './service/index.js'
|
|
14
28
|
export * from './migrations/index.js'
|
|
15
29
|
export { registerFigureToolCategories } from './service/figure/figure-tools.js'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 조각 — 시드 · 도메인 규칙 · 저장 검사를 **sqlite 를 실제로 들고** 확인한다(ADR-0095 조건 2 · 3).
|
|
3
|
+
*
|
|
4
|
+
* 규칙이 질의에 있으므로 DB 없이 시험하면 초록이어도 뜻이 없다. figure-rename-db 와 같이 tsc 산출물을 든다.
|
|
5
|
+
*/
|
|
6
|
+
import type { DataSource } from 'typeorm'
|
|
7
|
+
|
|
8
|
+
const { DataSource: TypeOrmDataSource } = require('typeorm')
|
|
9
|
+
const shell = require('@things-factory/shell')
|
|
10
|
+
const authBase = require('@things-factory/auth-base')
|
|
11
|
+
|
|
12
|
+
let dataSource: DataSource | null = null
|
|
13
|
+
let FigureRecipe: any
|
|
14
|
+
let store: any
|
|
15
|
+
|
|
16
|
+
function loadCompiled(modulePath: string): any {
|
|
17
|
+
const { existsSync } = require('fs')
|
|
18
|
+
const { join } = require('path')
|
|
19
|
+
const resolved = join(__dirname, '..', '..', '..', 'dist-server', `${modulePath}.js`)
|
|
20
|
+
if (!existsSync(resolved)) throw new Error(`figure-service 산출물이 없다: ${resolved} — 먼저 빌드할 것: yarn workspace @things-factory/figure-service build:server`)
|
|
21
|
+
return require(resolved)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
FigureRecipe = loadCompiled('service/figure-recipe/figure-recipe').FigureRecipe
|
|
26
|
+
store = loadCompiled('service/figure-recipe/figure-recipe-store')
|
|
27
|
+
dataSource = new TypeOrmDataSource({
|
|
28
|
+
type: 'sqlite',
|
|
29
|
+
database: ':memory:',
|
|
30
|
+
synchronize: true,
|
|
31
|
+
dropSchema: true,
|
|
32
|
+
namingStrategy: new shell.NamingStrategy(),
|
|
33
|
+
entities: [...shell.entities, ...authBase.entities, FigureRecipe],
|
|
34
|
+
logging: false
|
|
35
|
+
})
|
|
36
|
+
await dataSource.initialize()
|
|
37
|
+
shell.addDataSource('default', dataSource)
|
|
38
|
+
shell.addDataSource('tx', dataSource)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
if (!dataSource) return
|
|
43
|
+
shell.removeDataSource('default')
|
|
44
|
+
shell.removeDataSource('tx')
|
|
45
|
+
await dataSource.destroy()
|
|
46
|
+
dataSource = null
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
let SYSTEM: any
|
|
50
|
+
let CHILD: any
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
await dataSource!.synchronize(true)
|
|
54
|
+
const domains = dataSource!.getRepository(shell.Domain)
|
|
55
|
+
SYSTEM = await domains.save({ name: 'SYSTEM', subdomain: 'system', systemFlag: true })
|
|
56
|
+
CHILD = await domains.save({ name: 'plant', subdomain: 'plant', parentId: SYSTEM.id })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const rows = async () => (await dataSource!.getRepository(FigureRecipe).find({ relations: ['domain'], order: { name: 'ASC' } })).map((r: any) => [r.domain.subdomain, r.name])
|
|
60
|
+
|
|
61
|
+
describe('조각 — 시드', () => {
|
|
62
|
+
it('★ figure-model 이 싣는 조각 셋이 system 에 들어가고, 다시 실어도 같다 — 있으면 건너뛰고 덮지 않는다', async () => {
|
|
63
|
+
const first = await store.seedBuiltInRecipes()
|
|
64
|
+
expect(first.added).toEqual(['hinged-door', 'roller-row', 'sliding-door'])
|
|
65
|
+
expect(first.refused).toEqual([])
|
|
66
|
+
const repo = dataSource!.getRepository(FigureRecipe)
|
|
67
|
+
const door = await repo.findOne({ where: { name: 'sliding-door' } })
|
|
68
|
+
await repo.save({ ...door, state: 'draft' })
|
|
69
|
+
|
|
70
|
+
const again = await store.seedBuiltInRecipes()
|
|
71
|
+
expect(again.added).toEqual([])
|
|
72
|
+
expect(again.skipped).toEqual(['hinged-door', 'roller-row', 'sliding-door'])
|
|
73
|
+
expect(await rows()).toEqual([['system', 'hinged-door'], ['system', 'roller-row'], ['system', 'sliding-door']])
|
|
74
|
+
expect((await repo.findOne({ where: { name: 'sliding-door' } })).state).toBe('draft')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('조각 — 읽기는 도형과 같은 도메인 규칙', () => {
|
|
79
|
+
it('★ 자식 도메인은 부모의 조각을 보고, 같은 이름의 제 조각이 그것을 대신한다; 초안은 모두를 물을 때만', async () => {
|
|
80
|
+
await store.seedBuiltInRecipes()
|
|
81
|
+
const door = JSON.parse(store.builtInRecipes().find((r: any) => r.name === 'sliding-door').source)
|
|
82
|
+
door.title = { ko: '우리 공장 미닫이 문' }
|
|
83
|
+
await store.saveRecipe(CHILD, JSON.stringify(door))
|
|
84
|
+
const seen = await store.recipesFor({ id: CHILD.id, parentId: SYSTEM.id }, 'released')
|
|
85
|
+
expect(seen.map((r: any) => r.name)).toEqual(['hinged-door', 'roller-row', 'sliding-door'])
|
|
86
|
+
expect(JSON.parse(seen.find((r: any) => r.name === 'sliding-door').source).title.ko).toBe('우리 공장 미닫이 문')
|
|
87
|
+
|
|
88
|
+
const hinged = JSON.parse(store.builtInRecipes().find((r: any) => r.name === 'hinged-door').source)
|
|
89
|
+
await store.saveRecipe(CHILD, JSON.stringify({ ...hinged, name: 'shop-door' }), 'draft')
|
|
90
|
+
expect((await store.recipesFor({ id: CHILD.id, parentId: SYSTEM.id }, 'released')).map((r: any) => r.name)).not.toContain('shop-door')
|
|
91
|
+
expect((await store.recipesFor({ id: CHILD.id, parentId: SYSTEM.id })).map((r: any) => r.name)).toContain('shop-door')
|
|
92
|
+
expect((await store.recipesFor({ id: SYSTEM.id })).map((r: any) => r.name)).not.toContain('shop-door')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('조각 — 저장은 빈 방에 넣어 본 뒤에', () => {
|
|
97
|
+
it('★ 기본값으로 넣을 수 없는 조각은 커널의 말 그대로 거절되고 저장되지 않는다', async () => {
|
|
98
|
+
const door = JSON.parse(store.builtInRecipes().find((r: any) => r.name === 'sliding-door').source)
|
|
99
|
+
door.steps[2].source.part = 'nowhere'
|
|
100
|
+
await expect(store.saveRecipe(CHILD, JSON.stringify(door))).rejects.toThrow(/steps\[2\]/)
|
|
101
|
+
expect(await rows()).toEqual([])
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Arg, Ctx, Directive, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
|
|
2
|
+
|
|
3
|
+
import { SpeakRefusals } from '@things-factory/shell'
|
|
4
|
+
|
|
5
|
+
import { FigureRecipe } from './figure-recipe.js'
|
|
6
|
+
import { recipesFor, saveRecipe } from './figure-recipe-store.js'
|
|
7
|
+
|
|
8
|
+
@Resolver(FigureRecipe)
|
|
9
|
+
@UseMiddleware(SpeakRefusals)
|
|
10
|
+
export class FigureRecipeResolver {
|
|
11
|
+
@Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
|
|
12
|
+
@Query(returns => [FigureRecipe], { description: "The recipes this domain sees: its own and its parent's (its own stand in by name). Released ones unless all is asked." })
|
|
13
|
+
async figureRecipes(@Arg('all', { nullable: true, description: 'Drafts as well.' }) all: boolean, @Ctx() context: ResolverContext): Promise<FigureRecipe[]> {
|
|
14
|
+
return await recipesFor(context.state.domain, all ? undefined : 'released')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
18
|
+
@Mutation(returns => FigureRecipe, { description: 'Saves a recipe into this domain by its name, after putting it into an empty room with its defaults (ADR-0095).' })
|
|
19
|
+
async saveFigureRecipe(
|
|
20
|
+
@Arg('source', { description: 'The recipe JSON (figure-model V3Recipe).' }) source: string,
|
|
21
|
+
@Arg('state', { nullable: true, description: "'draft' or 'released' (the default)." }) state: string,
|
|
22
|
+
@Ctx() context: ResolverContext
|
|
23
|
+
): Promise<FigureRecipe> {
|
|
24
|
+
return await saveRecipe(context.state.domain, source, state === 'draft' ? 'draft' : 'released')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'fs'
|
|
2
|
+
import { dirname, join } from 'path'
|
|
3
|
+
import { In } from 'typeorm'
|
|
4
|
+
|
|
5
|
+
import { checkV3Recipe } from '@hatiolab/figure-model'
|
|
6
|
+
import { Domain, getRepository } from '@things-factory/shell'
|
|
7
|
+
|
|
8
|
+
import { FigureRecipe } from './figure-recipe.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The recipes a domain sees (ADR-0095 condition 2): its own and its parent's, as figures are read. A recipe of the
|
|
12
|
+
* domain's own stands in for the parent's of the same name.
|
|
13
|
+
*/
|
|
14
|
+
export async function recipesFor(domain: { id: string; parentId?: string | null }, state?: 'released'): Promise<FigureRecipe[]> {
|
|
15
|
+
const ids = [domain.id, domain.parentId].filter(Boolean) as string[]
|
|
16
|
+
const rows = await getRepository(FigureRecipe).find({
|
|
17
|
+
where: { domain: { id: In(ids) }, ...(state ? { state } : {}) },
|
|
18
|
+
relations: ['domain'],
|
|
19
|
+
order: { name: 'ASC' }
|
|
20
|
+
})
|
|
21
|
+
const byName = new Map<string, FigureRecipe>()
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
const mine = row.domainId === domain.id || (row.domain as Domain | undefined)?.id === domain.id
|
|
24
|
+
if (mine || !byName.has(row.name!)) byName.set(row.name!, row)
|
|
25
|
+
}
|
|
26
|
+
return [...byName.values()]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A recipe saved into a domain, by its name: checked first by putting it into the test room with its defaults
|
|
31
|
+
* (figure-model `checkV3Recipe`). Refused, it is not saved and the kernel's sentence goes back as it is.
|
|
32
|
+
*/
|
|
33
|
+
export async function saveRecipe(domain: Domain, source: unknown, state: 'draft' | 'released' = 'released'): Promise<FigureRecipe> {
|
|
34
|
+
const recipe = checkV3Recipe(typeof source === 'string' ? JSON.parse(source) : source)
|
|
35
|
+
const repository = getRepository(FigureRecipe)
|
|
36
|
+
const existing = await repository.findOne({ where: { domain: { id: domain.id }, name: recipe.name } })
|
|
37
|
+
return await repository.save({ ...(existing ?? {}), domain, name: recipe.name, source: JSON.stringify(recipe), state })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The recipes figure-model ships (`recipes/*.json`), as they are. */
|
|
41
|
+
export function builtInRecipes(): Array<{ name: string; source: string }> {
|
|
42
|
+
const dir = join(dirname(require.resolve('@hatiolab/figure-model/package.json')), 'recipes')
|
|
43
|
+
return readdirSync(dir)
|
|
44
|
+
.filter(file => file.endsWith('.json'))
|
|
45
|
+
.sort()
|
|
46
|
+
.map(file => {
|
|
47
|
+
const source = readFileSync(join(dir, file), 'utf-8')
|
|
48
|
+
return { name: JSON.parse(source).name, source }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The built-in recipes put into the system domain (ADR-0095 condition 2): once, and again gives the same result -- a
|
|
54
|
+
* recipe already there by name is skipped, not overwritten, so a system recipe someone changed stays as they left it.
|
|
55
|
+
* Each is checked as a saved one is; one that fails is left out and said, not half put in.
|
|
56
|
+
*/
|
|
57
|
+
export async function seedBuiltInRecipes(): Promise<{ added: string[]; skipped: string[]; refused: string[] }> {
|
|
58
|
+
const system = await getRepository(Domain).findOne({ where: { systemFlag: true } })
|
|
59
|
+
const result = { added: [] as string[], skipped: [] as string[], refused: [] as string[] }
|
|
60
|
+
if (!system) return result
|
|
61
|
+
const repository = getRepository(FigureRecipe)
|
|
62
|
+
for (const { name, source } of builtInRecipes()) {
|
|
63
|
+
if (await repository.count({ where: { domain: { id: system.id }, name } })) {
|
|
64
|
+
result.skipped.push(name)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await saveRecipe(system, source)
|
|
69
|
+
result.added.push(name)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
result.refused.push(`${name}: ${error instanceof Error ? error.message : String(error)}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
|
|
2
|
+
import { Field, ID, ObjectType } from 'type-graphql'
|
|
3
|
+
|
|
4
|
+
import { Domain, longTextColumn } from '@things-factory/shell'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FigureRecipe — 조각 하나(ADR-0095).
|
|
8
|
+
*
|
|
9
|
+
* 조각은 매개변수가 붙은 제안 명령 목록이다. 형식은 `@hatiolab/figure-model` 의 `V3Recipe` 가 정하고, 여기에는
|
|
10
|
+
* 그 원본을 통째로 담는다 — Figure 가 자산을 `source` 로 담는 것과 같다. 매개변수 · 명령을 컬럼으로 두지 않는다.
|
|
11
|
+
* 넣은 뒤의 부품은 조각과 이어지지 않으므로, 이 행을 고치거나 지워도 도형은 바뀌지 않는다.
|
|
12
|
+
*/
|
|
13
|
+
@Entity()
|
|
14
|
+
@Index('ix_figure_recipe_1', (recipe: FigureRecipe) => [recipe.domain, recipe.name], { unique: true, where: '"deleted_at" IS NULL' })
|
|
15
|
+
@ObjectType({ description: 'A recipe: a list of figure authoring commands with parameters (ADR-0095).' })
|
|
16
|
+
export class FigureRecipe {
|
|
17
|
+
@PrimaryGeneratedColumn('uuid')
|
|
18
|
+
@Field(type => ID, { nullable: true, description: 'Unique identifier for the recipe.' })
|
|
19
|
+
readonly id?: string
|
|
20
|
+
|
|
21
|
+
@ManyToOne(type => Domain)
|
|
22
|
+
@Field(type => Domain, { nullable: true, description: 'The domain this recipe belongs to.' })
|
|
23
|
+
domain?: Domain
|
|
24
|
+
|
|
25
|
+
@RelationId((recipe: FigureRecipe) => recipe.domain)
|
|
26
|
+
domainId?: string
|
|
27
|
+
|
|
28
|
+
@Column()
|
|
29
|
+
@Field({ description: 'The recipe name, unique per domain: lower case, digits and hyphens.' })
|
|
30
|
+
name?: string
|
|
31
|
+
|
|
32
|
+
@Column({ nullable: true, ...longTextColumn() })
|
|
33
|
+
@Field({ nullable: true, description: 'The recipe JSON (figure-model V3Recipe).' })
|
|
34
|
+
source?: string
|
|
35
|
+
|
|
36
|
+
@Column({ nullable: true, default: 'released' })
|
|
37
|
+
@Field({ nullable: true, description: "'draft' or 'released'. Only released recipes are offered in the modeller." })
|
|
38
|
+
state?: 'draft' | 'released'
|
|
39
|
+
|
|
40
|
+
@CreateDateColumn()
|
|
41
|
+
@Field({ nullable: true, description: 'When the recipe was created.' })
|
|
42
|
+
createdAt?: Date
|
|
43
|
+
|
|
44
|
+
@UpdateDateColumn()
|
|
45
|
+
@Field({ nullable: true, description: 'When the recipe was last changed.' })
|
|
46
|
+
updatedAt?: Date
|
|
47
|
+
|
|
48
|
+
@DeleteDateColumn()
|
|
49
|
+
@Field({ nullable: true, description: 'When the recipe was deleted.' })
|
|
50
|
+
deletedAt?: Date
|
|
51
|
+
}
|
package/server/service/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from './figure/figure.js'
|
|
|
3
3
|
export * from './figure/figure-type.js'
|
|
4
4
|
export * from './figure/figure-format.js'
|
|
5
5
|
export * from './figure/figure-history.js'
|
|
6
|
+
export * from './figure-recipe/figure-recipe.js'
|
|
7
|
+
export { recipesFor, saveRecipe, seedBuiltInRecipes, builtInRecipes } from './figure-recipe/figure-recipe-store.js'
|
|
6
8
|
|
|
7
9
|
// Entities, resolvers
|
|
8
10
|
import {
|
|
@@ -10,8 +12,9 @@ import {
|
|
|
10
12
|
resolvers as FigureResolvers,
|
|
11
13
|
subscribers as FigureSubscribers
|
|
12
14
|
} from './figure/index.js'
|
|
15
|
+
import { entities as RecipeEntities, resolvers as RecipeResolvers } from './figure-recipe/index.js'
|
|
13
16
|
|
|
14
|
-
export const entities = [...FigureEntities]
|
|
17
|
+
export const entities = [...FigureEntities, ...RecipeEntities]
|
|
15
18
|
|
|
16
19
|
/* 판본 스냅샷을 뜨는 구독자. 셸이 이 이름으로 모아 TypeORM 에 등록한다(board-service 와 같다). */
|
|
17
20
|
export const subscribers = [...FigureSubscribers]
|
|
@@ -27,5 +30,5 @@ export const subscribers = [...FigureSubscribers]
|
|
|
27
30
|
* board-service, ops-master and font-base all export the shape below.
|
|
28
31
|
*/
|
|
29
32
|
export const schema = {
|
|
30
|
-
resolverClasses: [...FigureResolvers]
|
|
33
|
+
resolverClasses: [...FigureResolvers, ...RecipeResolvers]
|
|
31
34
|
}
|