sonamu 0.7.0 → 0.7.2

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.
Files changed (39) hide show
  1. package/dist/ai/agents/types.d.ts +4 -3
  2. package/dist/ai/agents/types.d.ts.map +1 -1
  3. package/dist/ai/agents/types.js +1 -1
  4. package/dist/api/config.d.ts +4 -2
  5. package/dist/api/config.d.ts.map +1 -1
  6. package/dist/api/config.js +5 -2
  7. package/dist/api/decorators.d.ts.map +1 -1
  8. package/dist/api/decorators.js +3 -2
  9. package/dist/bin/cli.js +13 -13
  10. package/dist/bin/{hot-hook-register.d.ts → hmr-hook-register.d.ts} +3 -3
  11. package/dist/bin/hmr-hook-register.d.ts.map +1 -0
  12. package/dist/bin/{hot-hook-register.js → hmr-hook-register.js} +5 -5
  13. package/dist/bin/ts-loader-register.d.ts +2 -0
  14. package/dist/bin/ts-loader-register.d.ts.map +1 -0
  15. package/dist/bin/ts-loader-register.js +34 -0
  16. package/dist/database/base-model.d.ts +2 -34
  17. package/dist/database/base-model.d.ts.map +1 -1
  18. package/dist/database/base-model.js +4 -171
  19. package/dist/database/upsert-builder.d.ts.map +1 -1
  20. package/dist/database/upsert-builder.js +3 -4
  21. package/dist/syncer/syncer.js +3 -3
  22. package/dist/template/zod-converter.d.ts.map +1 -1
  23. package/dist/template/zod-converter.js +50 -3
  24. package/dist/types/types.d.ts +2 -2
  25. package/package.json +9 -9
  26. package/src/ai/agents/types.ts +6 -3
  27. package/src/api/config.ts +16 -5
  28. package/src/api/decorators.ts +2 -1
  29. package/src/bin/cli.ts +13 -13
  30. package/src/bin/{hot-hook-register.ts → hmr-hook-register.ts} +4 -4
  31. package/src/bin/{loader-register.ts → ts-loader-register.ts} +2 -2
  32. package/src/database/base-model.ts +6 -237
  33. package/src/database/upsert-builder.ts +2 -3
  34. package/src/syncer/syncer.ts +2 -2
  35. package/src/template/zod-converter.ts +57 -3
  36. package/dist/bin/hot-hook-register.d.ts.map +0 -1
  37. package/dist/bin/loader-register.d.ts +0 -2
  38. package/dist/bin/loader-register.d.ts.map +0 -1
  39. package/dist/bin/loader-register.js +0 -34
@@ -1,10 +1,9 @@
1
- import assert from "assert";
2
- import inflection from "inflection";
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: Puri의 타입은 개별 모델에서 확정되므로 BaseModel에서는 any를 허용함 */
2
+
3
3
  import type { Knex } from "knex";
4
- import { group, isObject, omit, set, unique } from "radashi";
4
+ import { group, isObject, omit, set } from "radashi";
5
5
  import { Sonamu } from "../api";
6
- import { type DatabaseSchemaExtend, isCustomJoinClause, type SubsetQuery } from "../types/types";
7
- import type { BaseListParams } from "../utils/model";
6
+ import type { DatabaseSchemaExtend } from "../types/types";
8
7
  import { getJoinTables, getTableNamesFromWhere } from "../utils/sql-parser";
9
8
  import { chunk } from "../utils/utils";
10
9
  import type {
@@ -56,7 +55,7 @@ export class BaseModelClass<
56
55
 
57
56
  // 트랜잭션이 없으면 새로운 PuriWrapper 반환
58
57
  const db = this.getDB(which);
59
- return new PuriWrapper(db, this.getUpsertBuilder());
58
+ return new PuriWrapper(db, new UpsertBuilder());
60
59
  }
61
60
 
62
61
  async destroy() {
@@ -229,7 +228,7 @@ export class BaseModelClass<
229
228
  // TODO: qb의 DISTINCT가 있는 경우 처리해야 함
230
229
  const countResult: { total?: number } = await countPuri
231
230
  .clear("select")
232
- .select({ total: Puri.rawNumber(`COUNT(*)`) })
231
+ .select({ total: Puri.rawNumber(`COUNT(*)::integer`) })
233
232
  .first();
234
233
 
235
234
  if (debug) {
@@ -365,236 +364,6 @@ export class BaseModelClass<
365
364
  return hydrated;
366
365
  }) as T[];
367
366
  }
