@things-factory/board-service 10.1.5 → 10.1.7

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.
@@ -11,7 +11,7 @@ export declare class BoardQuery {
11
11
  boardByName(name: string, context: ResolverContext): Promise<Board>;
12
12
  boardsCreatedByMe(params: ListParam, context: ResolverContext): Promise<BoardList>;
13
13
  boardVersions(id: string, context: ResolverContext): Promise<BoardHistory[]>;
14
- boardPublished(id: string, context: ResolverContext): Promise<BoardHistory>;
14
+ boardPublished(id: string, context: ResolverContext): Promise<BoardHistory | null>;
15
15
  boardVersionDiff(id: string, version: number, context: ResolverContext, toVersion?: number): Promise<BoardVersionDiff>;
16
16
  boards(params: ListParam, context: ResolverContext): Promise<BoardList>;
17
17
  boardsUpdatedSince(since: Date, context: ResolverContext): Promise<Board[]>;
@@ -81,6 +81,19 @@ let BoardQuery = class BoardQuery {
81
81
  take: 10
82
82
  });
83
83
  }
84
+ /*
85
+ * Nullable, because "nobody has published this board yet" is an ordinary state (2026-09-11).
86
+ *
87
+ * This was declared non-null while the implementation returns whatever findOne finds, so a board
88
+ * that exists but has never been published answered with
89
+ * `Cannot return null for non-nullable field Query.boardPublished` and code INTERNAL_SERVER_ERROR.
90
+ * Two callers reported it independently: operato-plant's station board page, which has to catch
91
+ * that error and ask a second question to tell "not published yet" apart from "no such board",
92
+ * and its station scenario tool, which asserts a draft cannot reach the shop floor.
93
+ *
94
+ * A caller cannot tell an unpublished board from a broken server, and the server logs an error
95
+ * for a normal request. Answering null says the true thing.
96
+ */
84
97
  async boardPublished(id, context) {
85
98
  const { domain } = context.state;
86
99
  return await (0, shell_1.getRepository)(board_history_js_1.BoardHistory).findOne({
@@ -223,7 +236,10 @@ tslib_1.__decorate([
223
236
  tslib_1.__metadata("design:returntype", Promise)
224
237
  ], BoardQuery.prototype, "boardVersions", null);
225
238
  tslib_1.__decorate([
226
- (0, type_graphql_1.Query)(returns => board_history_js_1.BoardHistory, { description: 'Retrieves the most recently published version of a board.' }),
239
+ (0, type_graphql_1.Query)(returns => board_history_js_1.BoardHistory, {
240
+ nullable: true,
241
+ description: 'Retrieves the most recently published version of a board, or null if none was published.'
242
+ }),
227
243
  (0, type_graphql_1.Directive)('@privilege(category: "board", privilege: "query", domainOwnerGranted: true)'),
228
244
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
229
245
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
@@ -1 +1 @@
1
- {"version":3,"file":"board-query.js","sourceRoot":"","sources":["../../../server/service/board/board-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { In, MoreThan } from 'typeorm'\nimport { Arg, Args, Ctx, FieldResolver, Int, Query, Resolver, Root, Directive } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\nimport { checkTarget, checkDomain, getPermission } from '@things-factory/operato-license-checker'\n\nimport { Group } from '../group/group.js'\nimport { PlayGroup } from '../play-group/play-group.js'\nimport { Board } from './board.js'\nimport { BoardList } from './board-type.js'\nimport { BoardHistory } from './board-history.js'\nimport { BoardVersionDiff } from './board-version-type.js'\nimport { diffBoardModels } from './board-version-diff.js'\n\nimport { LicenseError } from '../../errors/license-error.js'\n@Resolver(Board)\nexport class BoardQuery {\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => Board, { description: 'Finds a single board by its ID. If cachedUpdatedAt matches, model is omitted.' })\n async board(\n @Arg('id') id: string,\n @Arg('cachedUpdatedAt', { nullable: true, description: 'Client cache timestamp — if matches, model field is omitted' }) cachedUpdatedAt: string,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain } = context.state\n\n var board = await getRepository(Board).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }\n })\n\n // 캐시 유효성 체크: updatedAt이 동일하면 model을 생략하여 전송량 절감\n if (board && cachedUpdatedAt && board.updatedAt?.toISOString() === cachedUpdatedAt) {\n board.model = null\n }\n\n if (domain) {\n // 1. check domainSecret is over limit quantity or not\n var count = await getRepository(Domain)\n .manager.createQueryBuilder()\n .select('id')\n .from(Domain, 'domain')\n .getCount()\n\n if (!checkDomain(count)) {\n throw new LicenseError({\n errorCode: context.t(LicenseError.ERROR_CODES.OVER_LIMIT)\n })\n }\n }\n\n if (board) {\n // 1. check boardSecret is over limit quantity or not\n var count = await getRepository(Board)\n .manager.createQueryBuilder()\n .select('id')\n .from(Board, 'board')\n .where('\"domain_id\" = :domainId', { domainId: domain.id })\n .getCount()\n\n if (!checkTarget(count)) {\n throw new LicenseError({\n errorCode: context.t(LicenseError.ERROR_CODES.OVER_LIMIT)\n })\n }\n }\n\n return board\n }\n\n @Query(returns => Board, { nullable: true, description: 'Finds a single board by its name.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardByName(@Arg('name') name: string, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain } = context.state\n\n return await getRepository(Board).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, name }\n })\n }\n\n @Query(returns => BoardList, { description: 'Retrieves a paginated list of boards created by the current user.' })\n async boardsCreatedByMe(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<BoardList> {\n const { domain, user } = context.state\n\n const queryBuilder = getQueryBuilderFromListParams({\n repository: getRepository(Board),\n params,\n domain,\n alias: 'board',\n searchables: ['name', 'description']\n }).andWhere('board.creator = :user', { user: user.id })\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Query(returns => [BoardHistory], { description: 'Retrieves up to 10 historical versions of a specific board.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardVersions(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<BoardHistory[]> {\n const { domain } = context.state\n\n return await getRepository(BoardHistory).find({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },\n relations: ['updater'],\n order: { version: 'DESC' },\n take: 10\n })\n }\n\n @Query(returns => BoardHistory, { description: 'Retrieves the most recently published version of a board.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardPublished(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<BoardHistory> {\n const { domain } = context.state\n\n return await getRepository(BoardHistory).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },\n order: { version: 'DESC' }\n })\n }\n\n @Query(returns => BoardVersionDiff, {\n description:\n 'Computes the component-level difference between a board version and another version or the current draft. ' +\n 'Omit toVersion to diff the base version against the current draft.'\n })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardVersionDiff(\n @Arg('id') id: string,\n @Arg('version', type => Int) version: number,\n @Ctx() context: ResolverContext,\n @Arg('toVersion', type => Int, { nullable: true }) toVersion?: number\n ): Promise<BoardVersionDiff> {\n const { domain } = context.state\n const domainIds = [domain.id, domain.parentId].filter(Boolean)\n const historyRepo = getRepository(BoardHistory)\n\n const base = await historyRepo.findOne({\n where: { domain: { id: In(domainIds) }, originalId: id, version }\n })\n if (!base) {\n throw new Error(`Board version (${id}:${version}) is not found`)\n }\n\n let targetModel: any\n let targetVersion: number | null = null\n if (toVersion !== undefined && toVersion !== null) {\n const target = await historyRepo.findOne({\n where: { domain: { id: In(domainIds) }, originalId: id, version: toVersion }\n })\n if (!target) {\n throw new Error(`Board version (${id}:${toVersion}) is not found`)\n }\n targetModel = target.model\n targetVersion = toVersion\n } else {\n const board = await getRepository(Board).findOne({\n where: { domain: { id: In(domainIds) }, id }\n })\n if (!board) {\n throw new Error(`Board (${id}) is not found`)\n }\n targetModel = board.model\n }\n\n const componentDiffs = diffBoardModels(base.model, targetModel)\n return {\n fromVersion: version,\n toVersion: targetVersion,\n addedCount: componentDiffs.filter(d => d.changeType === 'added').length,\n removedCount: componentDiffs.filter(d => d.changeType === 'removed').length,\n modifiedCount: componentDiffs.filter(d => d.changeType === 'modified').length,\n componentDiffs\n }\n }\n\n @Query(returns => BoardList, { description: 'Retrieves a paginated list of all boards.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boards(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<BoardList> {\n const { domain } = context.state\n\n const queryBuilder = getQueryBuilderFromListParams({\n repository: getRepository(Board),\n params,\n domain,\n searchables: ['name', 'description']\n })\n\n // 클라이언트 정렬 지정이 없으면 sortOrder 기본 정렬\n if (!params.sortings?.length) {\n queryBuilder.addOrderBy(`\"${queryBuilder.alias}\".\"sort_order\"`, 'ASC', 'NULLS LAST')\n queryBuilder.addOrderBy(`\"${queryBuilder.alias}\".\"created_at\"`, 'ASC')\n }\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Query(returns => [Board], {\n description: 'Retrieves boards that have been updated or soft-deleted since the given timestamp.'\n })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardsUpdatedSince(\n @Arg('since') since: Date,\n @Ctx() context: ResolverContext\n ): Promise<Board[]> {\n const { domain } = context.state\n\n return await getRepository(Board)\n .createQueryBuilder('board')\n .withDeleted()\n .where('board.domain_id = :domainId', { domainId: domain.id })\n .andWhere('board.updated_at > :since', { since })\n .getMany()\n }\n\n @FieldResolver(type => Group, { description: 'Resolves the group that the board belongs to.' })\n async group(@Root() board: Board) {\n return (\n board.groupId &&\n (await getRepository(Group).findOneBy({\n id: board.groupId\n }))\n )\n }\n\n @FieldResolver(type => [PlayGroup], {\n description: 'Resolves the list of play groups that this board is a member of.'\n })\n async playGroups(@Root() board: Board) {\n return (\n await getRepository(Board).findOne({\n where: { id: board.id },\n relations: ['playGroups']\n })\n )?.playGroups\n }\n\n @FieldResolver(type => Domain, { description: 'Resolves the domain associated with the board.' })\n async domain(@Root() board: Board) {\n return (\n board.domainId &&\n (await getRepository(Domain).findOneBy({\n id: board.domainId\n }))\n )\n }\n\n @FieldResolver(type => User, { description: 'Resolves the user who last updated the board.' })\n async updater(@Root() board: Board): Promise<User> {\n return (\n board.updaterId &&\n (await getRepository(User).findOneBy({\n id: board.updaterId\n }))\n )\n }\n\n @FieldResolver(type => User, { description: 'Resolves the user who created the board.' })\n async creator(@Root() board: Board): Promise<User> {\n return (\n board.creatorId &&\n (await getRepository(User).findOneBy({\n id: board.creatorId\n }))\n )\n }\n}\n"]}
1
+ {"version":3,"file":"board-query.js","sourceRoot":"","sources":["../../../server/service/board/board-query.ts"],"names":[],"mappings":";;;;AAAA,qCAAsC;AACtC,+CAAmG;AAEnG,yDAAgD;AAChD,iDAAuG;AACvG,qFAAiG;AAEjG,gDAAyC;AACzC,+DAAuD;AACvD,yCAAkC;AAClC,mDAA2C;AAC3C,yDAAiD;AACjD,mEAA0D;AAC1D,mEAAyD;AAEzD,oEAA4D;AAErD,IAAM,UAAU,GAAhB,MAAM,UAAU;IAGf,AAAN,KAAK,CAAC,KAAK,CACE,EAAU,EACmG,eAAuB,EACxI,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,IAAI,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC,CAAC,OAAO,CAAC;YAC7C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;SAChF,CAAC,CAAA;QAEF,gDAAgD;QAChD,IAAI,KAAK,IAAI,eAAe,IAAI,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,CAAC;YACnF,KAAK,CAAC,KAAK,GAAG,IAAI,CAAA;QACpB,CAAC;QAED,IAAI,MAAM,EAAE,CAAC;YACX,sDAAsD;YACtD,IAAI,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC;iBACpC,OAAO,CAAC,kBAAkB,EAAE;iBAC5B,MAAM,CAAC,IAAI,CAAC;iBACZ,IAAI,CAAC,cAAM,EAAE,QAAQ,CAAC;iBACtB,QAAQ,EAAE,CAAA;YAEb,IAAI,CAAC,IAAA,qCAAW,EAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,+BAAY,CAAC;oBACrB,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,+BAAY,CAAC,WAAW,CAAC,UAAU,CAAC;iBAC1D,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,qDAAqD;YACrD,IAAI,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC;iBACnC,OAAO,CAAC,kBAAkB,EAAE;iBAC5B,MAAM,CAAC,IAAI,CAAC;iBACZ,IAAI,CAAC,gBAAK,EAAE,OAAO,CAAC;iBACpB,KAAK,CAAC,yBAAyB,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;iBACzD,QAAQ,EAAE,CAAA;YAEb,IAAI,CAAC,IAAA,qCAAW,EAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,+BAAY,CAAC;oBACrB,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,+BAAY,CAAC,WAAW,CAAC,UAAU,CAAC;iBAC1D,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAA;IACd,CAAC;IAIK,AAAN,KAAK,CAAC,WAAW,CAAc,IAAY,EAAS,OAAwB;QAC1E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC,CAAC,OAAO,CAAC;YACxC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE;SAClF,CAAC,CAAA;IACJ,CAAC;IAGK,AAAN,KAAK,CAAC,iBAAiB,CACI,MAAiB,EACnC,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEtC,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,UAAU,EAAE,IAAA,qBAAa,EAAC,gBAAK,CAAC;YAChC,MAAM;YACN,MAAM;YACN,KAAK,EAAE,OAAO;YACd,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC;SACrC,CAAC,CAAC,QAAQ,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;QAEvD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAE3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAIK,AAAN,KAAK,CAAC,aAAa,CAAY,EAAU,EAAS,OAAwB;QACxE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,IAAI,CAAC;YAC5C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;YAC3F,SAAS,EAAE,CAAC,SAAS,CAAC;YACtB,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;YAC1B,IAAI,EAAE,EAAE;SACT,CAAC,CAAA;IACJ,CAAC;IAED;;;;;;;;;;;;OAYG;IAMG,AAAN,KAAK,CAAC,cAAc,CAAY,EAAU,EAAS,OAAwB;QACzE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,OAAO,CAAC;YAC/C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;YAC3F,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;SAC3B,CAAC,CAAA;IACJ,CAAC;IAQK,AAAN,KAAK,CAAC,gBAAgB,CACT,EAAU,EACQ,OAAe,EACrC,OAAwB,EACoB,SAAkB;QAErE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAChC,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC9D,MAAM,WAAW,GAAG,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAA;QAE/C,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC;YACrC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE;SAClE,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,kBAAkB,EAAE,IAAI,OAAO,gBAAgB,CAAC,CAAA;QAClE,CAAC;QAED,IAAI,WAAgB,CAAA;QACpB,IAAI,aAAa,GAAkB,IAAI,CAAA;QACvC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YAClD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC;gBACvC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;aAC7E,CAAC,CAAA;YACF,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,kBAAkB,EAAE,IAAI,SAAS,gBAAgB,CAAC,CAAA;YACpE,CAAC;YACD,WAAW,GAAG,MAAM,CAAC,KAAK,CAAA;YAC1B,aAAa,GAAG,SAAS,CAAA;QAC3B,CAAC;aAAM,CAAC;YACN,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC,CAAC,OAAO,CAAC;gBAC/C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE;aAC7C,CAAC,CAAA;YACF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,KAAK,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;YAC/C,CAAC;YACD,WAAW,GAAG,KAAK,CAAC,KAAK,CAAA;QAC3B,CAAC;QAED,MAAM,cAAc,GAAG,IAAA,uCAAe,EAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;QAC/D,OAAO;YACL,WAAW,EAAE,OAAO;YACpB,SAAS,EAAE,aAAa;YACxB,UAAU,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,MAAM;YACvE,YAAY,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,MAAM;YAC3E,aAAa,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC,MAAM;YAC7E,cAAc;SACf,CAAA;IACH,CAAC;IAIK,AAAN,KAAK,CAAC,MAAM,CAA0B,MAAiB,EAAS,OAAwB;QACtF,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,UAAU,EAAE,IAAA,qBAAa,EAAC,gBAAK,CAAC;YAChC,MAAM;YACN,MAAM;YACN,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC;SACrC,CAAC,CAAA;QAEF,mCAAmC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;YAC7B,YAAY,CAAC,UAAU,CAAC,IAAI,YAAY,CAAC,KAAK,gBAAgB,EAAE,KAAK,EAAE,YAAY,CAAC,CAAA;YACpF,YAAY,CAAC,UAAU,CAAC,IAAI,YAAY,CAAC,KAAK,gBAAgB,EAAE,KAAK,CAAC,CAAA;QACxE,CAAC;QAED,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAE3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAMK,AAAN,KAAK,CAAC,kBAAkB,CACR,KAAW,EAClB,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC;aAC9B,kBAAkB,CAAC,OAAO,CAAC;aAC3B,WAAW,EAAE;aACb,KAAK,CAAC,6BAA6B,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;aAC7D,QAAQ,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,CAAC;aAChD,OAAO,EAAE,CAAA;IACd,CAAC;IAGK,AAAN,KAAK,CAAC,KAAK,CAAS,KAAY;QAC9B,OAAO,CACL,KAAK,CAAC,OAAO;YACb,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC,CAAC,SAAS,CAAC;gBACpC,EAAE,EAAE,KAAK,CAAC,OAAO;aAClB,CAAC,CAAC,CACJ,CAAA;IACH,CAAC;IAKK,AAAN,KAAK,CAAC,UAAU,CAAS,KAAY;QACnC,OAAO,CACL,MAAM,IAAA,qBAAa,EAAC,gBAAK,CAAC,CAAC,OAAO,CAAC;YACjC,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE;YACvB,SAAS,EAAE,CAAC,YAAY,CAAC;SAC1B,CAAC,CACH,EAAE,UAAU,CAAA;IACf,CAAC;IAGK,AAAN,KAAK,CAAC,MAAM,CAAS,KAAY;QAC/B,OAAO,CACL,KAAK,CAAC,QAAQ;YACd,CAAC,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,SAAS,CAAC;gBACrC,EAAE,EAAE,KAAK,CAAC,QAAQ;aACnB,CAAC,CAAC,CACJ,CAAA;IACH,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,KAAY;QAChC,OAAO,CACL,KAAK,CAAC,SAAS;YACf,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,SAAS,CAAC;gBACnC,EAAE,EAAE,KAAK,CAAC,SAAS;aACpB,CAAC,CAAC,CACJ,CAAA;IACH,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,KAAY;QAChC,OAAO,CACL,KAAK,CAAC,SAAS;YACf,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,SAAS,CAAC;gBACnC,EAAE,EAAE,KAAK,CAAC,SAAS;aACpB,CAAC,CAAC,CACJ,CAAA;IACH,CAAC;CACF,CAAA;AA9QY,gCAAU;AAGf;IAFL,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IACxF,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,gBAAK,EAAE,EAAE,WAAW,EAAE,+EAA+E,EAAE,CAAC;IAEvH,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IACT,mBAAA,IAAA,kBAAG,EAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC,CAAA;IACtH,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;uCA6CP;AAIK;IAFL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,gBAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mCAAmC,EAAE,CAAC;IAC7F,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IACtE,mBAAA,IAAA,kBAAG,EAAC,MAAM,CAAC,CAAA;IAAgB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;6CAMlD;AAGK;IADL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,yBAAS,EAAE,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;IAE/G,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IACvB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAD2B,iBAAS;;mDAgB3C;AAIK;IAFL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,+BAAY,CAAC,EAAE,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;IAChH,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IACpE,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;+CAShD;AAoBK;IALL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,+BAAY,EAAE;QAC9B,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,0FAA0F;KACxG,CAAC;IACD,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IACnE,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gDAOjD;AAQK;IANL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,wCAAgB,EAAE;QAClC,WAAW,EACT,4GAA4G;YAC5G,oEAAoE;KACvE,CAAC;IACD,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IAEtF,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IACT,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,CAAC,CAAA;IAC3B,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;kDA2CnD;AAIK;IAFL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,yBAAS,EAAE,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;IACzF,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IAC3E,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IAAqB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAAjB,iBAAS;;wCAmBtD;AAMK;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,gBAAK,CAAC,EAAE;QACzB,WAAW,EAAE,oFAAoF;KAClG,CAAC;IACD,IAAA,wBAAS,EAAC,6EAA6E,CAAC;IAEtF,mBAAA,IAAA,kBAAG,EAAC,OAAO,CAAC,CAAA;IACZ,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CADe,IAAI;;oDAW1B;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAK,EAAE,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;IAClF,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAQ,gBAAK;;uCAO/B;AAKK;IAHL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,yBAAS,CAAC,EAAE;QAClC,WAAW,EAAE,kEAAkE;KAChF,CAAC;IACgB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAQ,gBAAK;;4CAOpC;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;IACnF,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAQ,gBAAK;;wCAOhC;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;IAC/E,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAQ,gBAAK;;yCAOjC;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;IAC1E,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAQ,gBAAK;;yCAOjC;qBA7QU,UAAU;IADtB,IAAA,uBAAQ,EAAC,gBAAK,CAAC;GACH,UAAU,CA8QtB","sourcesContent":["import { In, MoreThan } from 'typeorm'\nimport { Arg, Args, Ctx, FieldResolver, Int, Query, Resolver, Root, Directive } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\nimport { checkTarget, checkDomain, getPermission } from '@things-factory/operato-license-checker'\n\nimport { Group } from '../group/group.js'\nimport { PlayGroup } from '../play-group/play-group.js'\nimport { Board } from './board.js'\nimport { BoardList } from './board-type.js'\nimport { BoardHistory } from './board-history.js'\nimport { BoardVersionDiff } from './board-version-type.js'\nimport { diffBoardModels } from './board-version-diff.js'\n\nimport { LicenseError } from '../../errors/license-error.js'\n@Resolver(Board)\nexport class BoardQuery {\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => Board, { description: 'Finds a single board by its ID. If cachedUpdatedAt matches, model is omitted.' })\n async board(\n @Arg('id') id: string,\n @Arg('cachedUpdatedAt', { nullable: true, description: 'Client cache timestamp — if matches, model field is omitted' }) cachedUpdatedAt: string,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain } = context.state\n\n var board = await getRepository(Board).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }\n })\n\n // 캐시 유효성 체크: updatedAt이 동일하면 model을 생략하여 전송량 절감\n if (board && cachedUpdatedAt && board.updatedAt?.toISOString() === cachedUpdatedAt) {\n board.model = null\n }\n\n if (domain) {\n // 1. check domainSecret is over limit quantity or not\n var count = await getRepository(Domain)\n .manager.createQueryBuilder()\n .select('id')\n .from(Domain, 'domain')\n .getCount()\n\n if (!checkDomain(count)) {\n throw new LicenseError({\n errorCode: context.t(LicenseError.ERROR_CODES.OVER_LIMIT)\n })\n }\n }\n\n if (board) {\n // 1. check boardSecret is over limit quantity or not\n var count = await getRepository(Board)\n .manager.createQueryBuilder()\n .select('id')\n .from(Board, 'board')\n .where('\"domain_id\" = :domainId', { domainId: domain.id })\n .getCount()\n\n if (!checkTarget(count)) {\n throw new LicenseError({\n errorCode: context.t(LicenseError.ERROR_CODES.OVER_LIMIT)\n })\n }\n }\n\n return board\n }\n\n @Query(returns => Board, { nullable: true, description: 'Finds a single board by its name.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardByName(@Arg('name') name: string, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain } = context.state\n\n return await getRepository(Board).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, name }\n })\n }\n\n @Query(returns => BoardList, { description: 'Retrieves a paginated list of boards created by the current user.' })\n async boardsCreatedByMe(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<BoardList> {\n const { domain, user } = context.state\n\n const queryBuilder = getQueryBuilderFromListParams({\n repository: getRepository(Board),\n params,\n domain,\n alias: 'board',\n searchables: ['name', 'description']\n }).andWhere('board.creator = :user', { user: user.id })\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Query(returns => [BoardHistory], { description: 'Retrieves up to 10 historical versions of a specific board.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardVersions(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<BoardHistory[]> {\n const { domain } = context.state\n\n return await getRepository(BoardHistory).find({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },\n relations: ['updater'],\n order: { version: 'DESC' },\n take: 10\n })\n }\n\n /*\n * Nullable, because \"nobody has published this board yet\" is an ordinary state (2026-09-11).\n *\n * This was declared non-null while the implementation returns whatever findOne finds, so a board\n * that exists but has never been published answered with\n * `Cannot return null for non-nullable field Query.boardPublished` and code INTERNAL_SERVER_ERROR.\n * Two callers reported it independently: operato-plant's station board page, which has to catch\n * that error and ask a second question to tell \"not published yet\" apart from \"no such board\",\n * and its station scenario tool, which asserts a draft cannot reach the shop floor.\n *\n * A caller cannot tell an unpublished board from a broken server, and the server logs an error\n * for a normal request. Answering null says the true thing.\n */\n @Query(returns => BoardHistory, {\n nullable: true,\n description: 'Retrieves the most recently published version of a board, or null if none was published.'\n })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardPublished(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<BoardHistory | null> {\n const { domain } = context.state\n\n return await getRepository(BoardHistory).findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id },\n order: { version: 'DESC' }\n })\n }\n\n @Query(returns => BoardVersionDiff, {\n description:\n 'Computes the component-level difference between a board version and another version or the current draft. ' +\n 'Omit toVersion to diff the base version against the current draft.'\n })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardVersionDiff(\n @Arg('id') id: string,\n @Arg('version', type => Int) version: number,\n @Ctx() context: ResolverContext,\n @Arg('toVersion', type => Int, { nullable: true }) toVersion?: number\n ): Promise<BoardVersionDiff> {\n const { domain } = context.state\n const domainIds = [domain.id, domain.parentId].filter(Boolean)\n const historyRepo = getRepository(BoardHistory)\n\n const base = await historyRepo.findOne({\n where: { domain: { id: In(domainIds) }, originalId: id, version }\n })\n if (!base) {\n throw new Error(`Board version (${id}:${version}) is not found`)\n }\n\n let targetModel: any\n let targetVersion: number | null = null\n if (toVersion !== undefined && toVersion !== null) {\n const target = await historyRepo.findOne({\n where: { domain: { id: In(domainIds) }, originalId: id, version: toVersion }\n })\n if (!target) {\n throw new Error(`Board version (${id}:${toVersion}) is not found`)\n }\n targetModel = target.model\n targetVersion = toVersion\n } else {\n const board = await getRepository(Board).findOne({\n where: { domain: { id: In(domainIds) }, id }\n })\n if (!board) {\n throw new Error(`Board (${id}) is not found`)\n }\n targetModel = board.model\n }\n\n const componentDiffs = diffBoardModels(base.model, targetModel)\n return {\n fromVersion: version,\n toVersion: targetVersion,\n addedCount: componentDiffs.filter(d => d.changeType === 'added').length,\n removedCount: componentDiffs.filter(d => d.changeType === 'removed').length,\n modifiedCount: componentDiffs.filter(d => d.changeType === 'modified').length,\n componentDiffs\n }\n }\n\n @Query(returns => BoardList, { description: 'Retrieves a paginated list of all boards.' })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boards(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<BoardList> {\n const { domain } = context.state\n\n const queryBuilder = getQueryBuilderFromListParams({\n repository: getRepository(Board),\n params,\n domain,\n searchables: ['name', 'description']\n })\n\n // 클라이언트 정렬 지정이 없으면 sortOrder 기본 정렬\n if (!params.sortings?.length) {\n queryBuilder.addOrderBy(`\"${queryBuilder.alias}\".\"sort_order\"`, 'ASC', 'NULLS LAST')\n queryBuilder.addOrderBy(`\"${queryBuilder.alias}\".\"created_at\"`, 'ASC')\n }\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Query(returns => [Board], {\n description: 'Retrieves boards that have been updated or soft-deleted since the given timestamp.'\n })\n @Directive('@privilege(category: \"board\", privilege: \"query\", domainOwnerGranted: true)')\n async boardsUpdatedSince(\n @Arg('since') since: Date,\n @Ctx() context: ResolverContext\n ): Promise<Board[]> {\n const { domain } = context.state\n\n return await getRepository(Board)\n .createQueryBuilder('board')\n .withDeleted()\n .where('board.domain_id = :domainId', { domainId: domain.id })\n .andWhere('board.updated_at > :since', { since })\n .getMany()\n }\n\n @FieldResolver(type => Group, { description: 'Resolves the group that the board belongs to.' })\n async group(@Root() board: Board) {\n return (\n board.groupId &&\n (await getRepository(Group).findOneBy({\n id: board.groupId\n }))\n )\n }\n\n @FieldResolver(type => [PlayGroup], {\n description: 'Resolves the list of play groups that this board is a member of.'\n })\n async playGroups(@Root() board: Board) {\n return (\n await getRepository(Board).findOne({\n where: { id: board.id },\n relations: ['playGroups']\n })\n )?.playGroups\n }\n\n @FieldResolver(type => Domain, { description: 'Resolves the domain associated with the board.' })\n async domain(@Root() board: Board) {\n return (\n board.domainId &&\n (await getRepository(Domain).findOneBy({\n id: board.domainId\n }))\n )\n }\n\n @FieldResolver(type => User, { description: 'Resolves the user who last updated the board.' })\n async updater(@Root() board: Board): Promise<User> {\n return (\n board.updaterId &&\n (await getRepository(User).findOneBy({\n id: board.updaterId\n }))\n )\n }\n\n @FieldResolver(type => User, { description: 'Resolves the user who created the board.' })\n async creator(@Root() board: Board): Promise<User> {\n return (\n board.creatorId &&\n (await getRepository(User).findOneBy({\n id: board.creatorId\n }))\n )\n }\n}\n"]}
@@ -14,16 +14,30 @@ describe('board-version-diff (pure)', () => {
14
14
  ]
15
15
  };
16
16
  const flat = (0, board_version_diff_1.flattenBoardComponents)(model);
17
- expect(flat.map(f => f.refid)).toEqual([1, 2, 3]);
17
+ /*
18
+ * 맨 앞은 **씬 자체**(`__root__`)다. 배경색·조명·scale 만 바뀐 릴리즈가 「차이 없음」으로
19
+ * 나오던 버그를 고치면서 들어갔고(§`flattenBoardComponents`), 이 시험이 안 따라와 있었다
20
+ * (2026-09-10 에 맞췄다). 루트를 빼면 그 버그가 조용히 돌아온다.
21
+ */
22
+ expect(flat[0].key).toBe('__root__');
23
+ expect(flat.map(f => f.refid)).toEqual([null, 1, 2, 3]);
18
24
  });
19
25
  it('parses a JSON string model', () => {
20
26
  const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] });
21
- expect((0, board_version_diff_1.flattenBoardComponents)(model).map(f => f.refid)).toEqual([1]);
27
+ expect((0, board_version_diff_1.flattenBoardComponents)(model).map(f => f.refid)).toEqual([null, 1]);
22
28
  });
