@things-factory/ops-master 10.1.34 → 10.1.35
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-client/master-change-approval.d.ts +7 -0
- package/dist-client/master-change-approval.js +16 -0
- package/dist-client/master-change-approval.js.map +1 -1
- package/dist-client/pages/master/material-definition.d.ts +4 -11
- package/dist-client/pages/master/material-definition.js +5 -53
- package/dist-client/pages/master/material-definition.js.map +1 -1
- package/dist-client/pages/master/material-revision-fields.d.ts +2 -0
- package/dist-client/pages/master/material-revision-fields.js +43 -0
- package/dist-client/pages/master/material-revision-fields.js.map +1 -0
- package/dist-client/pages/master/recipe-definition.d.ts +4 -135
- package/dist-client/pages/master/recipe-definition.js +7 -397
- package/dist-client/pages/master/recipe-definition.js.map +1 -1
- package/dist-client/pages/master/revision-comparison.d.ts +48 -0
- package/dist-client/pages/master/revision-comparison.js +553 -0
- package/dist-client/pages/master/revision-comparison.js.map +1 -0
- package/dist-client/pages/master/revision-view.d.ts +19 -0
- package/dist-client/pages/master/revision-view.js +133 -0
- package/dist-client/pages/master/revision-view.js.map +1 -0
- package/dist-client/pages/master/revision-workspace.d.ts +50 -0
- package/dist-client/pages/master/revision-workspace.js +752 -0
- package/dist-client/pages/master/revision-workspace.js.map +1 -0
- package/dist-client/pages/master/test-specification.d.ts +4 -29
- package/dist-client/pages/master/test-specification.js +5 -225
- package/dist-client/pages/master/test-specification.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +12 -2
- package/dist-server/service/index.js +10 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/master-axis.js +1 -1
- package/dist-server/service/master-axis.js.map +1 -1
- package/dist-server/service/master-change-policy.js +7 -2
- package/dist-server/service/master-change-policy.js.map +1 -1
- package/dist-server/service/master-conformance-resolver.js +11 -1
- package/dist-server/service/master-conformance-resolver.js.map +1 -1
- package/dist-server/service/master-crud.js +3 -0
- package/dist-server/service/master-crud.js.map +1 -1
- package/dist-server/service/master-list.js +6 -0
- package/dist-server/service/master-list.js.map +1 -1
- package/dist-server/service/material/index.d.ts +3 -2
- package/dist-server/service/material/index.js +4 -2
- package/dist-server/service/material/index.js.map +1 -1
- package/dist-server/service/material/material-axis-status.d.ts +8 -0
- package/dist-server/service/material/material-axis-status.js +19 -0
- package/dist-server/service/material/material-axis-status.js.map +1 -0
- package/dist-server/service/material/material-definition.d.ts +3 -0
- package/dist-server/service/material/material-definition.js +13 -0
- package/dist-server/service/material/material-definition.js.map +1 -1
- package/dist-server/service/material/material-effective-list.d.ts +39 -0
- package/dist-server/service/material/material-effective-list.js +54 -0
- package/dist-server/service/material/material-effective-list.js.map +1 -0
- package/dist-server/service/material/material-master-resolver.js +4 -10
- package/dist-server/service/material/material-master-resolver.js.map +1 -1
- package/dist-server/service/material/material-query.js +9 -2
- package/dist-server/service/material/material-query.js.map +1 -1
- package/dist-server/service/material/material-revision-content.d.ts +10 -0
- package/dist-server/service/material/material-revision-content.js +92 -0
- package/dist-server/service/material/material-revision-content.js.map +1 -0
- package/dist-server/service/material/material-revision-resolver.d.ts +54 -0
- package/dist-server/service/material/material-revision-resolver.js +76 -0
- package/dist-server/service/material/material-revision-resolver.js.map +1 -0
- package/dist-server/service/material/material-revision-service.d.ts +144 -0
- package/dist-server/service/material/material-revision-service.js +534 -0
- package/dist-server/service/material/material-revision-service.js.map +1 -0
- package/dist-server/service/material/material-revision.d.ts +84 -0
- package/dist-server/service/material/material-revision.js +339 -0
- package/dist-server/service/material/material-revision.js.map +1 -0
- package/dist-server/service/operation/operation-definition.js +1 -0
- package/dist-server/service/operation/operation-definition.js.map +1 -1
- package/dist-server/service/operation/operation-master-resolver.js +2 -5
- package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
- package/dist-server/service/recipe/index.d.ts +3 -2
- package/dist-server/service/recipe/index.js +4 -2
- package/dist-server/service/recipe/index.js.map +1 -1
- package/dist-server/service/recipe/recipe-definition.d.ts +4 -0
- package/dist-server/service/recipe/recipe-definition.js +14 -1
- package/dist-server/service/recipe/recipe-definition.js.map +1 -1
- package/dist-server/service/recipe/recipe-effective-list.d.ts +49 -0
- package/dist-server/service/recipe/recipe-effective-list.js +96 -0
- package/dist-server/service/recipe/recipe-effective-list.js.map +1 -0
- package/dist-server/service/recipe/recipe-query.js +9 -2
- package/dist-server/service/recipe/recipe-query.js.map +1 -1
- package/dist-server/service/recipe/recipe-revision-content.d.ts +32 -0
- package/dist-server/service/recipe/recipe-revision-content.js +65 -0
- package/dist-server/service/recipe/recipe-revision-content.js.map +1 -0
- package/dist-server/service/recipe/recipe-revision-line.d.ts +30 -0
- package/dist-server/service/recipe/recipe-revision-line.js +95 -0
- package/dist-server/service/recipe/recipe-revision-line.js.map +1 -0
- package/dist-server/service/recipe/recipe-revision-resolver.d.ts +59 -0
- package/dist-server/service/recipe/recipe-revision-resolver.js +76 -0
- package/dist-server/service/recipe/recipe-revision-resolver.js.map +1 -0
- package/dist-server/service/recipe/recipe-revision-service.d.ts +147 -0
- package/dist-server/service/recipe/recipe-revision-service.js +591 -0
- package/dist-server/service/recipe/recipe-revision-service.js.map +1 -0
- package/dist-server/service/recipe/recipe-revision.d.ts +100 -0
- package/dist-server/service/recipe/recipe-revision.js +373 -0
- package/dist-server/service/recipe/recipe-revision.js.map +1 -0
- package/dist-server/service/recipe/route-definition.js +1 -0
- package/dist-server/service/recipe/route-definition.js.map +1 -1
- package/dist-server/service/resource/class-members.js +9 -0
- package/dist-server/service/resource/class-members.js.map +1 -1
- package/dist-server/service/status/master-status.js +2 -1
- package/dist-server/service/status/master-status.js.map +1 -1
- package/dist-server/service/test-spec/test-spec-effective-list.d.ts +2 -0
- package/dist-server/service/test-spec/test-spec-effective-list.js +3 -1
- package/dist-server/service/test-spec/test-spec-effective-list.js.map +1 -1
- package/dist-server/service/test-spec/test-spec-revision-resolver.d.ts +1 -0
- package/dist-server/service/test-spec/test-spec-revision-service.d.ts +21 -0
- package/dist-server/service/test-spec/test-spec-revision-service.js +19 -0
- package/dist-server/service/test-spec/test-spec-revision-service.js.map +1 -1
- package/dist-server/service/test-spec/test-spec-revision.d.ts +2 -0
- package/dist-server/service/test-spec/test-spec-revision.js +6 -1
- package/dist-server/service/test-spec/test-spec-revision.js.map +1 -1
- package/dist-server/service/test-spec/test-specification.d.ts +2 -0
- package/dist-server/service/test-spec/test-specification.js +4 -0
- package/dist-server/service/test-spec/test-specification.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/translations/en.json +148 -0
- package/translations/ko.json +148 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-definition.js","sourceRoot":"","sources":["../../../server/service/operation/operation-definition.ts"],"names":[],"mappings":";;;;AAAA,qCASgB;AAChB,+CAAoD;AAEpD,iDAA4D;AAC5D,yDAAgD;AAEhD;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"operation-definition.js","sourceRoot":"","sources":["../../../server/service/operation/operation-definition.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 공정 정의 — **ISA-95 `OperationsSegment`**(`OperationsDefinition` 소속).\n *\n * 이 자리가 답하는 것은 「이 공정이 얼마나 걸리고 무엇을 필요로 하는가」다. 예측이 쓰는 소요시간의\n * 근거이고, 정상상태 상한과 병목을 공정별로 낼 수 있게 하는 값이다.\n *\n * ── 필요 자원을 적지 않으면 사람이 없는 공장이 된다 ─────────────────────────\n * 인원 명세가 없으면 사람은 예측에 존재하지 않는다. 설비만 있으면 언제나 가동되는 공장이 되고, 현장에서\n * 가장 자주 모자라는 자원이 만드는 줄이 통째로 사라진다. 등급으로 요구한다 — 「용접 자격자 2명」.\n */\n@Entity({ name: 'ops_operation_definition' })\n@Index('ix_ops_operation_domain_identity', ['domain', 'id'], { unique: true })\n@Index('ix_ops_operation_definition_0', (operation: OperationDefinition) => [operation.domain, operation.name], {\n unique: true\n})\n@ObjectType({ description: 'ISA-95 OperationsSegment — what an operation takes and requires' })\nexport class OperationDefinition {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((operation: OperationDefinition) => operation.domain)\n domainId?: string\n\n /** 공정 키 — 라우팅의 단계가 이 값을 가리킨다. */\n @Column()\n @Field()\n name: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /** 무엇을 하는 공정인가 — `transport` · `process` · `dwell`. */\n @Column()\n @Field()\n intent: string\n\n /** 이 공정이 벌어지는 자리 타입. 없으면 커널이 어디서 일하는지 정할 수 없다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n locationType?: string\n\n /** 필요한 설비 종류. `dwell` 은 자원을 잡지 않는다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n resourceType?: string\n\n /** 방출할 CBV bizStep URN. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n bizStep?: string\n\n /** 소요시간 — 표준 `Duration`. ISO 8601 기간 표기다. 없으면 커널이 기본값을 쓴다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n duration?: string\n\n /** 소요시간 변동 — 표준 밖 확장. 없으면 상수다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'DurationVariability' })\n variability?: Record<string, unknown>\n\n /** 공정 모수 — 표준 `ParameterSpecification`. 수율 · 셋업 시간 · 온도가 여기 온다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'OperationParameter[]' })\n parameters?: Record<string, unknown>[]\n\n /** 필요 인원 — 표준 `PersonnelSpecification`(등급 + 수량). */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'PersonnelSpecification[]' })\n personnelSpecification?: Record<string, unknown>[]\n\n /** 필요 물리 자산 — 표준 `PhysicalAssetSpecification`. 빈 팔레트가 없어 출고가 못 나가는 일은 잦다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'PhysicalAssetSpecification[]' })\n physicalAssetSpecification?: Record<string, unknown>[]\n\n /** 언제부터 유효한가 — 표준 `EffectiveStartDate`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /**\n * 언제까지 유효한가 — 표준 `EffectiveEndDate`.\n *\n * **없앨 때 행을 지우지 않고 이 칸을 채운다.** 지우면 그것을 가리키는 지난 기록이 가리킬 것을 잃는다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((operation: OperationDefinition) => operation.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((operation: OperationDefinition) => operation.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -11,6 +11,7 @@ const master_axis_1 = require("../master-axis");
|
|
|
11
11
|
const master_list_1 = require("../master-list");
|
|
12
12
|
const operation_definition_1 = require("./operation-definition");
|
|
13
13
|
const recipe_definition_1 = require("../recipe/recipe-definition");
|
|
14
|
+
const recipe_effective_list_1 = require("../recipe/recipe-effective-list");
|
|
14
15
|
const route_definition_1 = require("../recipe/route-definition");
|
|
15
16
|
const referrers_1 = require("../resource/referrers");
|
|
16
17
|
/**
|
|
@@ -71,11 +72,7 @@ async function scanOperationUse(domain) {
|
|
|
71
72
|
select: ['id', 'name', 'steps'],
|
|
72
73
|
take: OPERATION_SCAN_LIMIT
|
|
73
74
|
}),
|
|
74
|
-
recipeRepo.
|
|
75
|
-
where: { domain: { id: domain.id } },
|
|
76
|
-
select: ['id', 'name', 'inputs'],
|
|
77
|
-
take: OPERATION_SCAN_LIMIT
|
|
78
|
-
})
|
|
75
|
+
(0, recipe_effective_list_1.listEffectiveRecipes)(recipeRepo.manager, domain, { pagination: { page: 1, limit: OPERATION_SCAN_LIMIT } }).then(page => page.items)
|
|
79
76
|
]);
|
|
80
77
|
const routes = (0, referrers_1.indexMultiReferrers)(routeRows, one => one.steps ?? [], one => one.name);
|
|
81
78
|
const recipes = (0, referrers_1.indexMultiReferrers)(recipeRows, one => (one.inputs ?? []).map((part) => part?.operation), one => one.name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-master-resolver.js","sourceRoot":"","sources":["../../../server/service/operation/operation-master-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { UseMiddleware } from 'type-graphql'\nimport { SpeakRefusals } from '@things-factory/shell'\nimport { Args, Arg, Ctx, Directive, FieldResolver, Int, Mutation, ObjectType, Field, Query, Resolver, Root } from 'type-graphql'\n\nimport { Domain, ListParam, ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { MasterPatch, saveMasterPatches } from '../master-crud'\nimport { masterAxisOf } from '../master-axis'\nimport { listMaster } from '../master-list'\nimport { OperationDefinition } from './operation-definition'\nimport { RecipeDefinition } from '../recipe/recipe-definition'\nimport { RouteDefinition } from '../recipe/route-definition'\nimport { indexMultiReferrers } from '../resource/referrers'\n\n/**\n * 공정 축을 채우고 고친다.\n *\n * ── 왜 마스터는 그리드에서 고칠 수 있나 ────────────────────────────────────\n * 업무 기록과 달리 마스터는 **선언**이다. 「이 사람이 용접 자격을 갖는다」는 일어난 일이 아니라 지금의\n * 규정이고, 규정은 바뀐다. 없앨 때 행을 지우지 않고 유효 기간의 끝을 채우는 것이 그래서다 —\n * 지우면 그것을 가리키는 지난 기록이 가리킬 것을 잃는다.\n *\n * 규칙은 `master-crud.ts` 한 곳에 있다. 여기는 축의 이름만 붙인다.\n */\n\n@ObjectType({ description: 'A page of operation definitions' })\nexport class OperationDefinitionList {\n @Field(type => [OperationDefinition])\n items: OperationDefinition[]\n\n @Field(type => Int)\n total: number\n}\n\n/**\n * **이 공정을 누가 쓰나** — 들어오는 관계.\n *\n * ── 왜 필요한가 ────────────────────────────────────────────────────────────\n * 공정 하나를 고치거나 없앨 때 무엇이 딸려 무너지는지 알 수 없었다. 라우팅과 제조법을 하나씩 열어\n * 단계 목록을 눈으로 훑는 수밖에 없었다. 지금 miratek 은 라우팅 다섯에 제조법 다섯이라 그것이\n * 되지만, 제품이 늘면 안 된다.\n *\n * ── 배열 칸이라 훑는다 ─────────────────────────────────────────────────────\n * `RouteDefinition.steps` 와 `RecipeDefinition.inputs[].operation` 은 `simple-json` 이다.\n * 데이터베이스가 그 안을 못 보므로 `IN` 으로 못 좁힌다. 표를 훑어 뒤집는 수밖에 없다.\n *\n * **그래서 상한이 있고, 상한에 걸리면 말한다**(`referrerScanCapped`). 조용히 자르면 「이 공정을\n * 아무도 안 쓴다」와 「너무 많아 다 못 봤다」가 같아 보인다. 앞엣것은 지워도 되는 공정이고\n * 뒤엣것은 지우면 라인이 선다. 품목의 제조법 훑기가 같은 자리에 같은 것을 둔다.\n */\nconst OPERATION_SCAN_LIMIT = 2000\n\ninterface OperationUse {\n /** 이 공정을 단계로 갖는 라우팅들. */\n routes: Map<string, string[]>\n /** 이 공정에서 자재를 넣는 제조법들. */\n recipes: Map<string, string[]>\n /** 상한에 걸려 다 보지 못했나. */\n capped: boolean\n}\n\nasync function scanOperationUse(domain: Domain): Promise<OperationUse> {\n const routeRepo = getRepository(RouteDefinition)\n const recipeRepo = getRepository(RecipeDefinition)\n\n const [routeTotal, recipeTotal] = await Promise.all([\n routeRepo.count({ where: { domain: { id: domain.id } } as any }),\n recipeRepo.count({ where: { domain: { id: domain.id } } as any })\n ])\n\n const [routeRows, recipeRows] = await Promise.all([\n /*\n * `id` 를 함께 뽑는다. 빼면 TypeORM 이 「no such column: distinctAlias.…_id」로 터진다 —\n * `take` 를 쓰면 중복 제거를 위해 기본 키로 한 번 더 감싸는데, 그 키를 안 뽑으면 감쌀 것이 없다.\n */\n routeRepo.find({\n where: { domain: { id: domain.id } } as any,\n select: ['id', 'name', 'steps'] as any,\n take: OPERATION_SCAN_LIMIT\n }),\n recipeRepo.find({\n where: { domain: { id: domain.id } } as any,\n select: ['id', 'name', 'inputs'] as any,\n take: OPERATION_SCAN_LIMIT\n })\n ])\n\n const routes = indexMultiReferrers(\n routeRows as any[],\n one => one.steps ?? [],\n one => one.name\n )\n\n const recipes = indexMultiReferrers(\n recipeRows as any[],\n one => (one.inputs ?? []).map((part: any) => part?.operation),\n one => one.name\n )\n\n return { routes, recipes, capped: routeTotal > routeRows.length || recipeTotal > recipeRows.length }\n}\n\n/** 요청 안에서 한 번만 훑는다. **약속을 담는다** — 줄들이 거의 동시에 묻는다. */\nfunction operationUseOf(context: ResolverContext): Promise<OperationUse> {\n const state = context.state as { domain: Domain; operationUse?: Promise<OperationUse> }\n\n state.operationUse = state.operationUse || scanOperationUse(state.domain)\n\n return state.operationUse\n}\n\n@Resolver(OperationDefinition)\n@UseMiddleware(SpeakRefusals)\nexport class OperationDefinitionResolver {\n @Directive('@privilege(category: \"ops-master\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => OperationDefinitionList, { description: 'To fetch operation definitions' })\n async operationDefinitions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<OperationDefinitionList> {\n const { domain } = context.state\n const { items, total } = await listMaster('operationDefinition', params, domain)\n\n return { items: items as OperationDefinition[], total }\n }\n\n /**\n * 이 공정을 단계로 갖는 **라우팅들.**\n *\n * 공정을 고치기 전에 무엇이 딸려 오는지 여기서 본다. 여럿일 수 있다 — 같은 공정을 여러 라우팅이\n * 지난다(절단은 표준에도 빠른 것에도 있다).\n *\n * 아무도 안 쓰면 빈 목록이다. 다만 그것이 「안 쓴다」인지 「다 못 봤다」인지는\n * `referrerScanCapped` 가 말한다.\n */\n @FieldResolver(type => [String], { description: 'Routes that include this operation as a step' })\n async usedInRoutes(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<string[]> {\n return (await operationUseOf(context)).routes.get(operation.name) ?? []\n }\n\n /**\n * 이 공정에서 자재를 넣는 **제조법들.**\n *\n * 라우팅은 「어디를 지나나」이고 이것은 「거기서 무엇이 빠지나」다. 공정을 없애면 그 자재가\n * 빠질 자리를 잃는다.\n */\n @FieldResolver(type => [String], { description: 'Recipes that consume materials at this operation' })\n async consumedAt(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<string[]> {\n return (await operationUseOf(context)).recipes.get(operation.name) ?? []\n }\n\n /**\n * 라우팅·제조법을 다 보지 못했나.\n *\n * **조용히 자르지 않는다.** 상한에 걸리면 위의 두 칸이 덜 찬 답이고, 그것을 말하지 않으면\n * 「쓰는 데가 없다」와 「못 봤다」가 같아 보인다.\n */\n @FieldResolver(type => Boolean, { description: 'True when routes or recipes were too many to scan fully' })\n async referrerScanCapped(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<boolean> {\n return (await operationUseOf(context)).capped\n }\n\n /**\n * 그리드가 보낸 변경을 한 번에 적용한다.\n *\n * 패치를 `ScalarObject` 로 받는다 — 축마다 입력 타입을 따로 만들면 칸을 하나 더할 때마다 두 곳을\n * 고쳐야 하고, 한쪽을 빠뜨리면 그 칸이 오류 보고 없이 저장되지 않는다. 돌려주는 것은 타입이 있는\n * 엔티티라 무엇이 저장됐는지 스키마로 확인할 수 있다.\n */\n @Directive('@privilege(category: \"ops-master\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => [OperationDefinition], { description: 'To create, change or retire operation definitions' })\n async updateMultipleOperationDefinition(\n @Arg('patches', type => [ScalarObject]) patches: MasterPatch[],\n @Ctx() context: ResolverContext\n ): Promise<OperationDefinition[]> {\n const { domain, user, tx } = context.state\n const axis = masterAxisOf('operationDefinition')\n\n const saved = await saveMasterPatches(tx, domain, axis.entity, patches, user, {\n hasEffectivePeriod: axis.hasEffectivePeriod\n })\n\n return saved as OperationDefinition[]\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operation-master-resolver.js","sourceRoot":"","sources":["../../../server/service/operation/operation-master-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { UseMiddleware } from 'type-graphql'\nimport { SpeakRefusals } from '@things-factory/shell'\nimport { Args, Arg, Ctx, Directive, FieldResolver, Int, Mutation, ObjectType, Field, Query, Resolver, Root } from 'type-graphql'\n\nimport { Domain, ListParam, ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { MasterPatch, saveMasterPatches } from '../master-crud'\nimport { masterAxisOf } from '../master-axis'\nimport { listMaster } from '../master-list'\nimport { OperationDefinition } from './operation-definition'\nimport { RecipeDefinition } from '../recipe/recipe-definition'\nimport { listEffectiveRecipes } from '../recipe/recipe-effective-list'\nimport { RouteDefinition } from '../recipe/route-definition'\nimport { indexMultiReferrers } from '../resource/referrers'\n\n/**\n * 공정 축을 채우고 고친다.\n *\n * ── 왜 마스터는 그리드에서 고칠 수 있나 ────────────────────────────────────\n * 업무 기록과 달리 마스터는 **선언**이다. 「이 사람이 용접 자격을 갖는다」는 일어난 일이 아니라 지금의\n * 규정이고, 규정은 바뀐다. 없앨 때 행을 지우지 않고 유효 기간의 끝을 채우는 것이 그래서다 —\n * 지우면 그것을 가리키는 지난 기록이 가리킬 것을 잃는다.\n *\n * 규칙은 `master-crud.ts` 한 곳에 있다. 여기는 축의 이름만 붙인다.\n */\n\n@ObjectType({ description: 'A page of operation definitions' })\nexport class OperationDefinitionList {\n @Field(type => [OperationDefinition])\n items: OperationDefinition[]\n\n @Field(type => Int)\n total: number\n}\n\n/**\n * **이 공정을 누가 쓰나** — 들어오는 관계.\n *\n * ── 왜 필요한가 ────────────────────────────────────────────────────────────\n * 공정 하나를 고치거나 없앨 때 무엇이 딸려 무너지는지 알 수 없었다. 라우팅과 제조법을 하나씩 열어\n * 단계 목록을 눈으로 훑는 수밖에 없었다. 지금 miratek 은 라우팅 다섯에 제조법 다섯이라 그것이\n * 되지만, 제품이 늘면 안 된다.\n *\n * ── 배열 칸이라 훑는다 ─────────────────────────────────────────────────────\n * `RouteDefinition.steps` 와 `RecipeDefinition.inputs[].operation` 은 `simple-json` 이다.\n * 데이터베이스가 그 안을 못 보므로 `IN` 으로 못 좁힌다. 표를 훑어 뒤집는 수밖에 없다.\n *\n * **그래서 상한이 있고, 상한에 걸리면 말한다**(`referrerScanCapped`). 조용히 자르면 「이 공정을\n * 아무도 안 쓴다」와 「너무 많아 다 못 봤다」가 같아 보인다. 앞엣것은 지워도 되는 공정이고\n * 뒤엣것은 지우면 라인이 선다. 품목의 제조법 훑기가 같은 자리에 같은 것을 둔다.\n */\nconst OPERATION_SCAN_LIMIT = 2000\n\ninterface OperationUse {\n /** 이 공정을 단계로 갖는 라우팅들. */\n routes: Map<string, string[]>\n /** 이 공정에서 자재를 넣는 제조법들. */\n recipes: Map<string, string[]>\n /** 상한에 걸려 다 보지 못했나. */\n capped: boolean\n}\n\nasync function scanOperationUse(domain: Domain): Promise<OperationUse> {\n const routeRepo = getRepository(RouteDefinition)\n const recipeRepo = getRepository(RecipeDefinition)\n\n const [routeTotal, recipeTotal] = await Promise.all([\n routeRepo.count({ where: { domain: { id: domain.id } } as any }),\n recipeRepo.count({ where: { domain: { id: domain.id } } as any })\n ])\n\n const [routeRows, recipeRows] = await Promise.all([\n /*\n * `id` 를 함께 뽑는다. 빼면 TypeORM 이 「no such column: distinctAlias.…_id」로 터진다 —\n * `take` 를 쓰면 중복 제거를 위해 기본 키로 한 번 더 감싸는데, 그 키를 안 뽑으면 감쌀 것이 없다.\n */\n routeRepo.find({\n where: { domain: { id: domain.id } } as any,\n select: ['id', 'name', 'steps'] as any,\n take: OPERATION_SCAN_LIMIT\n }),\n listEffectiveRecipes(recipeRepo.manager, domain, { pagination: { page:1, limit:OPERATION_SCAN_LIMIT } } as any).then(page => page.items)\n ])\n\n const routes = indexMultiReferrers(\n routeRows as any[],\n one => one.steps ?? [],\n one => one.name\n )\n\n const recipes = indexMultiReferrers(\n recipeRows as any[],\n one => (one.inputs ?? []).map((part: any) => part?.operation),\n one => one.name\n )\n\n return { routes, recipes, capped: routeTotal > routeRows.length || recipeTotal > recipeRows.length }\n}\n\n/** 요청 안에서 한 번만 훑는다. **약속을 담는다** — 줄들이 거의 동시에 묻는다. */\nfunction operationUseOf(context: ResolverContext): Promise<OperationUse> {\n const state = context.state as { domain: Domain; operationUse?: Promise<OperationUse> }\n\n state.operationUse = state.operationUse || scanOperationUse(state.domain)\n\n return state.operationUse\n}\n\n@Resolver(OperationDefinition)\n@UseMiddleware(SpeakRefusals)\nexport class OperationDefinitionResolver {\n @Directive('@privilege(category: \"ops-master\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => OperationDefinitionList, { description: 'To fetch operation definitions' })\n async operationDefinitions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<OperationDefinitionList> {\n const { domain } = context.state\n const { items, total } = await listMaster('operationDefinition', params, domain)\n\n return { items: items as OperationDefinition[], total }\n }\n\n /**\n * 이 공정을 단계로 갖는 **라우팅들.**\n *\n * 공정을 고치기 전에 무엇이 딸려 오는지 여기서 본다. 여럿일 수 있다 — 같은 공정을 여러 라우팅이\n * 지난다(절단은 표준에도 빠른 것에도 있다).\n *\n * 아무도 안 쓰면 빈 목록이다. 다만 그것이 「안 쓴다」인지 「다 못 봤다」인지는\n * `referrerScanCapped` 가 말한다.\n */\n @FieldResolver(type => [String], { description: 'Routes that include this operation as a step' })\n async usedInRoutes(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<string[]> {\n return (await operationUseOf(context)).routes.get(operation.name) ?? []\n }\n\n /**\n * 이 공정에서 자재를 넣는 **제조법들.**\n *\n * 라우팅은 「어디를 지나나」이고 이것은 「거기서 무엇이 빠지나」다. 공정을 없애면 그 자재가\n * 빠질 자리를 잃는다.\n */\n @FieldResolver(type => [String], { description: 'Recipes that consume materials at this operation' })\n async consumedAt(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<string[]> {\n return (await operationUseOf(context)).recipes.get(operation.name) ?? []\n }\n\n /**\n * 라우팅·제조법을 다 보지 못했나.\n *\n * **조용히 자르지 않는다.** 상한에 걸리면 위의 두 칸이 덜 찬 답이고, 그것을 말하지 않으면\n * 「쓰는 데가 없다」와 「못 봤다」가 같아 보인다.\n */\n @FieldResolver(type => Boolean, { description: 'True when routes or recipes were too many to scan fully' })\n async referrerScanCapped(@Root() operation: OperationDefinition, @Ctx() context: ResolverContext): Promise<boolean> {\n return (await operationUseOf(context)).capped\n }\n\n /**\n * 그리드가 보낸 변경을 한 번에 적용한다.\n *\n * 패치를 `ScalarObject` 로 받는다 — 축마다 입력 타입을 따로 만들면 칸을 하나 더할 때마다 두 곳을\n * 고쳐야 하고, 한쪽을 빠뜨리면 그 칸이 오류 보고 없이 저장되지 않는다. 돌려주는 것은 타입이 있는\n * 엔티티라 무엇이 저장됐는지 스키마로 확인할 수 있다.\n */\n @Directive('@privilege(category: \"ops-master\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => [OperationDefinition], { description: 'To create, change or retire operation definitions' })\n async updateMultipleOperationDefinition(\n @Arg('patches', type => [ScalarObject]) patches: MasterPatch[],\n @Ctx() context: ResolverContext\n ): Promise<OperationDefinition[]> {\n const { domain, user, tx } = context.state\n const axis = masterAxisOf('operationDefinition')\n\n const saved = await saveMasterPatches(tx, domain, axis.entity, patches, user, {\n hasEffectivePeriod: axis.hasEffectivePeriod\n })\n\n return saved as OperationDefinition[]\n }\n}\n"]}
|
|
@@ -2,8 +2,9 @@ import { RecipeDefinition } from './recipe-definition';
|
|
|
2
2
|
import { RouteDefinition } from './route-definition';
|
|
3
3
|
import { RecipeDefinitionResolver, RouteDefinitionResolver } from './recipe-master-resolver';
|
|
4
4
|
import { RecipeQuery } from './recipe-query';
|
|
5
|
-
|
|
6
|
-
export declare const
|
|
5
|
+
import { RecipeRevisionResolver } from './recipe-revision-resolver';
|
|
6
|
+
export declare const entities: (typeof RecipeDefinition | typeof RouteDefinition | typeof import("./recipe-revision-line").RecipeRevisionOutput | typeof import("./recipe-revision").RecipeRevision | typeof import("./recipe-revision").RecipePlan | typeof import("./recipe-revision").RecipeSegment | typeof import("./recipe-revision").RecipeBasis)[];
|
|
7
|
+
export declare const resolvers: (typeof RecipeDefinitionResolver | typeof RouteDefinitionResolver | typeof RecipeRevisionResolver | typeof RecipeQuery)[];
|
|
7
8
|
export * from './recipe-definition';
|
|
8
9
|
export * from './route-definition';
|
|
9
10
|
export * from './recipe-master';
|
|
@@ -6,8 +6,10 @@ const recipe_definition_1 = require("./recipe-definition");
|
|
|
6
6
|
const route_definition_1 = require("./route-definition");
|
|
7
7
|
const recipe_master_resolver_1 = require("./recipe-master-resolver");
|
|
8
8
|
const recipe_query_1 = require("./recipe-query");
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const recipe_revision_1 = require("./recipe-revision");
|
|
10
|
+
const recipe_revision_resolver_1 = require("./recipe-revision-resolver");
|
|
11
|
+
exports.entities = [recipe_definition_1.RecipeDefinition, route_definition_1.RouteDefinition, ...recipe_revision_1.RECIPE_REVISION_ENTITIES];
|
|
12
|
+
exports.resolvers = [recipe_master_resolver_1.RecipeDefinitionResolver, recipe_master_resolver_1.RouteDefinitionResolver, recipe_query_1.RecipeQuery, recipe_revision_resolver_1.RecipeRevisionResolver];
|
|
11
13
|
tslib_1.__exportStar(require("./recipe-definition"), exports);
|
|
12
14
|
tslib_1.__exportStar(require("./route-definition"), exports);
|
|
13
15
|
tslib_1.__exportStar(require("./recipe-master"), exports);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/recipe/index.ts"],"names":[],"mappings":";;;;AAAA,2DAAsD;AACtD,yDAAoD;AACpD,qEAA4F;AAC5F,iDAA4C;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/recipe/index.ts"],"names":[],"mappings":";;;;AAAA,2DAAsD;AACtD,yDAAoD;AACpD,qEAA4F;AAC5F,iDAA4C;AAC5C,uDAA4D;AAC5D,yEAAmE;AAEtD,QAAA,QAAQ,GAAG,CAAC,oCAAgB,EAAE,kCAAe,EAAE,GAAG,0CAAwB,CAAC,CAAA;AAC3E,QAAA,SAAS,GAAG,CAAC,iDAAwB,EAAE,gDAAuB,EAAE,0BAAW,EAAE,iDAAsB,CAAC,CAAA;AAEjH,8DAAmC;AACnC,6DAAkC;AAClC,0DAA+B","sourcesContent":["import { RecipeDefinition } from './recipe-definition'\nimport { RouteDefinition } from './route-definition'\nimport { RecipeDefinitionResolver, RouteDefinitionResolver } from './recipe-master-resolver'\nimport { RecipeQuery } from './recipe-query'\nimport { RECIPE_REVISION_ENTITIES } from './recipe-revision'\nimport { RecipeRevisionResolver } from './recipe-revision-resolver'\n\nexport const entities = [RecipeDefinition, RouteDefinition, ...RECIPE_REVISION_ENTITIES]\nexport const resolvers = [RecipeDefinitionResolver, RouteDefinitionResolver, RecipeQuery, RecipeRevisionResolver]\n\nexport * from './recipe-definition'\nexport * from './route-definition'\nexport * from './recipe-master'\n"]}
|
|
@@ -22,6 +22,10 @@ import { User } from '@things-factory/auth-base';
|
|
|
22
22
|
* 가리키지 않고 레시피가 품목을 가리킨다** — 품목은 하나를 고를 수 없다.
|
|
23
23
|
*/
|
|
24
24
|
export declare class RecipeDefinition {
|
|
25
|
+
/** Read projection of the effective revision, not a mutable identity column. */
|
|
26
|
+
revisionNumber?: number;
|
|
27
|
+
planGeneration: number;
|
|
28
|
+
revisionLock: number;
|
|
25
29
|
readonly id: string;
|
|
26
30
|
domain?: Domain;
|
|
27
31
|
domainId?: string;
|
|
@@ -30,6 +30,18 @@ const auth_base_1 = require("@things-factory/auth-base");
|
|
|
30
30
|
let RecipeDefinition = class RecipeDefinition {
|
|
31
31
|
};
|
|
32
32
|
exports.RecipeDefinition = RecipeDefinition;
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, type_graphql_1.Field)(() => type_graphql_1.Int, { nullable: true }),
|
|
35
|
+
tslib_1.__metadata("design:type", Number)
|
|
36
|
+
], RecipeDefinition.prototype, "revisionNumber", void 0);
|
|
37
|
+
tslib_1.__decorate([
|
|
38
|
+
(0, typeorm_1.Column)('int', { default: 0 }),
|
|
39
|
+
tslib_1.__metadata("design:type", Number)
|
|
40
|
+
], RecipeDefinition.prototype, "planGeneration", void 0);
|
|
41
|
+
tslib_1.__decorate([
|
|
42
|
+
(0, typeorm_1.Column)('int', { default: 0 }),
|
|
43
|
+
tslib_1.__metadata("design:type", Number)
|
|
44
|
+
], RecipeDefinition.prototype, "revisionLock", void 0);
|
|
33
45
|
tslib_1.__decorate([
|
|
34
46
|
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
35
47
|
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
@@ -56,7 +68,7 @@ tslib_1.__decorate([
|
|
|
56
68
|
], RecipeDefinition.prototype, "description", void 0);
|
|
57
69
|
tslib_1.__decorate([
|
|
58
70
|
(0, typeorm_1.Column)(),
|
|
59
|
-
(0, type_graphql_1.Field)(),
|
|
71
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
60
72
|
tslib_1.__metadata("design:type", String)
|
|
61
73
|
], RecipeDefinition.prototype, "materialDefinitionName", void 0);
|
|
62
74
|
tslib_1.__decorate([
|
|
@@ -121,6 +133,7 @@ tslib_1.__decorate([
|
|
|
121
133
|
], RecipeDefinition.prototype, "updaterId", void 0);
|
|
122
134
|
exports.RecipeDefinition = RecipeDefinition = tslib_1.__decorate([
|
|
123
135
|
(0, typeorm_1.Entity)({ name: 'ops_recipe_definition' }),
|
|
136
|
+
(0, typeorm_1.Index)('ix_ops_recipe_domain_identity', ['domain', 'id'], { unique: true }),
|
|
124
137
|
(0, typeorm_1.Index)('ix_ops_recipe_definition_0', (recipe) => [recipe.domain, recipe.name], { unique: true })
|
|
125
138
|
/* 「이 품목을 만드는 레시피」로 묻는 질의가 이 축으로 온다 — MES 가 지시를 받을 때마다 부른다. */
|
|
126
139
|
,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipe-definition.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-definition.ts"],"names":[],"mappings":";;;;AAAA,qCAAkI;AAClI,+
|
|
1
|
+
{"version":3,"file":"recipe-definition.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-definition.ts"],"names":[],"mappings":";;;;AAAA,qCAAkI;AAClI,+CAAyD;AAEzD,iDAA4D;AAC5D,yDAAgD;AAEhD;;;;;;;;;;;;;;;;;;;;GAoBG;AAOI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CAmG5B,CAAA;AAnGY,4CAAgB;AAG3B;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;wDACd;AACQ;IAA9B,IAAA,gBAAM,EAAC,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;wDAAuB;AACtB;IAA9B,IAAA,gBAAM,EAAC,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;sDAAqB;AAG1C;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,CAAC;;4CACC;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;sCACb,cAAM;gDAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,MAAwB,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;;kDACvC;AAKjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,GAAE;;8CACI;AAIZ;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;qDACN;AAYpB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;gEACI;AAU9B;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAC/B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;gDAC1D;AAKjC;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAC/B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;;iDAC3B;AAUlC;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACZ;AAKd;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACT,IAAI;IAErB;;;;;OAKG;;wDAPkB;AAUrB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACX,IAAI;sDAAA;AAInB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;mDAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;mDAAA;AAIhB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAC9B,gBAAI;iDAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,MAAwB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC;;mDACvC;AAIlB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAC9B,gBAAI;iDAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,MAAwB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC;;mDACvC;2BAlGP,gBAAgB;IAN5B,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IACzC,IAAA,eAAK,EAAC,+BAA+B,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1E,IAAA,eAAK,EAAC,4BAA4B,EAAE,CAAC,MAAwB,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAClH,8DAA8D;;IAC7D,IAAA,eAAK,EAAC,4BAA4B,EAAE,CAAC,MAAwB,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,sBAAsB,CAAC,CAAC;IACjH,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,iFAAiF,EAAE,CAAC;GAClG,gBAAgB,CAmG5B","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 레시피 — **무엇으로 무엇을 만드나**. 커널 `RecipeDef` 와 같은 모양이다.\n *\n * ── 이 자리가 답하는 것 ──────────────────────────────────────────────────\n * 「이 품목은 무엇으로 만드나」와 「이 품목을 만드는 것은 어느 레시피인가」다. 둘째가 MES 가 물어야 했던\n * 것인데 물을 곳이 없었다.\n *\n * ── 품목에 지시서를 달지 않는 이유 ───────────────────────────────────────\n * ISA-95 에는 자재 명세가 붙는 자리가 둘이고 뜻이 다르다(§`RecipePart.operation`).\n *\n * · `ProcessSegment` — 그 자리가 **할 수 있는 일**. 품목에 매이지 않는다 → `OperationDefinition`\n * · `OperationsSegment` — 그 **품목을 만드는 절차**의 한 단계. 품목에 매인다 → **레시피가 그 범위다**\n *\n * 그래서 품목→지시서를 이으려면 그 사이에 레시피가 있어야 한다. 공정에 품목을 붙이면 범주 오류이고,\n * 실 데이터가 그 크기를 보였다 — 「배합」에 품목별 BOM 을 붙이면 배합을 지나는 **모든** 로트가 자재\n * 631종 36톤을 먹는다.\n *\n * ── 같은 품목에 레시피가 둘일 수 있다 ────────────────────────────────────\n * 대체 레시피가 정상이다(첫 실 연동에서 라우팅이 둘 이상인 레시피가 306/471). 그래서 **품목이 레시피를\n * 가리키지 않고 레시피가 품목을 가리킨다** — 품목은 하나를 고를 수 없다.\n */\n@Entity({ name: 'ops_recipe_definition' })\n@Index('ix_ops_recipe_domain_identity', ['domain', 'id'], { unique: true })\n@Index('ix_ops_recipe_definition_0', (recipe: RecipeDefinition) => [recipe.domain, recipe.name], { unique: true })\n/* 「이 품목을 만드는 레시피」로 묻는 질의가 이 축으로 온다 — MES 가 지시를 받을 때마다 부른다. */\n@Index('ix_ops_recipe_definition_1', (recipe: RecipeDefinition) => [recipe.domain, recipe.materialDefinitionName])\n@ObjectType({ description: 'Recipe — what a material is made of and through which route (kernel RecipeDef).' })\nexport class RecipeDefinition {\n /** Read projection of the effective revision, not a mutable identity column. */\n @Field(() => Int, { nullable: true })\n revisionNumber?: number\n @Column('int', { default: 0 }) planGeneration: number\n @Column('int', { default: 0 }) revisionLock: number\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((recipe: RecipeDefinition) => recipe.domain)\n domainId?: string\n\n /** 레시피 키 — 오더의 `recipeKey` 가 이 값을 가리킨다(커널 `RecipeDef.key`). */\n @Column()\n @Field()\n name: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * **무엇을 만드나** — `MaterialDefinition.name`.\n *\n * `outputs` 안에도 있지만 이 칸을 따로 둔다. 「이 품목을 만드는 레시피」를 JSON 안을 뒤져 찾으면\n * 다섯 드라이버에서 색인이 서지 않고, 그 질의는 MES 가 지시를 받을 때마다 부른다.\n *\n * 산출이 여럿인 레시피(부산물)에서는 **주 산출**을 적는다. 나머지는 `outputs` 가 든다.\n */\n @Column()\n @Field({ nullable: true })\n materialDefinitionName: string\n\n /**\n * 투입 — 커널 `RecipePart[]`(`material` · `qty` · `operation?`).\n *\n * `operation` 은 **이 투입이 들어가는 공정**이다. 없으면 오더 착수에 확보한다. 그 태그가 여기 있는\n * 이유는 품목별 소요가 사는 자리가 레시피이기 때문이다.\n */\n @Column({ type: 'simple-json' })\n @Field(type => ScalarObject, { description: 'RecipePart[] — material · qty · operation?' })\n inputs: Record<string, unknown>[]\n\n /** 산출 — 커널 `RecipePart[]`. 부산물이 있으면 여기 함께 든다. */\n @Column({ type: 'simple-json' })\n @Field(type => ScalarObject, { description: 'RecipePart[]' })\n outputs: Record<string, unknown>[]\n\n /**\n * 이 산출물을 만드는 공정 라우팅 — `RouteDefinition.name`(커널 `RecipeDef.route`).\n *\n * 순서는 라우팅이 소유하고 레시피는 그것을 가리킨다. 레시피에 단계 목록을 또 두면 두 목록이 어긋날 수\n * 있고, 어긋났을 때 어느 쪽이 맞는지 말할 근거가 없다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n route?: string\n\n /** 언제부터 유효한가 — 표준 `EffectiveStartDate`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /**\n * 언제까지 유효한가 — 표준 `EffectiveEndDate`.\n *\n * **개정할 때 행을 고치지 않고 이 칸을 채우고 새 행을 세운다.** 고치면 그 레시피로 만든 지난 실적이\n * 어느 배합으로 만든 것인지 잃는다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((recipe: RecipeDefinition) => recipe.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((recipe: RecipeDefinition) => recipe.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Domain, ListParam } from '@things-factory/shell';
|
|
3
|
+
import { RecipeDefinition } from './recipe-definition';
|
|
4
|
+
/** Filter, sort, paginate and display the same time projection. No per-row resolution queries. */
|
|
5
|
+
export declare function listEffectiveRecipes(manager: EntityManager, domain: Domain, params: ListParam, at?: Date): Promise<{
|
|
6
|
+
items: (RecipeDefinition & {
|
|
7
|
+
revisionNumber?: number;
|
|
8
|
+
planGeneration: number;
|
|
9
|
+
revisionLock: number;
|
|
10
|
+
id: string;
|
|
11
|
+
domain?: Domain;
|
|
12
|
+
domainId?: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
materialDefinitionName: string;
|
|
16
|
+
inputs: Record<string, unknown>[];
|
|
17
|
+
outputs: Record<string, unknown>[];
|
|
18
|
+
route?: string;
|
|
19
|
+
effectiveStart?: Date;
|
|
20
|
+
effectiveEnd?: Date;
|
|
21
|
+
createdAt?: Date;
|
|
22
|
+
updatedAt?: Date;
|
|
23
|
+
creator?: import("@things-factory/auth-base").User;
|
|
24
|
+
creatorId?: string;
|
|
25
|
+
updater?: import("@things-factory/auth-base").User;
|
|
26
|
+
updaterId?: string;
|
|
27
|
+
} & {
|
|
28
|
+
description: string;
|
|
29
|
+
revisionNumber: number;
|
|
30
|
+
materialDefinitionName: string;
|
|
31
|
+
route: string;
|
|
32
|
+
inputs: Record<string, unknown>[];
|
|
33
|
+
outputs: Record<string, unknown>[];
|
|
34
|
+
effectiveStart: Date;
|
|
35
|
+
effectiveEnd: Date;
|
|
36
|
+
})[];
|
|
37
|
+
total: number;
|
|
38
|
+
}>;
|
|
39
|
+
/** Candidate selection retains future/expired declarations so callers can explain why a choice is unavailable.
|
|
40
|
+
* One joined read; no per-order or per-recipe resolution queries. Payload rows are not loaded here.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recipeChoicesAt(manager: EntityManager, domainId: string, materialNames: string[], at?: Date): Promise<{
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
materialDefinitionName: string;
|
|
46
|
+
route: string | undefined;
|
|
47
|
+
effectiveStart: Date;
|
|
48
|
+
effectiveEnd: Date | undefined;
|
|
49
|
+
}[]>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listEffectiveRecipes = listEffectiveRecipes;
|
|
4
|
+
exports.recipeChoicesAt = recipeChoicesAt;
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const shell_1 = require("@things-factory/shell");
|
|
7
|
+
const recipe_definition_1 = require("./recipe-definition");
|
|
8
|
+
const recipe_revision_1 = require("./recipe-revision");
|
|
9
|
+
const recipe_revision_line_1 = require("./recipe-revision-line");
|
|
10
|
+
/** Filter, sort, paginate and display the same time projection. No per-row resolution queries. */
|
|
11
|
+
async function listEffectiveRecipes(manager, domain, params, at = new Date()) {
|
|
12
|
+
const query = (0, shell_1.getQueryBuilderFromListParams)({
|
|
13
|
+
repository: manager.getRepository(recipe_definition_1.RecipeDefinition),
|
|
14
|
+
domain,
|
|
15
|
+
params,
|
|
16
|
+
alias: 'recipe',
|
|
17
|
+
searchables: ['name', 'description'],
|
|
18
|
+
sortables: ['id', 'name', 'revisionNumber', 'description', 'effectiveStart', 'effectiveEnd', 'createdAt', 'updatedAt'],
|
|
19
|
+
columnExpressions: {
|
|
20
|
+
revisionNumber: 'revision.revisionNumber',
|
|
21
|
+
description: 'revision.description',
|
|
22
|
+
materialDefinitionName: 'revision.materialDefinitionName',
|
|
23
|
+
route: 'revision.route',
|
|
24
|
+
effectiveStart: 'segment.validFrom',
|
|
25
|
+
effectiveEnd: 'segment.validTo'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
query
|
|
29
|
+
.leftJoin(recipe_revision_1.RecipePlan, 'plan', 'plan.recipe_id = recipe.id AND plan.domain_id = recipe.domain_id AND plan.generation = recipe.plan_generation')
|
|
30
|
+
.leftJoinAndMapOne('recipe.selectedSegment', recipe_revision_1.RecipeSegment, 'segment', 'segment.plan_id = plan.id AND segment.validFrom <= :at AND (segment.validTo IS NULL OR segment.validTo > :at)', { at })
|
|
31
|
+
.leftJoinAndMapOne('recipe.selectedRevision', recipe_revision_1.RecipeRevision, 'revision', 'revision.id = segment.revision_id');
|
|
32
|
+
const [rows, total] = await query.getManyAndCount();
|
|
33
|
+
const revisionIds = rows.flatMap(row => row.selectedRevision ? [row.selectedRevision.id] : []);
|
|
34
|
+
const [inputs, outputs] = revisionIds.length ? await Promise.all([
|
|
35
|
+
manager.find(recipe_revision_line_1.RecipeRevisionInput, { where: { domain: { id: domain.id }, revision: { id: (0, typeorm_1.In)(revisionIds) } }, order: { position: 'ASC' } }),
|
|
36
|
+
manager.find(recipe_revision_line_1.RecipeRevisionOutput, { where: { domain: { id: domain.id }, revision: { id: (0, typeorm_1.In)(revisionIds) } }, order: { position: 'ASC' } })
|
|
37
|
+
]) : [[], []];
|
|
38
|
+
const group = (lines) => {
|
|
39
|
+
const result = new Map();
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const bucket = result.get(line.revisionId) ?? [];
|
|
42
|
+
bucket.push({ lineKey: line.lineKey, material: line.material, qty: line.qty,
|
|
43
|
+
...('operation' in line && line.operation ? { operation: line.operation } : {}) });
|
|
44
|
+
result.set(line.revisionId, bucket);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
const inputsByRevision = group(inputs), outputsByRevision = group(outputs);
|
|
49
|
+
const items = rows.map(row => {
|
|
50
|
+
const { selectedRevision, selectedSegment, ...identity } = row;
|
|
51
|
+
return Object.assign(new recipe_definition_1.RecipeDefinition(), identity, {
|
|
52
|
+
description: selectedRevision?.description ?? null,
|
|
53
|
+
revisionNumber: selectedRevision?.revisionNumber ?? null,
|
|
54
|
+
materialDefinitionName: selectedRevision?.materialDefinitionName ?? null,
|
|
55
|
+
route: selectedRevision?.route ?? null,
|
|
56
|
+
inputs: inputsByRevision.get(selectedRevision?.id) ?? [],
|
|
57
|
+
outputs: outputsByRevision.get(selectedRevision?.id) ?? [],
|
|
58
|
+
effectiveStart: selectedSegment?.validFrom ?? null,
|
|
59
|
+
effectiveEnd: selectedSegment?.validTo ?? null
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
return { items, total };
|
|
63
|
+
}
|
|
64
|
+
/** Candidate selection retains future/expired declarations so callers can explain why a choice is unavailable.
|
|
65
|
+
* One joined read; no per-order or per-recipe resolution queries. Payload rows are not loaded here.
|
|
66
|
+
*/
|
|
67
|
+
async function recipeChoicesAt(manager, domainId, materialNames, at = new Date()) {
|
|
68
|
+
if (!materialNames.length)
|
|
69
|
+
return [];
|
|
70
|
+
const rows = await manager.createQueryBuilder(recipe_definition_1.RecipeDefinition, 'recipe')
|
|
71
|
+
.innerJoin(recipe_revision_1.RecipePlan, 'plan', 'plan.recipe_id = recipe.id AND plan.domain_id = recipe.domain_id AND plan.generation = recipe.plan_generation')
|
|
72
|
+
.innerJoin(recipe_revision_1.RecipeSegment, 'segment', 'segment.plan_id = plan.id')
|
|
73
|
+
.innerJoin(recipe_revision_1.RecipeRevision, 'revision', 'revision.id = segment.revision_id')
|
|
74
|
+
.where('recipe.domain_id = :domainId', { domainId })
|
|
75
|
+
.select('recipe.id', 'id').addSelect('recipe.name', 'name')
|
|
76
|
+
.addSelect('revision.materialDefinitionName', 'materialDefinitionName').addSelect('revision.route', 'route')
|
|
77
|
+
.addSelect('segment.validFrom', 'validFrom').addSelect('segment.validTo', 'validTo')
|
|
78
|
+
.getRawMany();
|
|
79
|
+
const byRecipe = new Map();
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
// Driver raw dates may be SQLite's UTC "YYYY-MM-DD HH:mm:ss.sss" representation.
|
|
82
|
+
const date = (value) => value instanceof Date ? value : new Date(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : value.replace(' ', 'T') + 'Z');
|
|
83
|
+
row.effectiveStart = date(row.validFrom);
|
|
84
|
+
row.effectiveEnd = row.validTo ? date(row.validTo) : undefined;
|
|
85
|
+
const group = byRecipe.get(row.id) ?? [];
|
|
86
|
+
group.push(row);
|
|
87
|
+
byRecipe.set(row.id, group);
|
|
88
|
+
}
|
|
89
|
+
const rank = (row) => row.effectiveStart > at ? 1 : row.effectiveEnd && row.effectiveEnd <= at ? 2 : 0;
|
|
90
|
+
return [...byRecipe.values()].map(group => group.sort((a, b) => rank(a) - rank(b) ||
|
|
91
|
+
(rank(a) === 1 ? a.effectiveStart - b.effectiveStart : b.effectiveStart - a.effectiveStart))[0])
|
|
92
|
+
.filter(row => materialNames.includes(row.materialDefinitionName))
|
|
93
|
+
.map(row => ({ id: row.id, name: row.name, materialDefinitionName: row.materialDefinitionName,
|
|
94
|
+
route: row.route, effectiveStart: row.effectiveStart, effectiveEnd: row.effectiveEnd }));
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=recipe-effective-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-effective-list.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-effective-list.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\nimport { Domain, getQueryBuilderFromListParams, ListParam } from '@things-factory/shell'\nimport { RecipeDefinition } from './recipe-definition'\nimport { RecipePlan, RecipeRevision, RecipeSegment } from './recipe-revision'\n\nimport { RecipeRevisionInput, RecipeRevisionOutput } from './recipe-revision-line'\n\ntype EffectiveRow = RecipeDefinition & {\n selectedRevision?: RecipeRevision\n selectedSegment?: RecipeSegment\n}\n\n/** Filter, sort, paginate and display the same time projection. No per-row resolution queries. */\nexport async function listEffectiveRecipes(\n manager: EntityManager,\n domain: Domain,\n params: ListParam,\n at = new Date()\n) {\n const query = getQueryBuilderFromListParams({\n repository: manager.getRepository(RecipeDefinition),\n domain,\n params,\n alias: 'recipe',\n searchables: ['name', 'description'],\n sortables: ['id', 'name', 'revisionNumber', 'description', 'effectiveStart', 'effectiveEnd', 'createdAt', 'updatedAt'],\n columnExpressions: {\n revisionNumber: 'revision.revisionNumber',\n description: 'revision.description',\n materialDefinitionName: 'revision.materialDefinitionName',\n route: 'revision.route',\n effectiveStart: 'segment.validFrom',\n effectiveEnd: 'segment.validTo'\n }\n })\n query\n .leftJoin(\n RecipePlan,\n 'plan',\n 'plan.recipe_id = recipe.id AND plan.domain_id = recipe.domain_id AND plan.generation = recipe.plan_generation'\n )\n .leftJoinAndMapOne(\n 'recipe.selectedSegment',\n RecipeSegment,\n 'segment',\n 'segment.plan_id = plan.id AND segment.validFrom <= :at AND (segment.validTo IS NULL OR segment.validTo > :at)',\n { at }\n )\n .leftJoinAndMapOne(\n 'recipe.selectedRevision',\n RecipeRevision,\n 'revision',\n 'revision.id = segment.revision_id'\n )\n const [rows, total] = await query.getManyAndCount()\n const revisionIds = (rows as EffectiveRow[]).flatMap(row => row.selectedRevision ? [row.selectedRevision.id] : [])\n const [inputs, outputs] = revisionIds.length ? await Promise.all([\n manager.find(RecipeRevisionInput, { where: { domain: { id: domain.id }, revision: { id: In(revisionIds) } }, order: { position: 'ASC' } }),\n manager.find(RecipeRevisionOutput, { where: { domain: { id: domain.id }, revision: { id: In(revisionIds) } }, order: { position: 'ASC' } })\n ]) : [[], []]\n const group = (lines: (RecipeRevisionInput | RecipeRevisionOutput)[]) => {\n const result = new Map<string, Record<string, unknown>[]>()\n for (const line of lines) {\n const bucket = result.get(line.revisionId) ?? []\n bucket.push({ lineKey: line.lineKey, material: line.material, qty: line.qty,\n ...('operation' in line && line.operation ? { operation: line.operation } : {}) })\n result.set(line.revisionId, bucket)\n }\n return result\n }\n const inputsByRevision = group(inputs), outputsByRevision = group(outputs)\n const items = (rows as EffectiveRow[]).map(row => {\n const { selectedRevision, selectedSegment, ...identity } = row\n return Object.assign(new RecipeDefinition(), identity, {\n description: selectedRevision?.description ?? null,\n revisionNumber: selectedRevision?.revisionNumber ?? null,\n materialDefinitionName: selectedRevision?.materialDefinitionName ?? null,\n route: selectedRevision?.route ?? null,\n inputs: inputsByRevision.get(selectedRevision?.id) ?? [],\n outputs: outputsByRevision.get(selectedRevision?.id) ?? [],\n effectiveStart: selectedSegment?.validFrom ?? null,\n effectiveEnd: selectedSegment?.validTo ?? null\n })\n })\n return { items, total }\n}\n\n/** Candidate selection retains future/expired declarations so callers can explain why a choice is unavailable.\n * One joined read; no per-order or per-recipe resolution queries. Payload rows are not loaded here.\n */\nexport async function recipeChoicesAt(manager: EntityManager, domainId: string, materialNames: string[], at = new Date()) {\n if (!materialNames.length) return []\n const rows = await manager.createQueryBuilder(RecipeDefinition, 'recipe')\n .innerJoin(RecipePlan, 'plan', 'plan.recipe_id = recipe.id AND plan.domain_id = recipe.domain_id AND plan.generation = recipe.plan_generation')\n .innerJoin(RecipeSegment, 'segment', 'segment.plan_id = plan.id')\n .innerJoin(RecipeRevision, 'revision', 'revision.id = segment.revision_id')\n .where('recipe.domain_id = :domainId', { domainId })\n .select('recipe.id', 'id').addSelect('recipe.name', 'name')\n .addSelect('revision.materialDefinitionName', 'materialDefinitionName').addSelect('revision.route', 'route')\n .addSelect('segment.validFrom', 'validFrom').addSelect('segment.validTo', 'validTo')\n .getRawMany()\n const byRecipe = new Map<string, typeof rows>()\n for (const row of rows) {\n // Driver raw dates may be SQLite's UTC \"YYYY-MM-DD HH:mm:ss.sss\" representation.\n const date = (value: string | Date) => value instanceof Date ? value : new Date(/(?:Z|[+-]\\d\\d:\\d\\d)$/.test(value) ? value : value.replace(' ', 'T') + 'Z')\n row.effectiveStart = date(row.validFrom)\n row.effectiveEnd = row.validTo ? date(row.validTo) : undefined\n const group = byRecipe.get(row.id) ?? []\n group.push(row); byRecipe.set(row.id, group)\n }\n const rank = (row: any) => row.effectiveStart > at ? 1 : row.effectiveEnd && row.effectiveEnd <= at ? 2 : 0\n return [...byRecipe.values()].map(group => group.sort((a,b) => rank(a)-rank(b) ||\n (rank(a) === 1 ? a.effectiveStart-b.effectiveStart : b.effectiveStart-a.effectiveStart))[0])\n .filter(row => materialNames.includes(row.materialDefinitionName))\n .map(row => ({ id: row.id as string, name: row.name as string, materialDefinitionName: row.materialDefinitionName as string,\n route: row.route as string | undefined, effectiveStart: row.effectiveStart as Date, effectiveEnd: row.effectiveEnd as Date | undefined }))\n}\n"]}
|
|
@@ -8,6 +8,8 @@ const master_page_query_1 = require("../master-page-query");
|
|
|
8
8
|
const recipe_definition_1 = require("./recipe-definition");
|
|
9
9
|
const route_definition_1 = require("./route-definition");
|
|
10
10
|
const recipe_master_1 = require("./recipe-master");
|
|
11
|
+
const recipe_effective_list_1 = require("./recipe-effective-list");
|
|
12
|
+
const master_page_1 = require("../master-page");
|
|
11
13
|
let RecipeMaster = class RecipeMaster {
|
|
12
14
|
};
|
|
13
15
|
exports.RecipeMaster = RecipeMaster;
|
|
@@ -45,10 +47,15 @@ let RecipeQuery = class RecipeQuery {
|
|
|
45
47
|
* 레시피가 먼저 도착하고 라우팅이 나중에 오는 순간이 생긴다 — 이 파일이 원래 「따로 물으면 레시피가
|
|
46
48
|
* 없는 라우팅을 가리키는 순간이 생긴다」고 적어 둔 그 이유다.
|
|
47
49
|
*/
|
|
48
|
-
const [
|
|
49
|
-
(0,
|
|
50
|
+
const [effective, routes] = await Promise.all([
|
|
51
|
+
(0, recipe_effective_list_1.listEffectiveRecipes)((0, shell_1.getRepository)(recipe_definition_1.RecipeDefinition).manager, domain, {
|
|
52
|
+
filters: [...(after ? [{ name: 'name', operator: 'gt', value: after }] : []),
|
|
53
|
+
{ name: 'materialDefinitionName', operator: 'is_not_null', value: true }],
|
|
54
|
+
sortings: [{ name: 'name', desc: false }], pagination: { limit: (0, master_page_1.takeFor)(request), page: 1 }
|
|
55
|
+
}),
|
|
50
56
|
(0, shell_1.getRepository)(route_definition_1.RouteDefinition).find({ where: { domain: { id: domain.id } }, order: { name: 'ASC' } })
|
|
51
57
|
]);
|
|
58
|
+
const page = (0, master_page_1.pageOf)(effective.items, request, row => row.name);
|
|
52
59
|
/* 통째로 달라고 했는데 한도를 넘으면 **자르지 않고 거절한다**(§`master-page`). */
|
|
53
60
|
(0, master_page_query_1.refuseIfTooLarge)('레시피', request, page);
|
|
54
61
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipe-query.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-query.ts"],"names":[],"mappings":";;;;AAAA,+CAAgF;AAEhF,iDAAqD;AAErD,
|
|
1
|
+
{"version":3,"file":"recipe-query.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Field, Int, ObjectType, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { refuseIfTooLarge } from '../master-page-query'\n\nimport { RecipeDefinition } from './recipe-definition'\nimport { RouteDefinition } from './route-definition'\nimport { toRecipeEntry, toRouteEntry } from './recipe-master'\nimport { listEffectiveRecipes } from './recipe-effective-list'\nimport { pageOf, takeFor } from '../master-page'\n\n@ObjectType({ description: 'Recipes and routes as the twin receives them' })\nexport class RecipeMaster {\n @Field(type => String, { description: 'RecipeDef[] as JSON text' })\n recipes: string\n\n @Field(type => String, { description: 'RouteDef[] as JSON text' })\n routes: string\n\n /**\n * 더 있나 — **이 값을 안 보고 쓰면 조용히 잘린 마스터로 트윈을 세우게 된다.**\n *\n * 거짓이 될 때까지 `next` 를 `after` 로 넘겨 이어 받는다.\n */\n @Field({ description: 'true when more rows remain — keep pulling with next as after' })\n hasMore: boolean\n\n /** 이어 받을 자리. 더 없으면 없다. */\n @Field(type => String, { nullable: true, description: 'cursor for the next page — the last name in this page' })\n next?: string\n\n}\n\n@Resolver(RecipeDefinition)\nexport class RecipeQuery {\n /**\n * 레시피와 라우팅을 **한 질의로** 낸다.\n *\n * 따로 물으면 그 사이에 마스터가 바뀌어 레시피가 없는 라우팅을 가리키는 순간이 생긴다. 구조는\n * 서로를 가리키므로 통째로 맞아야 한다 — 마스터를 당겨 가는 다른 축과 같은 규율이다.\n */\n @Query(returns => RecipeMaster, { description: 'Recipes and routes for the twin to pull' })\n async recipeMaster(\n @Arg('after', { nullable: true, description: 'resume after this name — the previous page\\'s next' }) after: string | undefined,\n @Arg('limit', type => Int, { nullable: true, description: 'rows per page — omit only when the axis is small enough to pull whole' }) limit: number | undefined,\n @Ctx() context: ResolverContext\n ): Promise<RecipeMaster> {\n const { domain } = context.state\n\n const request = { after, limit }\n\n /*\n * **라우팅은 통째로, 레시피만 나눈다.** 레시피가 라우팅을 이름으로 가리키므로, 둘을 함께 나누면\n * 레시피가 먼저 도착하고 라우팅이 나중에 오는 순간이 생긴다 — 이 파일이 원래 「따로 물으면 레시피가\n * 없는 라우팅을 가리키는 순간이 생긴다」고 적어 둔 그 이유다.\n */\n const [effective, routes] = await Promise.all([\n listEffectiveRecipes(getRepository(RecipeDefinition).manager, domain, {\n filters: [...(after ? [{ name: 'name', operator: 'gt', value: after }] : []),\n { name: 'materialDefinitionName', operator: 'is_not_null', value: true }],\n sortings: [{ name: 'name', desc: false }], pagination: { limit: takeFor(request), page: 1 }\n } as any),\n getRepository(RouteDefinition).find({ where: { domain: { id: domain.id } }, order: { name: 'ASC' } })\n ])\n const page = pageOf(effective.items, request, row => row.name)\n\n /* 통째로 달라고 했는데 한도를 넘으면 **자르지 않고 거절한다**(§`master-page`). */\n refuseIfTooLarge('레시피', request, page)\n\n return {\n recipes: JSON.stringify(page.rows.map(toRecipeEntry)),\n routes: JSON.stringify(routes.map(toRouteEntry)),\n hasMore: page.hasMore,\n ...(page.next ? { next: page.next } : {})\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RecipeLineContent {
|
|
2
|
+
lineKey: string;
|
|
3
|
+
material: string;
|
|
4
|
+
qty: number;
|
|
5
|
+
operation?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface RecipeDraftContent {
|
|
8
|
+
materialDefinitionName: string;
|
|
9
|
+
description?: string | null;
|
|
10
|
+
route?: string | null;
|
|
11
|
+
inputs: RecipeLineContent[];
|
|
12
|
+
outputs: RecipeLineContent[];
|
|
13
|
+
effectiveFrom: Date | string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
/** Stable signed content excludes DB row IDs; array order is meaningful. */
|
|
17
|
+
export declare function recipeContentValues(input: Pick<RecipeDraftContent, 'materialDefinitionName' | 'description' | 'route' | 'inputs' | 'outputs'>): {
|
|
18
|
+
materialDefinitionName: string;
|
|
19
|
+
description: string;
|
|
20
|
+
route: string;
|
|
21
|
+
inputs: RecipeLineContent[];
|
|
22
|
+
outputs: RecipeLineContent[];
|
|
23
|
+
};
|
|
24
|
+
export declare function readRecipeContent(input: RecipeDraftContent): {
|
|
25
|
+
effectiveFrom: Date;
|
|
26
|
+
reason: string;
|
|
27
|
+
materialDefinitionName: string;
|
|
28
|
+
description: string;
|
|
29
|
+
route: string;
|
|
30
|
+
inputs: RecipeLineContent[];
|
|
31
|
+
outputs: RecipeLineContent[];
|
|
32
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recipeContentValues = recipeContentValues;
|
|
4
|
+
exports.readRecipeContent = readRecipeContent;
|
|
5
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
|
+
function invalid(field) {
|
|
7
|
+
throw new shell_1.Refusal('INVALID_RECIPE_CONTENT', { field }, `Invalid recipe content: ${field}`);
|
|
8
|
+
}
|
|
9
|
+
function required(value, field) {
|
|
10
|
+
if (typeof value !== 'string' || !value.trim())
|
|
11
|
+
invalid(field);
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
function lines(value, side) {
|
|
15
|
+
if (!Array.isArray(value) || !value.length)
|
|
16
|
+
invalid(side);
|
|
17
|
+
const keys = new Set();
|
|
18
|
+
return value.map((line, index) => {
|
|
19
|
+
if (!line || typeof line !== 'object')
|
|
20
|
+
invalid(`${side}.${index}`);
|
|
21
|
+
const lineKey = required(line.lineKey, `${side}.${index}.lineKey`);
|
|
22
|
+
if (keys.has(lineKey))
|
|
23
|
+
invalid(`${side}.duplicateLineKey`);
|
|
24
|
+
keys.add(lineKey);
|
|
25
|
+
if (typeof line.qty !== 'number' || !Number.isFinite(line.qty) || line.qty <= 0)
|
|
26
|
+
invalid(`${side}.${index}.qty`);
|
|
27
|
+
if (side === 'outputs' && line.operation != null)
|
|
28
|
+
invalid('outputs.operation');
|
|
29
|
+
return {
|
|
30
|
+
lineKey,
|
|
31
|
+
material: required(line.material, `${side}.${index}.material`),
|
|
32
|
+
qty: line.qty,
|
|
33
|
+
...(side === 'inputs' ? { operation: line.operation == null ? null : required(line.operation, 'operation') } : {})
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Stable signed content excludes DB row IDs; array order is meaningful. */
|
|
38
|
+
function recipeContentValues(input) {
|
|
39
|
+
const materialDefinitionName = required(input.materialDefinitionName, 'materialDefinitionName');
|
|
40
|
+
if (input.description != null && typeof input.description !== 'string')
|
|
41
|
+
invalid('description');
|
|
42
|
+
const inputs = lines(input.inputs, 'inputs');
|
|
43
|
+
const outputs = lines(input.outputs, 'outputs');
|
|
44
|
+
if (!outputs.some(line => line.material === materialDefinitionName))
|
|
45
|
+
invalid('primaryOutputMissing');
|
|
46
|
+
return {
|
|
47
|
+
materialDefinitionName,
|
|
48
|
+
description: input.description ?? null,
|
|
49
|
+
route: input.route == null ? null : required(input.route, 'route'),
|
|
50
|
+
inputs,
|
|
51
|
+
outputs
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readRecipeContent(input) {
|
|
55
|
+
if (!input || typeof input !== 'object')
|
|
56
|
+
invalid('content');
|
|
57
|
+
const from = input.effectiveFrom;
|
|
58
|
+
if (!(from instanceof Date) && (typeof from !== 'string' || !/(Z|[+-]\d{2}:\d{2})$/.test(from)))
|
|
59
|
+
invalid('effectiveFrom');
|
|
60
|
+
const effectiveFrom = new Date(from);
|
|
61
|
+
if (!Number.isFinite(effectiveFrom.getTime()))
|
|
62
|
+
invalid('effectiveFrom');
|
|
63
|
+
return { ...recipeContentValues(input), effectiveFrom, reason: required(input.reason, 'reason') };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=recipe-revision-content.js.map
|