368
-
369
- // Legacy SubsetQuery 실행 (Puri 도입 전 호환용)
370
- async runSubsetQuery<T extends BaseListParams, U extends string>({
371
- params,
372
- baseTable,
373
- subset,
374
- subsetQuery,
375
- build,
376
- afterBuild,
377
- debug,
378
- db: _db,
379
- optimizeCountQuery,
380
- }: {
381
- subset: U;
382
- params: T;
383
- subsetQuery: SubsetQuery;
384
- build: (buildParams: {
385
- qb: Knex.QueryBuilder;
386
- db: Knex;
387
- select: (string | Knex.Raw)[];
388
- joins: SubsetQuery["joins"];
389
- virtual: string[];
390
- }) => Knex.QueryBuilder;
391
- afterBuild?: (buildParams: {
392
- qb: Knex.QueryBuilder;
393
- db: Knex;
394
- select: (string | Knex.Raw)[];
395
- joins: SubsetQuery["joins"];
396
- virtual: string[];
397
- }) => Knex.QueryBuilder;
398
- baseTable?: string;
399
- debug?: boolean | "list" | "count";
400
- db?: Knex;
401
- optimizeCountQuery?: boolean;
402
- }): Promise<{
403
- // biome-ignore lint/suspicious/noExplicitAny: Puri 도입 전까지 any로 유지
404
- rows: any[];
405
- total?: number | undefined;
406
- subsetQuery: SubsetQuery;
407
- qb: Knex.QueryBuilder;
408
- }> {
409
- const chalk = (await import("chalk")).default;
410
- const SqlParser = (await import("node-sql-parser")).default;
411
- const { getTableName, getTableNamesFromWhere } = await import("../utils/sql-parser");
412
-
413
- const db = _db ?? this.getDB(subset.startsWith("A") ? "w" : "r");
414
- baseTable = baseTable ?? inflection.pluralize(inflection.underscore(this.modelName));
415
- const queryMode = params.queryMode ?? (params.id !== undefined ? "list" : "both");
416
-
417
- const { select, virtual, joins, loaders } = subsetQuery;
418
- const qb = build({
419
- qb: db.from(baseTable),
420
- db,
421
- select,
422
- joins,
423
- virtual,
424
- });
425
-
426
- const applyJoinClause = (qb: Knex.QueryBuilder, joins: SubsetQuery["joins"]) => {
427
- joins.forEach((join) => {
428
- if (join.join === "inner") {
429
- qb.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
430
- } else if (join.join === "outer") {
431
- qb.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
432
- }
433
- });
434
- };
435
-
436
- // countQuery
437
- const total = await (async () => {
438
- if (queryMode === "list") {
439
- return undefined;
440
- }
441
-
442
- const clonedQb = qb.clone().clear("order").clear("offset").clear("limit");
443
- const parser = new SqlParser.Parser();
444
-
445
- if (optimizeCountQuery) {
446
- const parsedQuery = parser.astify(clonedQb.toQuery(), {
447
- database: Sonamu.config.database.database,
448
- });
449
- const tables = getTableNamesFromWhere(parsedQuery);
450
- const needToJoin = unique(
451
- tables.flatMap((table) => table.split("__").map((t) => inflection.pluralize(t))),
452
- );
453
- applyJoinClause(
454
- clonedQb,
455
- joins.filter((j) => needToJoin.includes(j.table)),
456
- );
457
- } else {
458
- applyJoinClause(clonedQb, joins);
459
- }
460
-
461
- const processedQb = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
462
-
463
- const parsedQuery = parser.astify(processedQb.toQuery(), {
464
- database: Sonamu.config.database.database,
465
- });
466
- const q = Array.isArray(parsedQuery) ? parsedQuery[0] : parsedQuery;
467
- if (q.type !== "select") {
468
- throw new Error("Invalid query");
469
- }
470
-
471
- const countQuery =
472
- q.distinct !== null
473
- ? clonedQb
474
- .clear("select")
475
- .select(
476
- db.raw(
477
- `COUNT(DISTINCT \`${getTableName(q.columns[0].expr)}\`.\`${q.columns[0].expr.column}\`) as total`,
478
- ),
479
- )
480
- .first()
481
- : clonedQb.clear("select").count("*", { as: "total" }).first();
482
- const countRow: { total?: number } = await countQuery;
483
-
484
- if (debug === true || debug === "count") {
485
- console.debug("DEBUG: count query", chalk.blue(countQuery.toQuery().toString()));
486
- }
487
-
488
- return countRow?.total ?? 0;
489
- })();
490
-
491
- // listQuery
492
- const rows = await (async () => {
493
- if (queryMode === "count") {
494
- return [];
495
- }
496
-
497
- if (params.num !== 0) {
498
- assert(params.num);
499
- qb.limit(params.num);
500
- qb.offset(params.num * ((params.page ?? 1) - 1));
501
- }
502
-
503
- const clonedQb = qb.clone().select(select);
504
- applyJoinClause(clonedQb, joins);
505
-
506
- const listQuery = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
507
-
508
- let rows = await listQuery;
509
- if (debug === true || debug === "list") {
510
- console.debug("DEBUG: list query", chalk.blue(listQuery.toQuery().toString()));
511
- }
512
-
513
- rows = await this.useLoaders(db, rows, loaders);
514
- rows = this.hydrate(rows);
515
- return rows;
516
- })();
517
-
518
- return { rows, total, subsetQuery, qb };
519
- }
520
-
521
- // Legacy Loader 처리 (Puri 도입 전 호환용)
522
- async useLoaders(db: Knex, rows: UnknownDBRecord[], loaders: SubsetQuery["loaders"]) {
523
- if (loaders.length === 0) {
524
- return rows;
525
- }
526
-
527
- for (const loader of loaders) {
528
- let subQ: Knex.QueryBuilder;
529
- let subRows: UnknownDBRecord[];
530
- let toCol: string;
531
-
532
- const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
533
-
534
- if (loader.manyJoin.through === undefined) {
535
- // HasMany
536
- const idColumn = `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`;
537
- subQ = db(loader.manyJoin.toTable)
538
- .whereIn(idColumn as string, fromIds as string[])
539
- .select([...loader.select, idColumn]);
540
-
541
- loader.oneJoins.forEach((join) => {
542
- if (join.join === "inner") {
543
- subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
544
- } else if (join.join === "outer") {
545
- subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
546
- }
547
- });
548
- toCol = loader.manyJoin.toCol;
549
- } else {
550
- // ManyToMany
551
- const idColumn = `${loader.manyJoin.through.table}.${loader.manyJoin.through.fromCol}`;
552
- subQ = db(loader.manyJoin.through.table)
553
- .join(
554
- loader.manyJoin.toTable,
555
- `${loader.manyJoin.through.table}.${loader.manyJoin.through.toCol}`,
556
- `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`,
557
- )
558
- .whereIn(idColumn as string, fromIds as string[])
559
- .select(unique([...loader.select, idColumn]));
560
-
561
- loader.oneJoins.forEach((join) => {
562
- if (join.join === "inner") {
563
- subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
564
- } else if (join.join === "outer") {
565
- subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
566
- }
567
- });
568
- toCol = loader.manyJoin.through.fromCol;
569
- }
570
- subRows = await subQ;
571
-
572
- if (loader.loaders) {
573
- subRows = await this.useLoaders(db, subRows, loader.loaders);
574
- }
575
-
576
- const subRowGroups = group(subRows, (row) => row[toCol] as string);
577
- rows = rows.map((row) => {
578
- row[loader.as] = (subRowGroups[row[loader.manyJoin.idField] as string] ?? []).map((r) =>
579
- omit(r, [toCol]),
580
- );
581
- return row;
582
- });
583
- }
584
- return rows;
585
- }
586
-
587
- getJoinClause(db: Knex<any, unknown>, join: SubsetQuery["joins"][number]): Knex.Raw<any> {
588
- if (!isCustomJoinClause(join)) {
589
- return db.raw(`${join.from} = ${join.to}`);
590
- } else {
591
- return db.raw(join.custom);
592
- }
593
- }
594
-
595
- getUpsertBuilder(): UpsertBuilder {
596
- return new UpsertBuilder();
597
- }
598
367
  }
