sonamu 0.7.15 → 0.7.17

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 (96) hide show
  1. package/dist/ai/providers/rtzr/error.d.ts +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/api/config.d.ts +1 -0
  4. package/dist/api/config.d.ts.map +1 -1
  5. package/dist/api/config.js +1 -1
  6. package/dist/api/decorators.d.ts +1 -1
  7. package/dist/api/decorators.d.ts.map +1 -1
  8. package/dist/api/decorators.js +1 -1
  9. package/dist/api/sonamu.d.ts +3 -1
  10. package/dist/api/sonamu.d.ts.map +1 -1
  11. package/dist/api/sonamu.js +51 -40
  12. package/dist/database/base-model.d.ts +16 -6
  13. package/dist/database/base-model.d.ts.map +1 -1
  14. package/dist/database/base-model.js +44 -3
  15. package/dist/database/base-model.types.d.ts +29 -48
  16. package/dist/database/base-model.types.d.ts.map +1 -1
  17. package/dist/database/base-model.types.js +12 -2
  18. package/dist/database/puri.d.ts +2 -1
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +2 -1
  21. package/dist/database/puri.types.d.ts +3 -3
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +1 -1
  24. package/dist/entity/entity-manager.d.ts +8 -4
  25. package/dist/entity/entity-manager.d.ts.map +1 -1
  26. package/dist/entity/entity.d.ts +10 -1
  27. package/dist/entity/entity.d.ts.map +1 -1
  28. package/dist/entity/entity.js +84 -39
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/syncer/checksum.d.ts +8 -3
  33. package/dist/syncer/checksum.d.ts.map +1 -1
  34. package/dist/syncer/checksum.js +17 -9
  35. package/dist/syncer/code-generator.js +7 -2
  36. package/dist/syncer/syncer.d.ts +6 -6
  37. package/dist/syncer/syncer.d.ts.map +1 -1
  38. package/dist/syncer/syncer.js +27 -13
  39. package/dist/tasks/workflow-manager.d.ts +3 -3
  40. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  41. package/dist/tasks/workflow-manager.js +15 -11
  42. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  43. package/dist/template/implementations/generated.template.js +8 -6
  44. package/dist/template/implementations/model.template.js +5 -5
  45. package/dist/template/implementations/services.template.d.ts +17 -0
  46. package/dist/template/implementations/services.template.d.ts.map +1 -0
  47. package/dist/template/implementations/services.template.js +159 -0
  48. package/dist/template/implementations/view_form.template.js +2 -2
  49. package/dist/template/implementations/view_id_async_select.template.js +2 -2
  50. package/dist/template/implementations/view_list.template.js +5 -5
  51. package/dist/types/types.d.ts +43 -25
  52. package/dist/types/types.d.ts.map +1 -1
  53. package/dist/types/types.js +29 -17
  54. package/dist/ui/ai-api.d.ts +2 -0
  55. package/dist/ui/ai-api.d.ts.map +1 -1
  56. package/dist/ui/ai-api.js +43 -49
  57. package/dist/ui/ai-client.d.ts +10 -0
  58. package/dist/ui/ai-client.d.ts.map +1 -1
  59. package/dist/ui/ai-client.js +457 -437
  60. package/dist/ui/api.d.ts.map +1 -1
  61. package/dist/ui/api.js +14 -3
  62. package/dist/ui-web/assets/{index-J9MCfjCd.js → index-DzqUrTB-.js} +56 -59
  63. package/dist/ui-web/index.html +1 -1
  64. package/package.json +12 -8
  65. package/src/api/config.ts +3 -0
  66. package/src/api/decorators.ts +6 -1
  67. package/src/api/sonamu.ts +71 -52
  68. package/src/database/base-model.ts +66 -11
  69. package/src/database/base-model.types.ts +79 -76
  70. package/src/database/puri.ts +5 -1
  71. package/src/database/puri.types.ts +3 -6
  72. package/src/entity/entity.ts +83 -34
  73. package/src/index.ts +1 -0
  74. package/src/shared/app.shared.ts.txt +1 -1
  75. package/src/shared/web.shared.ts.txt +0 -43
  76. package/src/syncer/checksum.ts +31 -9
  77. package/src/syncer/code-generator.ts +8 -1
  78. package/src/syncer/syncer.ts +38 -26
  79. package/src/tasks/workflow-manager.ts +16 -12
  80. package/src/template/implementations/generated.template.ts +17 -3
  81. package/src/template/implementations/model.template.ts +4 -4
  82. package/src/template/implementations/services.template.ts +226 -0
  83. package/src/template/implementations/view_form.template.ts +1 -1
  84. package/src/template/implementations/view_id_async_select.template.ts +1 -1
  85. package/src/template/implementations/view_list.template.ts +4 -4
  86. package/src/types/types.ts +33 -16
  87. package/src/ui/ai-api.ts +61 -60
  88. package/src/ui/ai-client.ts +535 -499
  89. package/src/ui/api.ts +14 -2
  90. package/src/ui/entity.instructions.md +536 -0
  91. package/dist/template/implementations/service.template.d.ts +0 -29
  92. package/dist/template/implementations/service.template.d.ts.map +0 -1
  93. package/dist/template/implementations/service.template.js +0 -202
  94. package/dist/ui-web/assets/provider-utils_false-BKJD46kk.js +0 -1
  95. package/dist/ui-web/assets/provider-utils_false-Bu5lmX18.js +0 -1
  96. package/src/template/implementations/service.template.ts +0 -328
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>{{projectName}}: Sonamu UI</title>
7
- <script type="module" crossorigin src="/sonamu-ui/assets/index-J9MCfjCd.js"></script>
7
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-DzqUrTB-.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-CpaB9P6g.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.7.15",
3
+ "version": "0.7.17",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -81,10 +81,10 @@
81
81
  "tsicli": "^1.0.5",