23
29
  it('returns [] for null/invalid model', () => {
24
30
  expect((0, board_version_diff_1.flattenBoardComponents)(null)).toEqual([]);
25
31
  expect((0, board_version_diff_1.flattenBoardComponents)('not-json')).toEqual([]);
26
- expect((0, board_version_diff_1.flattenBoardComponents)({})).toEqual([]);
32
+ });
33
+ it('an empty object is an empty scene, not an invalid model', () => {
34
+ /*
35
+ * `{}` 가 위 묶음에 함께 있었다. 무효한 입력이 아니라 **컴포넌트가 없는 씬**이고, 그것이
36
+ * 갈려야 배경색만 바뀐 릴리즈가 차이로 잡힌다 — `__root__` 항목이 있는 이유가 그것이다.
37
+ * 여기서 `[]` 를 기대하면 그 버그가 시험으로 고정된다.
38
+ */
39
+ const flat = (0, board_version_diff_1.flattenBoardComponents)({});
40
+ expect(flat.map(f => f.key)).toEqual(['__root__']);
27
41
  });
28
42
  });
29
43
  describe('diffBoardModels', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"board-version-diff.test.js","sourceRoot":"","sources":["../../../server/service/board/board-version-diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-version-diff — 보드 model(things-scene 컴포넌트 트리) diff 순수 로직 단위 검증.\n */\nimport { diffBoardModels, flattenBoardComponents } from './board-version-diff'\n\ndescribe('board-version-diff (pure)', () => {\n describe('flattenBoardComponents', () => {\n it('flattens nested components recursively', () => {\n const model = {\n components: [\n { refid: 1, type: 'rect' },\n { refid: 2, type: 'group', components: [{ refid: 3, type: 'text' }] }\n ]\n }\n const flat = flattenBoardComponents(model)\n expect(flat.map(f => f.refid)).toEqual([1, 2, 3])\n })\n\n it('parses a JSON string model', () => {\n const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] })\n expect(flattenBoardComponents(model).map(f => f.refid)).toEqual([1])\n })\n\n it('returns [] for null/invalid model', () => {\n expect(flattenBoardComponents(null)).toEqual([])\n expect(flattenBoardComponents('not-json')).toEqual([])\n expect(flattenBoardComponents({})).toEqual([])\n })\n })\n\n describe('diffBoardModels', () => {\n it('detects added, removed, and modified components by refid', () => {\n const base = {\n components: [\n { refid: 1, type: 'rect', left: 0 },\n { refid: 2, type: 'text', text: 'hello' }\n ]\n }\n const target = {\n components: [\n { refid: 1, type: 'rect', left: 50 }, // modified (left)\n { refid: 3, type: 'circle' } // added\n // refid 2 removed\n ]\n }\n const diffs = diffBoardModels(base, target)\n const byType = (t: string) => diffs.filter(d => d.changeType === t).map(d => d.refid).sort()\n\n expect(byType('added')).toEqual([3])\n expect(byType('removed')).toEqual([2])\n expect(byType('modified')).toEqual([1])\n\n const modified = diffs.find(d => d.refid === 1)\n expect(modified?.changedProps).toContain('left')\n })\n\n it('reports no diffs for identical models', () => {\n const model = { components: [{ refid: 1, type: 'rect', left: 10 }] }\n expect(diffBoardModels(model, JSON.parse(JSON.stringify(model)))).toEqual([])\n })\n\n it('ignores children array when diffing a container component props', () => {\n const base = { components: [{ refid: 1, type: 'group', components: [{ refid: 2, type: 'text' }] }] }\n // 자식만 바뀌고 컨테이너 자체 속성은 동일 → 컨테이너는 modified 아님, 자식만 removed\n const target = { components: [{ refid: 1, type: 'group', components: [] }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs.find(d => d.refid === 1)).toBeUndefined()\n expect(diffs.find(d => d.refid === 2)?.changeType).toBe('removed')\n })\n\n it('falls back to positional key when refid is missing', () => {\n const base = { components: [{ type: 'rect', left: 0 }] }\n const target = { components: [{ type: 'rect', left: 99 }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs).toHaveLength(1)\n expect(diffs[0].changeType).toBe('modified')\n expect(diffs[0].changedProps).toContain('left')\n })\n })\n})\n"]}