599
368
 
600
369
  /**
@@ -261,16 +261,15 @@ export class UpsertBuilder {
261
261
  .select(selectFields)
262
262
  .whereIn("uuid", uuids as readonly string[]);
263
263
  } else {
264
- // UPSERT 모드 (uniqueIndexes 이미 체크됨)
264
+ // UPSERT 모드: onConflict로 중복 처리
265
265
  const conflictColumns = table.uniqueIndexes[0].columns;
266
266
  const updateColumns = Object.keys(dataChunk[0]).filter(
267
267
  (col) => col !== "uuid" && !conflictColumns.includes(col),
268
268
  );
269
269
 
270
- // RETURNING으로 결과 받기
271
270
  const query = wdb.insert(dataChunk).into(tableName).onConflict(conflictColumns);
272
271
 
273
- // updateColumns 비어있으면 ignore(), 아니면 merge()
272
+ // updateColumns 유무에 따라 ignore/merge 선택하고 RETURNING으로 결과 받기
274
273
  if (updateColumns.length === 0) {
275
274
  resultRows = await query.ignore().returning(selectFields);
276
275
  } else {
@@ -1,4 +1,4 @@
1
- import { hot } from "@sonamu-kit/hot-hook";
1
+ import { hot } from "@sonamu-kit/hmr-hook";
2
2
  import assert from "assert";
3
3
  import chalk from "chalk";
4
4
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -122,7 +122,7 @@ export class Syncer {
122
122
  }
123
123
 
124
124
  // 싱크 작업이 끝나면 모든 모듈을 로드합니다.
125
- // hot-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
125
+ // hmr-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
126
126
  await this.autoloadTypes();
127
127
  await this.autoloadModels();
128
128
  await this.autoloadApis();
@@ -62,7 +62,7 @@ type AnyZodDefault = z.ZodDefault<z.ZodType>;
62
62
  type AnyZodUnion = z.ZodUnion<z.ZodType[]>;
63
63
  type AnyZodArray = z.ZodArray<z.ZodType>;
64
64
  type AnyZodOptional = z.ZodOptional<z.ZodType>;
65
-
65
+ type AnyZodTemplateLiteral = z.ZodTemplateLiteral<string>;
66
66
  /**
67
67
  * Zod 타입 ID로부터 동적으로 Zod 스키마를 로드합니다.
68
68
  * dist 디렉토리에서 ESM으로 import하여 가져옵니다.
@@ -261,7 +261,6 @@ export function propNodeToZodTypeDef(propNode: EntityPropNode, injectImportKeys:
261
261
  }
262
262
  }
263
263
 
264
- // TODO(Haze, 251031): "template_literal", "file"에 대한 지원이 필요함.
265
264
  export function zodTypeToTsTypeDef(zt: z.ZodType): string {
266
265
  switch (zt.def.type) {
267
266
  case "string":
@@ -325,12 +324,40 @@ export function zodTypeToTsTypeDef(zt: z.ZodType): string {
325
324
  }
326
325
  case "optional":
327
326
  return `${zodTypeToTsTypeDef((zt as AnyZodOptional).def.innerType)} | undefined`;
327
+ case "template_literal": {
328
+ const def = (zt as AnyZodTemplateLiteral).def;
329
+
330
+ // 빈 template literal은 string으로 폴백
331
+ if (!def.parts || def.parts.length === 0) {
332
+ return "string";
333
+ }
334
+
335
+ // 각 part를 TypeScript 타입 문자열로 변환
336
+ const parts = def.parts.map((part: unknown) => {
337
+ // 리터럴 값 (string, number, boolean, null, undefined)
338
+ if (typeof part === "string") {
339
+ return `${part}`;
340
+ }
341
+
342
+ // ZodType - 재귀적으로 변환
343
+ if (part && typeof part === "object" && (part as z.ZodType)._zod) {
344
+ const innerType = zodTypeToTsTypeDef(part as z.ZodType);
345
+ return `$\{${innerType}}`;
346
+ }
347
+
348
+ // 폴백
349
+ return `\${string}`;
350
+ });
351
+
352
+ return `\`${parts.join("")}\``;
353
+ }
354
+ case "file":
355
+ return "File";
328
356
  default:
329
357
  throw new Error(`처리되지 않은 ZodType ${zt.def.type}`);
330
358
  }
331
359
  }
332
360
 
333
- // TODO(Haze, 251031): "template_literal", "file"에 대한 지원이 필요함.
334
361
  /**
335
362
  * Zod 타입 인스턴스를 해당하는 Zod 코드 문자열로 변환합니다.
336
363
  */