82
82
  "vitest": "^4.0.10",
83
83
  "zod": "^4.1.12",
84
- "@sonamu-kit/hmr-hook": "^0.4.1",
84
+ "@sonamu-kit/hmr-runner": "^0.1.1",
85
+ "@sonamu-kit/tasks": "^0.1.1",
85
86
  "@sonamu-kit/ts-loader": "^2.1.3",
86
- "@sonamu-kit/tasks": "^0.1.0",
87
- "@sonamu-kit/hmr-runner": "^0.1.1"
87
+ "@sonamu-kit/hmr-hook": "^0.4.1"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@biomejs/biome": "^2.3.7",
@@ -100,12 +100,13 @@
100
100
  "typescript": "^5.9.3"
101
101
  },
102
102
  "peerDependencies": {
103
- "@ai-sdk/openai": "3.0.0-beta.75",
104
- "@ai-sdk/provider": "3.0.0-beta.22",
105
- "@ai-sdk/provider-utils": "4.0.0-beta.40",
103
+ "@ai-sdk/openai": "3.0.0",
104
+ "@ai-sdk/provider": "3.0.0",
105
+ "@ai-sdk/provider-utils": "4.0.0",
106
+ "@ai-sdk/anthropic": "3.0.0",
106
107
  "@swc/cli": "^0.7.8",
107
108
  "@swc/core": "^1.13.5",
108
- "ai": "6.0.0-beta.138",
109
+ "ai": "6.0.1",
109
110
  "fastify": "^4.23.2",
110
111
  "knex": "^3.1.0",
111
112
  "pgvector": "^0.2.1",
@@ -122,6 +123,9 @@
122
123
  "@ai-sdk/provider-utils": {
123
124
  "optional": true
124
125
  },
126
+ "@ai-sdk/anthropic": {
127
+ "optional": true
128
+ },
125
129
  "ai": {
126
130
  "optional": true
127
131
  },
package/src/api/config.ts CHANGED
@@ -53,6 +53,9 @@ export type SonamuConfig = {
53
53
  };
54
54
 
55
55
  export type SonamuServerOptions = {
56
+ // 프로젝트 외부에서 접근할 수 있는 URL. 기본값은 {server.listen.host}:{server.listen.port} 입니다.
57
+ baseUrl?: string;
58
+
56
59
  fastify?: FastifyServerOptions;
57
60
 
58
61
  listen?: {
@@ -21,7 +21,12 @@ export interface GuardKeys {
21
21
  user: true;
22
22
  }
23
23
  export type GuardKey = keyof GuardKeys;
24
- export type ServiceClient = "axios" | "axios-multipart" | "swr" | "window-fetch";
24
+ export type ServiceClient =
25
+ | "axios"
26
+ | "axios-multipart"
27
+ | "tanstack-query"
28
+ | "tanstack-mutation"
29
+ | "window-fetch";
25
30
  export type ApiDecoratorOptions = {
26
31
  httpMethod?: HTTPMethods;
27
32
  contentType?:
package/src/api/sonamu.ts CHANGED
@@ -363,7 +363,7 @@ class SonamuClass {
363
363
  (api.options.httpMethod ?? "GET") === request.method.toUpperCase(),
364
364
  );
365
365
  if (found) {
366
- return this.getApiHandler(found, config)(request, reply);
366
+ return this.createApiHandler(found, config)(request, reply);
367
367
  }
368
368
 
369
369
  if (request.url.startsWith("/api/")) {
@@ -385,13 +385,13 @@ class SonamuClass {
385
385
  server.route({
386
386
  method: api.options.httpMethod ?? "GET",
387
387
  url: this.config.api.route.prefix + api.path,
388
- handler: this.getApiHandler(api, config),
388
+ handler: this.createApiHandler(api, config),
389
389
  }); // END server.route
390
390
  }
391
391
  }
392
392
  }
393
393
 
394
- getApiHandler(
394
+ createApiHandler(
395
395
  api: ExtendedApi,
396
396
  config: SonamuFastifyConfig,
397
397
  ): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown> {
@@ -429,56 +429,74 @@ class SonamuClass {
429
429
  // Content-Type
430
430
  reply.type(api.options.contentType ?? "application/json");
431
431
 
432
- // createSSEFactory 함수에 미리 request의 socket과 reply를 바인딩.
433
- const { createSSEFactory } = await import("../stream/sse");
434
- const createSSE = (<T extends ZodObject>(
435
- _request: FastifyRequest,
436
- _reply: FastifyReply,
437
- _events: T,
438
- ) => createSSEFactory(_request.socket, _reply, _events)).bind(null, request, reply);
439
-
440
- const context: Context = {
441
- ...(await Promise.resolve(
442
- config.contextProvider(
443
- {
444
- request,
445
- reply,
446
- headers: request.headers,
447
- createSSE,
448
- naiteStore: Naite.createStore(),
449
- // auth
450
- user: request.user ?? null,
451
- passport: {
452
- login: request.login.bind(request) as AuthContext["passport"]["login"],
453
- logout: request.logout.bind(request) as AuthContext["passport"]["logout"],
454
- },
455
- },
456
- request,
457
- reply,
458
- ),
459
- )),
460
- };
461
-
462
- const model = this.syncer.models[api.modelName];
463
- return this.asyncLocalStorage.run({ context }, async () => {
464
- const { ApiParamType } = await import("../types/types");
465
- // biome-ignore lint/suspicious/noExplicitAny: model은 모델 인스턴스이므로 메서드 호출 가능
466
- const result = await (model as any)[api.methodName].apply(
467
- model,
468
- api.parameters.map((param) => {
469
- // Context 인젝션
470
- if (ApiParamType.isContext(param.type)) {
471
- return context;
472
- } else {
473
- return reqBody[param.name];
474
- }
475
- }),
476
- );
477
- reply.type(api.options.contentType ?? "application/json");
432
+ // Context 생성
433
+ const context: Context = await this.createContext(config, request, reply);
478
434
 
479
- return result;
435
+ // 모델 메소드 args 생성하여 호출
436
+ const { ApiParamType } = await import("../types/types");
437
+ const args = api.parameters.map((param) => {
438
+ // Context 인젝션
439
+ if (ApiParamType.isContext(param.type)) {
440
+ return context;
441
+ } else {
442
+ return reqBody[param.name];
443
+ }
480
444
  });
445
+ return this.invokeModelMethod(api, args, context, reply);
446
+ };
447
+ }
448
+
449
+ async invokeModelMethod(
450
+ api: ExtendedApi,
451
+ args: unknown[],
452
+ context: Context,
453
+ reply: FastifyReply,
454
+ ): Promise<unknown> {
455
+ const model = this.syncer.models[api.modelName];
456
+ return this.asyncLocalStorage.run({ context }, async () => {
457
+ // biome-ignore lint/suspicious/noExplicitAny: model은 모델 인스턴스이므로 메서드 호출 가능
458
+ const result = await (model as any)[api.methodName].apply(model, args);
459
+ reply.type(api.options.contentType ?? "application/json");
460
+
461
+ return result;
462
+ });
463
+ }
464
+
465
+ async createContext(
466
+ config: SonamuFastifyConfig,
467
+ request: FastifyRequest,
468
+ reply: FastifyReply,
469
+ ): Promise<Context> {
470
+ // createSSEFactory 함수에 미리 request의 socket과 reply를 바인딩.
471
+ const { createSSEFactory } = await import("../stream/sse");
472
+ const createSSE = (<T extends ZodObject>(
473
+ _request: FastifyRequest,
474
+ _reply: FastifyReply,
475
+ _events: T,
476
+ ) => createSSEFactory(_request.socket, _reply, _events)).bind(null, request, reply);
477
+
478
+ const context: Context = {
479
+ ...(await Promise.resolve(
480
+ config.contextProvider(
481
+ {
482
+ request,
483
+ reply,
484
+ headers: request.headers,
485
+ createSSE,
486
+ naiteStore: Naite.createStore(),
487
+ // auth
488
+ user: request.user ?? null,
489
+ passport: {
490
+ login: request.login.bind(request) as AuthContext["passport"]["login"],
491
+ logout: request.logout.bind(request) as AuthContext["passport"]["logout"],
492
+ },
493
+ },
494
+ request,
495
+ reply,
496
+ ),
497
+ )),
481
498
  };
499
+ return context;
482
500
  }
483
501
 
484
502
  async startWatcher(): Promise<void> {
@@ -595,7 +613,7 @@ class SonamuClass {
595
613
  private async initializeWorkflows(options: SonamuTaskOptions | undefined) {
596
614
  const { WorkflowManager } = await import("../tasks/workflow-manager");
597
615
  // NOTE: @sonamu-kit/tasks 안에선 knex config를 수정하기 때문에 connection이 아닌 config 째로 보냅니다.
598
- this._workflows = await WorkflowManager.create(DB.getDBConfig("w"), true);
616
+ this._workflows = new WorkflowManager(DB.getDBConfig("w"));
599
617
  if (!options) {
600
618
  return;
601
619
  }
@@ -608,7 +626,7 @@ class SonamuClass {
608
626
  };
609
627
 
610
628
  if (enableWorker) {
611
- await this.workflows.setupWorker({
629
+ this.workflows.setupWorker({
612
630
  ...defaultWorkerOptions,
613
631
  ...options.workerOptions,
614
632
  });
@@ -645,6 +663,7 @@ class SonamuClass {
645
663
  server
646
664
  .listen({ port, host })
647
665
  .then(async () => {
666
+ await this.workflows.startWorker();
648
667
  await options.lifecycle?.onStart?.(server);
649
668
  })
650
669
  .catch(async (err) => {
@@ -1,20 +1,18 @@
1
1
  /** biome-ignore-all lint/suspicious/noExplicitAny: Puri의 타입은 개별 모델에서 확정되므로 BaseModel에서는 any를 허용함 */
2
2
 
3
3
  import type { Knex } from "knex";
4
- import { group, isObject, omit, set } from "radashi";
4
+ import { cloneDeep, group, isObject, omit, set } from "radashi";
5
5
  import type { ListResult } from "..";
6
6
  import { Sonamu } from "../api";
7
+ import { EntityManager } from "../entity/entity-manager";
7
8
  import type { DatabaseSchemaExtend, SonamuQueryMode } from "../types/types";
8
9
  import { getJoinTables, getTableNamesFromWhere } from "../utils/sql-parser";
9
10
  import { chunk } from "../utils/utils";
10
- import type {
11
- EnhancerMap,
12
- ResolveSubsetIntersection,
13
- UnionExtractedTTables,
14
- } from "./base-model.types";
11
+ import type { EnhancerMap, ResolveSubsetIntersection } from "./base-model.types";
15
12
  import type { DBPreset } from "./db";
16
13
  import { DB } from "./db";
17
14
  import { Puri } from "./puri";
15
+ import type { UnionExtractedTTables } from "./puri.types";
18
16
  import type { InferAllSubsets, PuriLoaderQueries, PuriSubsetFn } from "./puri-subset.types";
19
17
  import { PuriWrapper } from "./puri-wrapper";
20
18
  import { UpsertBuilder } from "./upsert-builder";
@@ -137,7 +135,12 @@ export class BaseModelClass<
137
135
  * 타입 검증 및 추론을 도와줌
138
136
  */
139
137
  createEnhancers<T extends TSubsetKey>(
140
- enhancers: EnhancerMap<T, InferAllSubsets<TSubsetQueries, TLoaderQueries>, TSubsetMapping>,
138
+ enhancers: EnhancerMap<
139
+ T,
140
+ InferAllSubsets<TSubsetQueries, TLoaderQueries>,
141
+ TSubsetMapping,
142
+ TSubsetQueries
143
+ >,
141
144
  ) {
142
145
  return enhancers;
143
146
  }
@@ -169,7 +172,7 @@ export class BaseModelClass<
169
172
  };
170
173
  debug?: boolean;
171
174
  optimizeCountQuery?: boolean;
172
- } & EnhancerParam<TSubsetKey, TComputedResults, TSubsetMapping>,
175
+ } & EnhancerParam<TSubsetKey, TComputedResults, TSubsetMapping, TSubsetQueries>,
173
176
  ): Promise<ListResult<LP, TSubsetMapping[T]>> {
174
177
  const { subset, qb, params: queryParams, debug = false, optimizeCountQuery = false } = params;
175
178
 
@@ -191,10 +194,18 @@ export class BaseModelClass<
191
194
 
192
195
  // Enhancer 적용
193
196
  const enhancer = (params as any).enhancers?.[subset];
194
- const rows = (await Promise.all(
197
+ const enhancedRows = (await Promise.all(
195
198
  computedRows.map((row) => enhancer?.(row) ?? row),
196
199
  )) as TSubsetMapping[T][];
197
200
 
201
+ // Internal 필드 제거
202
+ const entity = EntityManager.get(this.modelName);
203
+ const internalFields = entity.subsetsInternal[subset] ?? [];
204
+ const rows =
205
+ internalFields.length > 0
206
+ ? enhancedRows.map((row) => this.omitInternalFields(row, internalFields))
207
+ : enhancedRows;
208
+
198
209
  if (queryParams.queryMode === "list") {
199
210
  // 리스트만 리턴
200
211
  return { rows } as ListResult<LP, TSubsetMapping[T]>;
@@ -204,6 +215,49 @@ export class BaseModelClass<
204
215
  }
205
216
  }
206
217
 
218
+ /**
219
+ * 객체에서 internal 필드 제거
220
+ * 중첩 필드(예: "user.email") 및 배열(예: "employees.salary")도 처리
221
+ */
222
+ omitInternalFields<T extends object>(row: T, fields: string[]): T {
223
+ const result = cloneDeep(row);
224
+ for (const field of fields) {
225
+ this.deleteField(result, field.split("."));
226
+ }
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * 중첩 필드 삭제 (배열 내 객체도 처리)
232
+ */
233
+ deleteField(obj: any, parts: string[]): void {
234
+ if (!obj || typeof obj !== "object") {
235
+ return;
236
+ }
237
+
238
+ if (parts.length === 1) {
239
+ if (Array.isArray(obj)) {
240
+ obj.forEach((item) => {
241
+ if (item && typeof item === "object") {
242
+ delete item[parts[0]];
243
+ }
244
+ });
245
+ } else {
246
+ delete obj[parts[0]];
247
+ }
248
+ return;
249
+ }
250
+
251
+ const [first, ...rest] = parts;
252
+ const next = obj[first];
253
+
254
+ if (Array.isArray(next)) {
255
+ next.map((item) => this.deleteField(item, rest));
256
+ } else if (next && typeof next === "object") {
257
+ this.deleteField(next, rest);
258
+ }
259
+ }
260
+
207
261
  /**
208
262
  * COUNT 쿼리 실행 (내부 메서드)
209
263
  */
@@ -401,9 +455,10 @@ type EnhancerParam<
401
455
  TSubsetKey extends string,
402
456
  TComputedResults extends Record<TSubsetKey, any>,
403
457
  TSubsetMapping extends Record<TSubsetKey, any>,
458
+ TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn>,
404
459
  > = [RequiredEnhancerKeys<TSubsetKey, TComputedResults, TSubsetMapping>] extends [never]
405
- ? { enhancers?: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping> }
406
- : { enhancers: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping> };
460
+ ? { enhancers?: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping, TSubsetQueries> }
461
+ : { enhancers: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping, TSubsetQueries> };
407
462
 
408
463
  type RequiredEnhancerKeys<
409
464
  TSubsetKey extends string,
@@ -7,29 +7,11 @@
7
7
  * Enhancer, SubsetQuery 교집합 등 Model 계층에서 필요한 타입 정의.
8
8
  */
9
9
 
10
- import type { ListResult } from "..";
11
10
  import type { DatabaseSchemaExtend } from "../types/types";
12
11
  import type { Puri } from "./puri";
12
+ import type { ExtractTTables } from "./puri.types";
13
13
  import type { PuriSubsetFn } from "./puri-subset.types";
14
14
 
15
- // ============================================================================
16
- // Puri 테이블 추출 유틸리티
17
- // ============================================================================
18
-
19
- /**
20
- * Puri 인스턴스에서 TTables 타입 추출
21
- */
22
- export type ExtractPuriTables<T> = T extends Puri<any, infer TTables, any> ? TTables : never;
23
-
24
- /**
25
- * SubsetQueries에서 모든 TTables의 유니온 추출
26
- * getSubsetQueries의 qb 타입 정의에 사용
27
- */
28
- export type UnionExtractedTTables<
29
- TSubsetKey extends string,
30
- TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn>,
31
- > = ExtractPuriTables<ReturnType<TSubsetQueries[TSubsetKey]>>;
32
-
33
15
  // ============================================================================
34
16
  // Subset 교집합 계산 (onSubset 메서드용)
35
17
  // ============================================================================
@@ -37,11 +19,12 @@ export type UnionExtractedTTables<
37
19
  /**
38
20
  * 두 Puri의 테이블 교집합을 가진 새로운 Puri 생성
39
21
  */
40
- type MergePuriTables<A, B, TA = ExtractPuriTables<A>, TB = ExtractPuriTables<B>> = Puri<
41
- DatabaseSchemaExtend,
42
- Pick<TA, Extract<keyof TA, keyof TB>>,
43
- any
44
- >;
22
+ type MergePuriTables<
23
+ A extends Puri<any, any, any>,
24
+ B extends Puri<any, any, any>,
25
+ TA = ExtractTTables<A>,
26
+ TB = ExtractTTables<B>,
27
+ > = Puri<DatabaseSchemaExtend, Pick<TA, Extract<keyof TA, keyof TB>>, any>;
45
28
 
46
29
  /**
47
30
  * 서브셋 키 배열을 순회하며 테이블 교집합 Puri 계산
@@ -62,24 +45,79 @@ export type ResolveSubsetIntersection<
62
45
  // Enhancer
63
46
  // ============================================================================
64
47
 
48
+ /**
49
+ * SubsetQueries의 Puri 반환 타입에서 TTables를 추출하고,
50
+ * TTables의 키 중 DatabaseSchemaExtend의 키와 일치하는 것이 메인 테이블.
51
+ * 해당 테이블의 BaseSchema에서 __virtual_query__ 키를 추출.
52
+ */
53
+ type ExtractMainTable<TSubsetQueries extends Record<string, PuriSubsetFn>> = Extract<
54
+ keyof ExtractTTables<ReturnType<TSubsetQueries[keyof TSubsetQueries]>>,
55
+ keyof DatabaseSchemaExtend
56
+ >;
57
+
58
+ type ExtractVirtualQueryKeys<TSubsetQueries extends Record<string, PuriSubsetFn>> =
59
+ ExtractMainTable<TSubsetQueries> extends infer TTable extends keyof DatabaseSchemaExtend
60
+ ? DatabaseSchemaExtend[TTable] extends { __virtual_query__: readonly (infer K)[] }
61
+ ? K
62
+ : never
63
+ : never;
64
+
65
+ /**
66
+ * TSubsetMapping에서 virtualQuery 키를 optional로 만든 타입
67
+ * Enhancer 필수 여부 판단 시 사용
68
+ */
69
+ type OmitVirtualQueryFromMapping<TMapping, TVirtualQueryKeys> = Omit<
70
+ TMapping,
71
+ TVirtualQueryKeys & keyof TMapping
72
+ > &
73
+ Partial<Pick<TMapping, TVirtualQueryKeys & keyof TMapping>>;
74
+
75
+ /**
76
+ * Computed가 Mapping에 호환되는지 판단 (virtualQuery 키 제외)
77
+ */
78
+ type IsEnhancerOptional<
79
+ TSubsetKey extends string,
80
+ TComputedResults extends Record<TSubsetKey, any>,
81
+ TSubsetMapping extends Record<TSubsetKey, any>,
82
+ TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn>,
83
+ K extends TSubsetKey,
84
+ > = TComputedResults[K] extends OmitVirtualQueryFromMapping<
85
+ TSubsetMapping[K],
86
+ ExtractVirtualQueryKeys<TSubsetQueries>
87
+ >
88
+ ? true
89
+ : false;
90
+
65
91
  /**
66
92
  * 단일 Enhancer 함수 타입
67
- * computed 결과를 받아 최종 mapping 타입으로 변환
93
+ * computed 결과 + virtualQuery props를 받아 최종 mapping 타입으로 변환
68
94
  */
69
- export type EnhancerFn<TComputed, TMapping> = (row: TComputed) => TMapping | Promise<TMapping>;
95
+ type EnhancerFnWithVirtualQuery<TComputed, TMapping, TVirtualQueryKeys> = (
96
+ row: TComputed & Pick<TMapping, TVirtualQueryKeys & keyof TMapping>,
97
+ ) => TMapping | Promise<TMapping>;
70
98
 
71
99
  /**
72
100
  * Enhancer가 필수인 SubsetKey 추출
73
101
  *
74
102
  * ComputedResults[K]가 SubsetMapping[K]에 할당 불가능하면 해당 K는 필수
75
103
  * (즉, virtual 필드 등 추가 변환이 필요한 경우)
104
+ * 단, virtualQuery 키는 무시 (사용자가 appendSelect로 직접 추가)
76
105
  */
77
106
  export type RequiredEnhancerKeys<
78
107
  TSubsetKey extends string,
79
108
  TComputedResults extends Record<TSubsetKey, any>,
80
109
  TSubsetMapping extends Record<TSubsetKey, any>,
110
+ TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn>,
81
111
  > = {
82
- [K in TSubsetKey]: TComputedResults[K] extends TSubsetMapping[K] ? never : K;
112
+ [K in TSubsetKey]: IsEnhancerOptional<
113
+ TSubsetKey,
114
+ TComputedResults,
115
+ TSubsetMapping,
116
+ TSubsetQueries,
117
+ K
118
+ > extends true
119
+ ? never
120
+ : K;
83
121
  }[TSubsetKey];
84
122
 
85
123
  /**
@@ -87,6 +125,7 @@ export type RequiredEnhancerKeys<
87
125
  *
88
126
  * - ComputedResults[K]가 SubsetMapping[K]에 assignable하면 → enhancer 선택적
89
127
  * - 그렇지 않으면 → enhancer 필수
128
+ * - 단, virtualQuery 키는 무시 (사용자가 appendSelect로 직접 추가)
90
129
  *
91
130
  * @example
92
131
  * // virtual 필드 employee_count가 있는 경우
@@ -98,59 +137,23 @@ export type EnhancerMap<
98
137
  TSubsetKey extends string,
99
138
  TComputedResults extends Record<TSubsetKey, any>,
100
139
  TSubsetMapping extends Record<TSubsetKey, any>,
140
+ TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn>,
141
+ TRequiredKeys extends TSubsetKey = RequiredEnhancerKeys<
142
+ TSubsetKey,
143
+ TComputedResults,
144
+ TSubsetMapping,
145
+ TSubsetQueries
146
+ >,
101
147
  > = {
102
- // Computed가 Mapping에 호환되면 선택적
103
- [K in TSubsetKey as TComputedResults[K] extends TSubsetMapping[K] ? K : never]?: EnhancerFn<
148
+ [K in Exclude<TSubsetKey, TRequiredKeys>]?: EnhancerFnWithVirtualQuery<
104
149
  TComputedResults[K],
105
- TSubsetMapping[K]
150
+ TSubsetMapping[K],
151
+ ExtractVirtualQueryKeys<TSubsetQueries>
106
152
  >;
107
153
  } & {
108
- // 호환되지 않으면 필수
109
- [K in TSubsetKey as TComputedResults[K] extends TSubsetMapping[K] ? never : K]: EnhancerFn<
154
+ [K in TRequiredKeys]: EnhancerFnWithVirtualQuery<
110
155
  TComputedResults[K],
111
- TSubsetMapping[K]
156
+ TSubsetMapping[K],
157
+ ExtractVirtualQueryKeys<TSubsetQueries>
112
158
  >;
113
159
  };
114
-
115
- // ============================================================================
116
- // executeSubsetQuery
117
- // ============================================================================
118
-
119
- /**
120
- * executeSubsetQuery 기본 파라미터
121
- */
122
- export type ExecuteSubsetQueryBaseParams<TSubsetKey extends string> = {
123
- subset: TSubsetKey;
124
- qb: Puri<any, any, any>;
125
- params: {
126
- num?: number;
127
- page?: number;
128
- queryMode?: "list" | "count" | "both";
129
- };
130
- debug?: boolean;
131
- optimizeCountQuery?: boolean;
132
- };
133
-
134
- /**
135
- * executeSubsetQuery 파라미터 (Enhancer 포함)
136
- *
137
- * RequiredEnhancerKeys가 없으면 enhancers 선택적, 있으면 필수
138
- */
139
- export type ExecuteSubsetQueryParams<
140
- TSubsetKey extends string,
141
- TComputedResults extends Record<TSubsetKey, any>,
142
- TSubsetMapping extends Record<TSubsetKey, any>,
143
- T extends TSubsetKey,
144
- > = ExecuteSubsetQueryBaseParams<T> &
145
- ([RequiredEnhancerKeys<TSubsetKey, TComputedResults, TSubsetMapping>] extends [never]
146
- ? { enhancers?: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping> }
147
- : { enhancers: EnhancerMap<TSubsetKey, TComputedResults, TSubsetMapping> });
148
-
149
- /**
150
- * executeSubsetQuery 반환 타입
151
- */
152
- export type ExecuteSubsetQueryResult<
153
- TSubsetMapping extends Record<string, any>,
154
- T extends string,
155
- LP extends { queryMode?: "list" | "count" | "both" },
156
- > = ListResult<LP, TSubsetMapping[T]>;
@@ -1205,12 +1205,14 @@ export class JoinClauseGroup<
1205
1205
  TResolved: 쿼리 실행 후 반환될 결과 타입
1206
1206
  TReturning: RETURNING 절에 사용될 타입
1207
1207
  */
1208
- export class ResolvedPuri<TResolved, TReturning> {
1208
+ export class ResolvedPuri<TResolved, TReturning> implements Promise<TResolved> {
1209
1209
  constructor(
1210
1210
  public knexQuery: Knex.QueryBuilder,
1211
1211
  private knex: Knex,
1212
1212
  ) {}
1213
1213
 
1214
+ [Symbol.toStringTag]: string = "Promise";
1215
+
1214
1216
  toQuery(): string {
1215
1217
  return this.knexQuery.toQuery();
1216
1218
  }
@@ -1227,11 +1229,13 @@ export class ResolvedPuri<TResolved, TReturning> {
1227
1229
  Naite.t("puri:executed-query", this.toQuery());
1228
1230
  return this.knexQuery.then(onfulfilled as any, onrejected);
1229
1231
  }
1232
+
1230
1233
  catch<TResult2 = never>(
1231
1234
  onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
1232
1235
  ): Promise<TResolved | TResult2> {
1233
1236
  return this.knexQuery.catch(onrejected);
1234
1237
  }
1238
+
1235
1239
  finally(onfinally?: (() => void) | null): Promise<TResolved> {
1236
1240
  return this.knexQuery.finally(onfinally);
1237
1241
  }
@@ -3,7 +3,7 @@
3
3
  import type { QueryResult } from "pg";
4
4
  import type { DatabaseForeignKeys, DatabaseSchemaExtend } from "../types/types";
5
5
  import type { Puri } from "./puri";
6
- import type { PuriWrapper } from "./puri-wrapper";
6
+ import type { PuriSubsetFn } from "./puri-subset.types";
7
7
 
8
8
  // ============================================
9
9
  // 내부 타입 키 (메타데이터)
@@ -342,15 +342,12 @@ export type InsertData<T> = Omit<
342
342
  export type InsertResult = Pick<QueryResult<any>, "command" | "rowCount" | "rows" | "oid">;
343
343
 
344
344
  // SubsetQuery를 위한 타입 유틸리티
345
- type ExtractTTables<T extends Puri<any, any, any>> = T extends Puri<any, infer TTables, any>
345
+ export type ExtractTTables<T extends Puri<any, any, any>> = T extends Puri<any, infer TTables, any>
346
346
  ? TTables
347
347
  : never;
348
348
  export type UnionExtractedTTables<
349
349
  SubsetKey extends string,
350
- SubsetQueries extends Record<
351
- SubsetKey,
352
- (qbWrapper: PuriWrapper<DatabaseSchemaExtend>) => Puri<any, any, any>
353
- >,
350
+ SubsetQueries extends Record<SubsetKey, PuriSubsetFn>,
354
351
  > = {
355
352
  [K in SubsetKey]: ExtractTTables<ReturnType<SubsetQueries[K]>>;
356
353
  }[SubsetKey];