1
+ {"version":3,"file":"board-version-diff.test.js","sourceRoot":"","sources":["../../../server/service/board/board-version-diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-version-diff — 보드 model(things-scene 컴포넌트 트리) diff 순수 로직 단위 검증.\n */\nimport { diffBoardModels, flattenBoardComponents } from './board-version-diff'\n\ndescribe('board-version-diff (pure)', () => {\n describe('flattenBoardComponents', () => {\n it('flattens nested components recursively', () => {\n const model = {\n components: [\n { refid: 1, type: 'rect' },\n { refid: 2, type: 'group', components: [{ refid: 3, type: 'text' }] }\n ]\n }\n const flat = flattenBoardComponents(model)\n /*\n * 맨 앞은 **씬 자체**(`__root__`)다. 배경색·조명·scale 만 바뀐 릴리즈가 「차이 없음」으로\n * 나오던 버그를 고치면서 들어갔고(§`flattenBoardComponents`), 이 시험이 안 따라와 있었다\n * (2026-09-10 에 맞췄다). 루트를 빼면 그 버그가 조용히 돌아온다.\n */\n expect(flat[0].key).toBe('__root__')\n expect(flat.map(f => f.refid)).toEqual([null, 1, 2, 3])\n })\n\n it('parses a JSON string model', () => {\n const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] })\n expect(flattenBoardComponents(model).map(f => f.refid)).toEqual([null, 1])\n })\n\n it('returns [] for null/invalid model', () => {\n expect(flattenBoardComponents(null)).toEqual([])\n expect(flattenBoardComponents('not-json')).toEqual([])\n })\n\n it('an empty object is an empty scene, not an invalid model', () => {\n /*\n * `{}` 가 위 묶음에 함께 있었다. 무효한 입력이 아니라 **컴포넌트가 없는 씬**이고, 그것이\n * 갈려야 배경색만 바뀐 릴리즈가 차이로 잡힌다 — `__root__` 항목이 있는 이유가 그것이다.\n * 여기서 `[]` 를 기대하면 그 버그가 시험으로 고정된다.\n */\n const flat = flattenBoardComponents({})\n expect(flat.map(f => f.key)).toEqual(['__root__'])\n })\n })\n\n describe('diffBoardModels', () => {\n it('detects added, removed, and modified components by refid', () => {\n const base = {\n components: [\n { refid: 1, type: 'rect', left: 0 },\n { refid: 2, type: 'text', text: 'hello' }\n ]\n }\n const target = {\n components: [\n { refid: 1, type: 'rect', left: 50 }, // modified (left)\n { refid: 3, type: 'circle' } // added\n // refid 2 removed\n ]\n }\n const diffs = diffBoardModels(base, target)\n const byType = (t: string) => diffs.filter(d => d.changeType === t).map(d => d.refid).sort()\n\n expect(byType('added')).toEqual([3])\n expect(byType('removed')).toEqual([2])\n expect(byType('modified')).toEqual([1])\n\n const modified = diffs.find(d => d.refid === 1)\n expect(modified?.changedProps).toContain('left')\n })\n\n it('reports no diffs for identical models', () => {\n const model = { components: [{ refid: 1, type: 'rect', left: 10 }] }\n expect(diffBoardModels(model, JSON.parse(JSON.stringify(model)))).toEqual([])\n })\n\n it('ignores children array when diffing a container component props', () => {\n const base = { components: [{ refid: 1, type: 'group', components: [{ refid: 2, type: 'text' }] }] }\n // 자식만 바뀌고 컨테이너 자체 속성은 동일 → 컨테이너는 modified 아님, 자식만 removed\n const target = { components: [{ refid: 1, type: 'group', components: [] }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs.find(d => d.refid === 1)).toBeUndefined()\n expect(diffs.find(d => d.refid === 2)?.changeType).toBe('removed')\n })\n\n it('falls back to positional key when refid is missing', () => {\n const base = { components: [{ type: 'rect', left: 0 }] }\n const target = { components: [{ type: 'rect', left: 99 }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs).toHaveLength(1)\n expect(diffs[0].changeType).toBe('modified')\n expect(diffs[0].changedProps).toContain('left')\n })\n })\n})\n"]}