@@ -413,6 +440,31 @@ export function zodTypeToZodCode(zt: z.ZodType): string {
413
440
  return `${zodTypeToZodCode((zt as z.ZodOptional<z.ZodType>).def.innerType)}.optional()`;
414
441
  case "file":
415
442
  return `z.file()`;
443
+ case "template_literal": {
444
+ const def = (zt as AnyZodTemplateLiteral).def;
445
+
446
+ // 빈 template literal
447
+ if (!def.parts || def.parts.length === 0) {
448
+ return "z.templateLiteral([])";
449
+ }
450
+
451
+ // 각 part를 Zod 코드 문자열로 변환
452
+ const parts = def.parts.map((part: unknown) => {
453
+ // 문자열 리터럴
454
+ if (typeof part === "string") {
455
+ return `"${part}"`;
456
+ }
457
+ // ZodType - 재귀적으로 변환
458
+ if (part && typeof part === "object" && (part as z.ZodType)._zod) {
459
+ return zodTypeToZodCode(part as z.ZodType);
460
+ }
461
+
462
+ // 폴백
463
+ return "z.string()";
464
+ });
465
+
466
+ return `z.templateLiteral([${parts.join(", ")}])`;
467
+ }
416
468
  case "intersection": {
417
469
  const zIntersectionDef = (zt as z.ZodIntersection<z.ZodType, z.ZodType>).def;
418
470
  return `z.intersection(${zodTypeToZodCode(zIntersectionDef.left)}, ${zodTypeToZodCode(zIntersectionDef.right)})`;
@@ -519,6 +571,8 @@ function resolveRenderType(key: string, zodType: z.ZodTypeAny): RenderingNode["r
519
571
  return "string-plain";
520
572
  } else if (zodType instanceof z.ZodLiteral) {
521
573
  return "string-plain";
574
+ } else if (zodType instanceof z.ZodTemplateLiteral) {
575
+ return "string-plain";
522
576
  } else {
523
577
  throw new Error(`타입 파싱 불가 ${key} ${zodType.def.type}`);
524
578
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"hot-hook-register.d.ts","sourceRoot":"","sources":["../../src/bin/hot-hook-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAE,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=loader-register.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-register.d.ts","sourceRoot":"","sources":["../../src/bin/loader-register.ts"],"names":[],"mappings":""}
@@ -1,34 +0,0 @@
1
- import { register } from "node:module";
2
- import * as path from "node:path";
3
- import { exists } from "../utils/fs-utils.js";
4
- import { findApiRootPath } from "../utils/utils.js";
5
- /**
6
- * @sonamu-kit/loader/loader를 등록하는 스크립트입니다.
7
- * 이 스크립트는 sonamu cli로 dev 실행할 때 --import로 실행됩니다.
8
- */ async function setupSwcConfig() {
9
- try {
10
- const apiRoot = findApiRootPath();
11
- // 프로젝트 루트에서 .swcrc 찾기
12
- const projectSwcrcPath = path.join(apiRoot, ".swcrc");
13
- if (await exists(projectSwcrcPath)) {
14
- // 사용자 프로젝트에 .swcrc가 있으면 우선으로 사용합니다.
15
- process.env.SWCRC_PATH = projectSwcrcPath;
16
- return;
17
- }
18
- // 아니라면 sonamu가 관리하는 .swcrc.project-default를 가져다 씁니다.
19
- const sonamuSwcrcPath = path.join(import.meta.dirname, "..", "..", ".swcrc.project-default");
20
- if (await exists(sonamuSwcrcPath)) {
21
- process.env.SWCRC_PATH = sonamuSwcrcPath;
22
- return;
23
- }
24
- } catch {
25
- // 환경 변수 설정 실패는 무시 (loader가 기본 설정 사용)
26
- }
27
- }
28
- // swc 설정 파일 경로를 환경 변수로 설정
29
- await setupSwcConfig();
30
- register("@sonamu-kit/loader/loader", {
31
- parentURL: import.meta.url
32
- });
33
-
34
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vbG9hZGVyLXJlZ2lzdGVyLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHJlZ2lzdGVyIH0gZnJvbSBcIm5vZGU6bW9kdWxlXCI7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gXCJub2RlOnBhdGhcIjtcbmltcG9ydCB7IGV4aXN0cyB9IGZyb20gXCIuLi91dGlscy9mcy11dGlscy5qc1wiO1xuaW1wb3J0IHsgZmluZEFwaVJvb3RQYXRoIH0gZnJvbSBcIi4uL3V0aWxzL3V0aWxzLmpzXCI7XG5cbi8qKlxuICogQHNvbmFtdS1raXQvbG9hZGVyL2xvYWRlcuulvCDrk7HroZ3tlZjripQg7Iqk7YGs66a97Yq47J6F64uI64ukLlxuICog7J20IOyKpO2BrOumve2KuOuKlCBzb25hbXUgY2xp66GcIGRldiDsi6TtlontlaAg65WMIC0taW1wb3J066GcIOyLpO2WieuQqeuLiOuLpC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gc2V0dXBTd2NDb25maWcoKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgYXBpUm9vdCA9IGZpbmRBcGlSb290UGF0aCgpO1xuXG4gICAgLy8g7ZSE66Gc7KCd7Yq4IOujqO2KuOyXkOyEnCAuc3djcmMg7LC+6riwXG4gICAgY29uc3QgcHJvamVjdFN3Y3JjUGF0aCA9IHBhdGguam9pbihhcGlSb290LCBcIi5zd2NyY1wiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHByb2plY3RTd2NyY1BhdGgpKSB7XG4gICAgICAvLyDsgqzsmqnsnpAg7ZSE66Gc7KCd7Yq47JeQIC5zd2NyY+qwgCDsnojsnLzrqbQg7Jqw7ISg7Jy866GcIOyCrOyaqe2VqeuLiOuLpC5cbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBwcm9qZWN0U3djcmNQYXRoO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIOyVhOuLiOudvOuptCBzb25hbXXqsIAg6rSA66as7ZWY64qUIC5zd2NyYy5wcm9qZWN0LWRlZmF1bHTrpbwg6rCA7KC464ukIOyUgeuLiOuLpC5cbiAgICBjb25zdCBzb25hbXVTd2NyY1BhdGggPSBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIuLlwiLCBcIi4uXCIsIFwiLnN3Y3JjLnByb2plY3QtZGVmYXVsdFwiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHNvbmFtdVN3Y3JjUGF0aCkpIHtcbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBzb25hbXVTd2NyY1BhdGg7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9IGNhdGNoIHtcbiAgICAvLyDtmZjqsr0g67OA7IiYIOyEpOyglSDsi6TtjKjripQg66y07IucIChsb2FkZXLqsIAg6riw67O4IOyEpOyglSDsgqzsmqkpXG4gIH1cbn1cblxuLy8gc3djIOyEpOyglSDtjIzsnbwg6rK966Gc66W8IO2ZmOqyvSDrs4DsiJjroZwg7ISk7KCVXG5hd2FpdCBzZXR1cFN3Y0NvbmZpZygpO1xuXG5yZWdpc3RlcihcIkBzb25hbXUta2l0L2xvYWRlci9sb2FkZXJcIiwge1xuICBwYXJlbnRVUkw6IGltcG9ydC5tZXRhLnVybCxcbn0pO1xuIl0sIm5hbWVzIjpbInJlZ2lzdGVyIiwicGF0aCIsImV4aXN0cyIsImZpbmRBcGlSb290UGF0aCIsInNldHVwU3djQ29uZmlnIiwiYXBpUm9vdCIsInByb2plY3RTd2NyY1BhdGgiLCJqb2luIiwicHJvY2VzcyIsImVudiIsIlNXQ1JDX1BBVEgiLCJzb25hbXVTd2NyY1BhdGgiLCJkaXJuYW1lIiwicGFyZW50VVJMIiwidXJsIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxRQUFRLFFBQVEsY0FBYztBQUN2QyxZQUFZQyxVQUFVLFlBQVk7QUFDbEMsU0FBU0MsTUFBTSxRQUFRLHVCQUF1QjtBQUM5QyxTQUFTQyxlQUFlLFFBQVEsb0JBQW9CO0FBRXBEOzs7Q0FHQyxHQUNELGVBQWVDO0lBQ2IsSUFBSTtRQUNGLE1BQU1DLFVBQVVGO1FBRWhCLHNCQUFzQjtRQUN0QixNQUFNRyxtQkFBbUJMLEtBQUtNLElBQUksQ0FBQ0YsU0FBUztRQUM1QyxJQUFJLE1BQU1ILE9BQU9JLG1CQUFtQjtZQUNsQyxvQ0FBb0M7WUFDcENFLFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHSjtZQUN6QjtRQUNGO1FBRUEscURBQXFEO1FBQ3JELE1BQU1LLGtCQUFrQlYsS0FBS00sSUFBSSxDQUFDLFlBQVlLLE9BQU8sRUFBRSxNQUFNLE1BQU07UUFDbkUsSUFBSSxNQUFNVixPQUFPUyxrQkFBa0I7WUFDakNILFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHQztZQUN6QjtRQUNGO0lBQ0YsRUFBRSxPQUFNO0lBQ04scUNBQXFDO0lBQ3ZDO0FBQ0Y7QUFFQSwwQkFBMEI7QUFDMUIsTUFBTVA7QUFFTkosU0FBUyw2QkFBNkI7SUFDcENhLFdBQVcsWUFBWUMsR0FBRztBQUM1QiJ9