sonamu 0.7.4 → 0.7.5

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 (133) hide show
  1. package/dist/api/config.d.ts +1 -4
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +2 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +19 -47
  7. package/dist/bin/cli.js +6 -6
  8. package/dist/database/base-model.d.ts +1 -1
  9. package/dist/database/base-model.d.ts.map +1 -1
  10. package/dist/database/base-model.js +15 -4
  11. package/dist/database/code-generator.d.ts.map +1 -1
  12. package/dist/database/code-generator.js +3 -3
  13. package/dist/database/db.d.ts.map +1 -1
  14. package/dist/database/db.js +1 -1
  15. package/dist/database/puri-wrapper.d.ts +11 -11
  16. package/dist/database/puri-wrapper.d.ts.map +1 -1
  17. package/dist/database/puri-wrapper.js +7 -11
  18. package/dist/database/puri.d.ts +36 -17
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +54 -7
  21. package/dist/database/puri.types.d.ts +54 -17
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +2 -4
  24. package/dist/database/puri.types.test-d.js +129 -0
  25. package/dist/database/upsert-builder.d.ts +16 -10
  26. package/dist/database/upsert-builder.d.ts.map +1 -1
  27. package/dist/database/upsert-builder.js +10 -19
  28. package/dist/entity/entity-manager.d.ts +113 -22
  29. package/dist/entity/entity-manager.d.ts.map +1 -1
  30. package/dist/entity/entity-manager.js +1 -1
  31. package/dist/entity/entity.d.ts +34 -0
  32. package/dist/entity/entity.d.ts.map +1 -1
  33. package/dist/entity/entity.js +110 -37
  34. package/dist/index.d.ts +5 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -2
  37. package/dist/migration/code-generation.d.ts.map +1 -1
  38. package/dist/migration/code-generation.js +341 -149
  39. package/dist/migration/migration-set.d.ts.map +1 -1
  40. package/dist/migration/migration-set.js +21 -5
  41. package/dist/migration/migrator.d.ts.map +1 -1
  42. package/dist/migration/migrator.js +7 -1
  43. package/dist/migration/postgresql-schema-reader.d.ts +11 -1
  44. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  45. package/dist/migration/postgresql-schema-reader.js +111 -10
  46. package/dist/syncer/syncer.d.ts.map +1 -1
  47. package/dist/syncer/syncer.js +4 -3
  48. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  49. package/dist/template/implementations/generated.template.js +12 -2
  50. package/dist/template/implementations/generated_sso.template.d.ts +3 -3
  51. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  52. package/dist/template/implementations/generated_sso.template.js +50 -2
  53. package/dist/template/implementations/model.template.js +6 -6
  54. package/dist/template/implementations/model_test.template.js +4 -4
  55. package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
  56. package/dist/template/implementations/view_enums_select.template.js +2 -2
  57. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  58. package/dist/template/implementations/view_form.template.js +12 -9
  59. package/dist/template/implementations/view_id_async_select.template.js +4 -4
  60. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  61. package/dist/template/implementations/view_list.template.js +12 -9
  62. package/dist/template/implementations/view_search_input.template.js +2 -2
  63. package/dist/template/template.js +2 -2
  64. package/dist/template/zod-converter.d.ts.map +1 -1
  65. package/dist/template/zod-converter.js +17 -2
  66. package/dist/testing/fixture-manager.d.ts +2 -1
  67. package/dist/testing/fixture-manager.d.ts.map +1 -1
  68. package/dist/testing/fixture-manager.js +29 -29
  69. package/dist/types/types.d.ts +593 -68
  70. package/dist/types/types.d.ts.map +1 -1
  71. package/dist/types/types.js +113 -9
  72. package/dist/vector/chunking.d.ts +25 -0
  73. package/dist/vector/chunking.d.ts.map +1 -0
  74. package/dist/vector/chunking.js +97 -0
  75. package/dist/vector/config.d.ts +12 -0
  76. package/dist/vector/config.d.ts.map +1 -0
  77. package/dist/vector/config.js +83 -0
  78. package/dist/vector/embedding.d.ts +42 -0
  79. package/dist/vector/embedding.d.ts.map +1 -0
  80. package/dist/vector/embedding.js +147 -0
  81. package/dist/vector/types.d.ts +105 -0
  82. package/dist/vector/types.d.ts.map +1 -0
  83. package/dist/vector/types.js +5 -0
  84. package/dist/vector/vector-search.d.ts +47 -0
  85. package/dist/vector/vector-search.d.ts.map +1 -0
  86. package/dist/vector/vector-search.js +176 -0
  87. package/package.json +9 -8
  88. package/src/api/config.ts +0 -4
  89. package/src/api/sonamu.ts +21 -36
  90. package/src/bin/cli.ts +5 -5
  91. package/src/database/base-model.ts +20 -11
  92. package/src/database/code-generator.ts +6 -2
  93. package/src/database/db.ts +1 -0
  94. package/src/database/puri-wrapper.ts +22 -16
  95. package/src/database/puri.ts +150 -27
  96. package/src/database/puri.types.test-d.ts +457 -0
  97. package/src/database/puri.types.ts +231 -33
  98. package/src/database/upsert-builder.ts +43 -34
  99. package/src/entity/entity-manager.ts +2 -2
  100. package/src/entity/entity.ts +134 -44
  101. package/src/index.ts +6 -0
  102. package/src/migration/code-generation.ts +377 -174
  103. package/src/migration/migration-set.ts +22 -3
  104. package/src/migration/migrator.ts +6 -0
  105. package/src/migration/postgresql-schema-reader.ts +121 -21
  106. package/src/syncer/syncer.ts +3 -2
  107. package/src/template/implementations/generated.template.ts +51 -9
  108. package/src/template/implementations/generated_sso.template.ts +71 -2
  109. package/src/template/implementations/model.template.ts +5 -5
  110. package/src/template/implementations/model_test.template.ts +3 -3
  111. package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
  112. package/src/template/implementations/view_enums_select.template.ts +1 -1
  113. package/src/template/implementations/view_form.template.ts +11 -8
  114. package/src/template/implementations/view_id_async_select.template.ts +3 -3
  115. package/src/template/implementations/view_list.template.ts +11 -8
  116. package/src/template/implementations/view_search_input.template.ts +1 -1
  117. package/src/template/template.ts +1 -1
  118. package/src/template/zod-converter.ts +20 -0
  119. package/src/testing/fixture-manager.ts +31 -30
  120. package/src/types/types.ts +226 -48
  121. package/src/vector/chunking.ts +115 -0
  122. package/src/vector/config.ts +68 -0
  123. package/src/vector/embedding.ts +193 -0
  124. package/src/vector/types.ts +122 -0
  125. package/src/vector/vector-search.ts +261 -0
  126. package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
  127. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
  128. package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
  129. package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
  130. package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
  131. package/dist/template/implementations/view_list_columns.template.js +0 -49
  132. package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
  133. package/src/template/implementations/view_list_columns.template.ts +0 -53
@@ -86,6 +86,9 @@ export class Template__view_list extends Template {
86
86
  }
87
87
  case "array":
88
88
  return `<>{ /* array ${colName} */ }</>`;
89
+ case "vector":
90
+ // vector 타입은 차원 수만 표시 (실제 데이터는 너무 김)
91
+ return `<>{${col.nullable ? `${colName} ? ` : ""}[Vector: {${colName}${col.nullable ? "" : " ?? []"}.length}d]${col.nullable ? " : '-'" : ""}}</>`;
89
92
  default:
90
93
  throw new Error(`렌더 불가 컬럼 ${col.renderType}`);
91
94
  }
@@ -98,7 +101,7 @@ export class Template__view_list extends Template {
98
101
  ): (string | null)[] {
99
102
  if (col.renderType === "enums") {
100
103
  const { id: enumId } = getEnumInfoFromColName(names.capital, col.name);
101
- return [`import { ${enumId}Label } from 'src/services/sonamu.generated';`];
104
+ return [`import { ${enumId}Label } from '@/services/sonamu.generated';`];
102
105
  } else if (col.renderType === "object") {
103
106
  try {
104
107
  const relProp = getRelationPropFromColName(entityId, col.name);
@@ -121,11 +124,11 @@ export class Template__view_list extends Template {
121
124
 
122
125
  renderFilterImport(entityId: string, col: RenderingNode, names: EntityNamesRecord) {
123
126
  if (col.name === "search") {
124
- return `import { ${names.capital}SearchInput } from "src/components/${names.fs}/${names.capital}SearchInput";`;
127
+ return `import { ${names.capital}SearchInput } from "@/components/${names.fs}/${names.capital}SearchInput";`;
125
128
  } else if (col.renderType === "enums") {
126
129
  if (col.name === "orderBy") {
127
130
  const componentId = `${names.capital}${inflection.camelize(col.name)}Select`;
128
- return `import { ${componentId} } from "src/components/${names.fs}/${componentId}";`;
131
+ return `import { ${componentId} } from "@/components/${names.fs}/${componentId}";`;
129
132
  } else {
130
133
  try {
131
134
  const { id, targetEntityNames: targetMDNames } = getEnumInfoFromColName(
@@ -133,7 +136,7 @@ export class Template__view_list extends Template {
133
136
  col.name,
134
137
  );
135
138
  const componentId = `${id}Select`;
136
- return `import { ${componentId} } from "src/components/${targetMDNames.fs}/${componentId}";`;
139
+ return `import { ${componentId} } from "@/components/${targetMDNames.fs}/${componentId}";`;
137
140
  } catch {
138
141
  return "";
139
142
  }
@@ -143,7 +146,7 @@ export class Template__view_list extends Template {
143
146
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
144
147
  const targetNames = EntityManager.getNamesFromId(relProp.with);
145
148
  const componentId = `${relProp.with}IdAsyncSelect`;
146
- return `import { ${componentId} } from "src/components/${targetNames.fs}/${componentId}";`;
149
+ return `import { ${componentId} } from "@/components/${targetNames.fs}/${componentId}";`;
147
150
  } catch {
148
151
  return "";
149
152
  }
@@ -322,9 +325,9 @@ import classNames from 'classnames';
322
325
  import { DateTime } from "luxon";
323
326
  import { DelButton, EditButton, AppBreadcrumbs, AddButton, useSelection, useListParams, SonamuCol, numF, formatDate, formatDateTime } from '@sonamu-kit/react-sui';
324
327
 
325
- import { ${names.capital}SubsetA } from "src/services/sonamu.generated";
326
- import { ${names.capital}Service } from 'src/services/${names.fs}/${names.fs}.service';
327
- import { ${names.capital}ListParams } from 'src/services/${names.fs}/${names.fs}.types';
328
+ import { ${names.capital}SubsetA } from "@/services/sonamu.generated";
329
+ import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
330
+ import { ${names.capital}ListParams } from '@/services/${names.fs}/${names.fs}.types';
328
331
  ${columnImports}
329
332
  ${filterColumns
330
333
  .map((col) => {
@@ -23,7 +23,7 @@ export class Template__view_search_input extends Template {
23
23
  import React from "react";
24
24
  import { useState } from "react";
25
25
  import { DropdownProps, Input, InputProps } from "semantic-ui-react";
26
- import { ${names.capital}SearchFieldDropdown } from "src/components/${names.fs}/${names.capital}SearchFieldDropdown";
26
+ import { ${names.capital}SearchFieldDropdown } from "@/components/${names.fs}/${names.capital}SearchFieldDropdown";
27
27
 
28
28
  export function ${names.capital}SearchInput({
29
29
  input: { value: inputValue, onChange: inputOnChange, ...inputProps },
@@ -70,7 +70,7 @@ export abstract class Template {
70
70
  const instance = Template.templates.get(key);
71
71
  if (!instance) {
72
72
  throw new Error(
73
- `Template ${key} not found. It might be becasuse you tried to find a template before loading all templates. Did you call Template.loadAll()?`,
73
+ `Template ${key} not found. It might be because you tried to find a template before loading all templates. Did you call Template.loadAll()?`,
74
74
  );
75
75
  }
76
76
  return instance;
@@ -49,6 +49,8 @@ import {
49
49
  isStringSingleProp,
50
50
  isUuidArrayProp,
51
51
  isUuidSingleProp,
52
+ isVectorArrayProp,
53
+ isVectorSingleProp,
52
54
  isVirtualProp,
53
55
  type RenderingNode,
54
56
  } from "../types/types";
@@ -131,6 +133,10 @@ export async function propToZodType(prop: EntityProp): Promise<z.ZodTypeAny> {
131
133
  zodType = z.uuid().array();
132
134
  } else if (isJsonProp(prop)) {
133
135
  zodType = await getZodTypeById(prop.id);
136
+ } else if (isVectorSingleProp(prop)) {
137
+ zodType = z.array(z.number());
138
+ } else if (isVectorArrayProp(prop)) {
139
+ zodType = z.array(z.array(z.number()));
134
140
  } else if (isVirtualProp(prop)) {
135
141
  zodType = await getZodTypeById(prop.id);
136
142
  } else if (isRelationProp(prop)) {
@@ -205,6 +211,10 @@ export function propToZodTypeDef(prop: EntityProp, injectImportKeys: string[]):
205
211
  } else if (isJsonProp(prop)) {
206
212
  stmt = `${prop.name}: ${prop.id}`;
207
213
  injectImportKeys.push(prop.id);
214
+ } else if (isVectorSingleProp(prop)) {
215
+ stmt = `${prop.name}: z.array(z.number())`;
216
+ } else if (isVectorArrayProp(prop)) {
217
+ stmt = `${prop.name}: z.array(z.array(z.number()))`;
208
218
  } else if (isVirtualProp(prop)) {
209
219
  stmt = `${prop.name}: ${prop.id}`;
210
220
  injectImportKeys.push(prop.id);
@@ -506,6 +516,16 @@ export function zodTypeToRenderingNode(
506
516
  renderType: "array-images",
507
517
  };
508
518
  }
519
+ // vector 타입 판별: number 배열이면서 embedding, vector 등의 이름을 가진 경우
520
+ if (
521
+ innerType instanceof z.ZodNumber &&
522
+ (baseKey.includes("embedding") || baseKey.includes("vector"))
523
+ ) {
524
+ return {
525
+ ...def,
526
+ renderType: "vector",
527
+ };
528
+ }
509
529
  return {
510
530
  ...def,
511
531
  renderType: "array",
@@ -13,6 +13,7 @@ import { type UBRef, UpsertBuilder } from "../database/upsert-builder";
13
13
  import type { Entity } from "../entity/entity";
14
14
  import { EntityManager } from "../entity/entity-manager";
15
15
  import {
16
+ type DatabaseSchemaExtend,
16
17
  type EntityProp,
17
18
  type FixtureImportResult,
18
19
  type FixtureRecord,
@@ -95,47 +96,45 @@ export class FixtureManagerClass {
95
96
  }
96
97
 
97
98
  /**
98
- 이제 FixtureManager.sync() checksum 비교 없이 create database template 으로 수행합니다.
99
+ 원격 fixture DB를 로컬 test DB로 복사합니다.
100
+ pg_dump로 원격 DB를 덤프하고, pg_restore로 로컬에 복원합니다.
99
101
  */
100
102
  async sync() {
101
103
  const fixtureConn = Sonamu.dbConfig.fixture_remote.connection as Knex.PgConnectionConfig;
102
104
  const testConn = Sonamu.dbConfig.test.connection as Knex.PgConnectionConfig;
103
105
 
104
- // PostgreSQL 패스워드 환경변수 설정
105
- const pgEnv = { PGPASSWORD: testConn.password || "" };
106
-
107
- // 1. 연결 강제 종료
106
+ // 1. 로컬 test DB 연결 종료 및 재생성
107
+ const testPgEnv = { PGPASSWORD: testConn.password || "" };
108
108
  execSync(
109
109
  `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "
110
- SELECT pg_terminate_backend(pg_stat_activity.pid)
111
- FROM pg_stat_activity
112
- WHERE datname = '${testConn.database}'
113
- AND pid <> pg_backend_pid();
114
- "`,
115
- { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
116
- );
117
-
118
- execSync(
119
- `psql -h ${fixtureConn.host} -p ${fixtureConn.port ?? 5432} -U ${fixtureConn.user} -d postgres -c "
120
110
  SELECT pg_terminate_backend(pg_stat_activity.pid)
121
111
  FROM pg_stat_activity
122
- WHERE datname = '${fixtureConn.database}'
112
+ WHERE datname = '${testConn.database}'
123
113
  AND pid <> pg_backend_pid();
124
114
  "`,
125
- { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
115
+ { stdio: "inherit", env: { ...process.env, ...testPgEnv } as NodeJS.ProcessEnv },
126
116
  );
127
117
 
128
- // 2. DROP DATABASE (별도 실행!)
129
118
  execSync(
130
119
  `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "DROP DATABASE IF EXISTS \\"${testConn.database}\\""`,
131
- { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
120
+ { stdio: "inherit", env: { ...process.env, ...testPgEnv } as NodeJS.ProcessEnv },
132
121
  );
133
122
 
134
- // 3. CREATE DATABASE
135
123
  execSync(
136
- `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "CREATE DATABASE \\"${testConn.database}\\" TEMPLATE \\"${fixtureConn.database}\\""`,
137
- { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
124
+ `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "CREATE DATABASE \\"${testConn.database}\\""`,
125
+ { stdio: "inherit", env: { ...process.env, ...testPgEnv } as NodeJS.ProcessEnv },
138
126
  );
127
+
128
+ // 2. 원격 fixture DB → 로컬 test DB로 복사 (pg_dump | pg_restore)
129
+ const fixturePgEnv = { PGPASSWORD: fixtureConn.password || "" };
130
+ const dumpCmd = `pg_dump -h ${fixtureConn.host} -p ${fixtureConn.port ?? 5432} -U ${fixtureConn.user} -d ${fixtureConn.database} -Fc`;
131
+ const restoreCmd = `pg_restore -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d ${testConn.database} --no-owner --no-acl`;
132
+
133
+ execSync(`${dumpCmd} | PGPASSWORD="${testConn.password || ""}" ${restoreCmd}`, {
134
+ stdio: "inherit",
135
+ env: { ...process.env, ...fixturePgEnv } as NodeJS.ProcessEnv,
136
+ shell: "/bin/bash",
137
+ });
139
138
  }
140
139
 
141
140
  private visitedRecords = new Set<string>();
@@ -506,7 +505,9 @@ export class FixtureManagerClass {
506
505
  // upsert된 row들의 uuid -> id 매핑 구축
507
506
  if (uuids.length > 0) {
508
507
  const uuidToId = new Map<string, number>();
509
- const rows = await trx(tableName).select("uuid", "id").whereIn("uuid", uuids);
508
+ const rows = await trx(tableName as string)
509
+ .select("uuid", "id")
510
+ .whereIn("uuid", uuids);
510
511
 
511
512
  for (const row of rows) {
512
513
  uuidToId.set(row.uuid, row.id);
@@ -656,7 +657,7 @@ export class FixtureManagerClass {
656
657
  /**
657
658
  * 테이블 순서 추출 (fixtures에 포함된 테이블만)
658
659
  */
659
- private getTableOrder(fixtures: FixtureRecord[]): string[] {
660
+ private getTableOrder(fixtures: FixtureRecord[]): (keyof DatabaseSchemaExtend)[] {
660
661
  const tables: string[] = [];
661
662
  const seen = new Set<string>();
662
663
 
@@ -668,7 +669,7 @@ export class FixtureManagerClass {
668
669
  }
669
670
  }
670
671
 
671
- return tables;
672
+ return tables as (keyof DatabaseSchemaExtend)[];
672
673
  }
673
674
 
674
675
  private async processManyToManyRelations(
@@ -755,7 +756,7 @@ export class FixtureManagerClass {
755
756
  const _uniqueIndexes = entity.indexes?.filter((i) => i.type === "unique") ?? [];
756
757
 
757
758
  const uniqueIndexes = _uniqueIndexes.filter((index) =>
758
- index.columns.every((column) => !column.startsWith(`${entity.table}__`)),
759
+ index.columns.every((column) => !column.name.startsWith(`${entity.table}__`)),
759
760
  );
760
761
  if (uniqueIndexes.length === 0) {
761
762
  return null;
@@ -767,7 +768,7 @@ export class FixtureManagerClass {
767
768
  for (const index of uniqueIndexes) {
768
769
  // 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
769
770
  const containsNull = index.columns.some((column) => {
770
- const field = column.replace(/_id$/, "");
771
+ const field = column.name.replace(/_id$/, "");
771
772
  return fixture.columns[field]?.value === null;
772
773
  });
773
774
  if (containsNull) {
@@ -776,12 +777,12 @@ export class FixtureManagerClass {
776
777
 
777
778
  uniqueQuery = uniqueQuery.orWhere((qb) => {
778
779
  for (const column of index.columns) {
779
- const field = column.replace(/_id$/, "");
780
+ const field = column.name.replace(/_id$/, "");
780
781
 
781
782
  if (Array.isArray(fixture.columns[field]?.value)) {
782
- qb.whereIn(column, fixture.columns[field].value);
783
+ qb.whereIn(column.name, fixture.columns[field].value);
783
784
  } else {
784
- qb.andWhere(column, fixture.columns[field]?.value);
785
+ qb.andWhere(column.name, fixture.columns[field]?.value);
785
786
  }
786
787
  }
787
788
  });
@@ -15,12 +15,18 @@ export type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K
15
15
  /*
16
16
  Model-Defintion
17
17
  */
18
+ export type GeneratedColumnType = "STORED" | "VIRTUAL";
19
+ export type GeneratedColumn = {
20
+ type: GeneratedColumnType;
21
+ expression: string;
22
+ };
18
23
  export type CommonProp = {
19
24
  name: string;
20
25
  nullable?: boolean;
21
26
  toFilter?: true;
22
27
  desc?: string;
23
28
  dbDefault?: string;
29
+ generated?: GeneratedColumn;
24
30
  };
25
31
  export type IntegerProp = CommonProp & {
26
32
  type: "integer";
@@ -99,7 +105,14 @@ export type VirtualProp = CommonProp & {
99
105
  type: "virtual";
100
106
  id: string;
101
107
  }; // PG: none / TS: any(id) / JSON: any
102
-
108
+ export type VectorProp = CommonProp & {
109
+ type: "vector";
110
+ dimensions: number;
111
+ };
112
+ export type VectorArrayProp = CommonProp & {
113
+ type: "vector[]";
114
+ dimensions: number;
115
+ };
103
116
  export type RelationType = "HasMany" | "BelongsToOne" | "ManyToMany" | "OneToOne";
104
117
  export type RelationOn = "CASCADE" | "SET NULL" | "NO ACTION" | "SET DEFAULT" | "RESTRICT";
105
118
  type _RelationProp = {
@@ -170,13 +183,82 @@ export type EntityProp =
170
183
  | UuidArrayProp
171
184
  | JsonProp
172
185
  | VirtualProp
186
+ | VectorProp
187
+ | VectorArrayProp
173
188
  | RelationProp;
174
189
 
190
+ /**
191
+ * pgvector 거리 연산자 클래스
192
+ *
193
+ * @description
194
+ * - `vector_cosine_ops`: 코사인 거리 (Cosine Distance) - 권장
195
+ * - SQL 연산자: `<=>`
196
+ * - 벡터의 방향만 비교 (크기 무시), 1 - cosine_similarity
197
+ * - 텍스트 임베딩, 시맨틱 검색에 가장 일반적으로 사용
198
+ * - 사용 예: OpenAI, Voyage 등 대부분의 임베딩 모델에 권장
199
+ *
200
+ * - `vector_ip_ops`: 내적 (Inner Product)
201
+ * - SQL 연산자: `<#>`
202
+ * - 두 벡터의 내적을 계산 (sum(a[i] * b[i]))
203
+ * - 정규화된 벡터에서 코사인 유사도와 동일한 결과
204
+ * - 값이 클수록 유사 (음수 연산자이므로 ORDER BY에서 주의)
205
+ * - 사용 예: 이미 정규화된 임베딩에서 가장 빠른 성능
206
+ *
207
+ * - `vector_l2_ops`: 유클리드 거리 (L2 Distance)
208
+ * - SQL 연산자: `<->`
209
+ * - 두 벡터 간의 직선 거리를 계산 (sqrt(sum((a[i] - b[i])^2)))
210
+ * - 벡터의 크기(magnitude)가 중요할 때 사용
211
+ * - 사용 예: 이미지 유사도, 절대적 거리 측정이 필요한 경우
212
+ */
213
+ export type VectorOps = "vector_cosine_ops" | "vector_ip_ops" | "vector_l2_ops";
214
+
215
+ type EntityIndexColumn = {
216
+ name: string;
217
+ nullsFirst?: boolean;
218
+ sortOrder?: "ASC" | "DESC";
219
+ /** pgvector 인덱스에서 사용할 거리 연산자 (vector 컬럼에만 적용) */
220
+ vectorOps?: VectorOps;
221
+ };
175
222
  export type EntityIndex = {
176
- type: "index" | "unique" | "fulltext";
177
- columns: string[];
223
+ type: "index" | "unique" | "fulltext" | "hnsw" | "ivfflat";
224
+ columns: EntityIndexColumn[];
178
225
  name: string;
179
226
  parser?: "built-in" | "ngram";
227
+ nullsNotDistinct?: boolean; // unique index only
228
+ /**
229
+ * HNSW (Hierarchical Navigable Small World) 인덱스: 각 노드의 최대 연결 수
230
+ *
231
+ * @description
232
+ * 그래프에서 각 노드가 가질 수 있는 최대 연결 수입니다.
233
+ * HNSW는 빠른 검색 속도와 높은 정확도를 제공하므로 권장됩니다.
234
+ * - 기본값: 16
235
+ * - 범위: 2 ~ 100
236
+ * - 높을수록: 정확도↑, 빌드 시간↑, 메모리↑
237
+ * - 권장: 빠른 빌드(8), 균형(16), 높은 정확도(32), 최고 정확도(64)
238
+ */
239
+ m?: number;
240
+ /**
241
+ * HNSW (Hierarchical Navigable Small World) 인덱스: 구성 시 탐색 범위
242
+ *
243
+ * @description
244
+ * 인덱스 구성 시 각 노드에서 탐색할 범위입니다.
245
+ * - 기본값: 64
246
+ * - 범위: 4 ~ 1000
247
+ * - 높을수록: 정확도↑, 빌드 시간↑
248
+ * - 권장: 빠른 빌드(32), 균형(64), 높은 정확도(128), 최고 정확도(256)
249
+ */
250
+ efConstruction?: number;
251
+ /**
252
+ * IVFFlat (Inverted File with Flat Compression) 인덱스: 클러스터링 리스트 수
253
+ *
254
+ * @description
255
+ * 벡터를 클러스터링할 버킷 수를 지정합니다.
256
+ * IVFFlat은 빠른 빌드와 낮은 메모리 사용이 필요할 때 사용합니다.
257
+ * - 권장값: sqrt(row_count) ~ row_count / 1000
258
+ * - 예시: 10,000행 → 100, 100,000행 → 300, 1,000,000행 → 1000
259
+ * - 많을수록 정확도↑, 검색 속도↓
260
+ */
261
+ lists?: number;
180
262
  };
181
263
  export type EntityJson = {
182
264
  id: string;
@@ -327,6 +409,15 @@ export function isJsonProp(p: unknown): p is JsonProp {
327
409
  export function isVirtualProp(p: unknown): p is VirtualProp {
328
410
  return (p as VirtualProp)?.type === "virtual";
329
411
  }
412
+ export function isVectorSingleProp(p: unknown): p is VectorProp {
413
+ return (p as VectorProp)?.type === "vector";
414
+ }
415
+ export function isVectorArrayProp(p: unknown): p is VectorArrayProp {
416
+ return (p as VectorArrayProp)?.type === "vector[]";
417
+ }
418
+ export function isVectorProp(p: unknown): p is VectorProp | VectorArrayProp {
419
+ return isVectorSingleProp(p) || isVectorArrayProp(p);
420
+ }
330
421
  export function isRelationProp(p: unknown): p is RelationProp {
331
422
  return (p as RelationProp)?.type === "relation";
332
423
  }
@@ -439,7 +530,10 @@ export type MigrationColumnType =
439
530
  | "date[]"
440
531
  | "uuid"
441
532
  | "uuid[]"
442
- | "json";
533
+ | "json"
534
+ | "vector"
535
+ | "vector[]"
536
+ | "tsvector";
443
537
  export type MigrationColumn = {
444
538
  name: string;
445
539
  type: MigrationColumnType;
@@ -449,12 +543,21 @@ export type MigrationColumn = {
449
543
  defaultTo?: string;
450
544
  precision?: number;
451
545
  scale?: number;
546
+ dimensions?: number;
547
+ generated?: GeneratedColumn;
452
548
  };
453
549
  export type MigrationIndex = {
550
+ type: "unique" | "index" | "fulltext" | "hnsw" | "ivfflat";
551
+ columns: EntityIndexColumn[];
454
552
  name: string;
455
- columns: string[];
456
- type: "unique" | "index" | "fulltext";
457
553
  parser?: "built-in" | "ngram";
554
+ nullsNotDistinct?: boolean;
555
+ /** HNSW (Hierarchical Navigable Small World): 각 노드의 최대 연결 수 */
556
+ m?: number;
557
+ /** HNSW (Hierarchical Navigable Small World): 구성 시 탐색 범위 */
558
+ efConstruction?: number;
559
+ /** IVFFlat (Inverted File with Flat Compression): 클러스터링 리스트 수 */
560
+ lists?: number;
458
561
  };
459
562
  export type MigrationForeign = {
460
563
  columns: string[];
@@ -698,7 +801,8 @@ export type RenderingNode = {
698
801
  | "array-images"
699
802
  | "object"
700
803
  | "object-pick"
701
- | "record";
804
+ | "record"
805
+ | "vector";
702
806
  zodType: z.ZodTypeAny;
703
807
  element?: RenderingNode;
704
808
  children?: RenderingNode[];
@@ -709,12 +813,18 @@ export type RenderingNode = {
709
813
  nullable?: boolean;
710
814
  };
711
815
 
816
+ const GeneratedColumnSchema = z.object({
817
+ type: z.enum(["STORED", "VIRTUAL"]),
818
+ expression: z.string(),
819
+ });
820
+
712
821
  const BasePropFields = {
713
822
  name: z.string(),
714
823
  desc: z.string().optional(),
715
824
  nullable: z.boolean().optional(),
716
- toFilter: z.literal(true).optional(),
825
+ toFilter: z.boolean().default(false).optional(),
717
826
  dbDefault: z.union([z.string(), z.number(), z.boolean()]).optional(),
827
+ generated: GeneratedColumnSchema.optional(),
718
828
  };
719
829
 
720
830
  // 부가 필드가 필요없는 prop
@@ -764,6 +874,7 @@ const EnumPropSchema = z
764
874
  ...BasePropFields,
765
875
  type: z.literal("enum"),
766
876
  id: z.string(),
877
+ length: z.number().optional(),
767
878
  })
768
879
  .strict();
769
880
  const EnumArrayPropSchema = z
@@ -822,6 +933,21 @@ const VirtualPropSchema = z
822
933
  })
823
934
  .strict();
824
935
 
936
+ const VectorPropSchema = z
937
+ .object({
938
+ ...BasePropFields,
939
+ type: z.literal("vector"),
940
+ dimensions: z.number(),
941
+ })
942
+ .strict();
943
+ const VectorArrayPropSchema = z
944
+ .object({
945
+ ...BasePropFields,
946
+ type: z.literal("vector[]"),
947
+ dimensions: z.number(),
948
+ })
949
+ .strict();
950
+
825
951
  // Relation 타입은 relationType에 따라 세분화
826
952
  const BaseRelationFields = {
827
953
  ...BasePropFields,
@@ -910,27 +1036,84 @@ const NormalPropTypes = [
910
1036
  "uuid[]",
911
1037
  "json",
912
1038
  "virtual",
1039
+ "vector",
1040
+ "vector[]",
1041
+ ] as const;
1042
+
1043
+ // VIRTUAL Generated Column에서 사용 불가능한 타입들
1044
+ const VirtualGeneratedDisallowedTypes = [
1045
+ "json",
1046
+ "vector",
1047
+ "vector[]",
1048
+ "string[]",
1049
+ "integer[]",
1050
+ "bigInteger[]",
1051
+ "boolean[]",
1052
+ "date[]",
1053
+ "uuid[]",
1054
+ "number[]",
1055
+ "numeric[]",
1056
+ "enum[]",
913
1057
  ] as const;
914
- export const NormalPropSchema = z.discriminatedUnion(
915
- "type",
916
- [
917
- BasePropFieldsWithoutAdditional,
918
- StringPropSchema,
919
- StringArrayPropSchema,
920
- EnumPropSchema,
921
- EnumArrayPropSchema,
922
- NumberPropSchema,
923
- NumberArrayPropSchema,
924
- NumericPropSchema,
925
- NumericArrayPropSchema,
926
- JsonPropSchema,
927
- VirtualPropSchema,
928
- ],
929
- {
930
- error: (iss) =>
931
- `type은 ${NormalPropTypes.map((t) => `'${t}'`).join(", ")} 중 하나여야 합니다. 입력값: "${(iss.input as Record<string, unknown>)?.type}"`,
932
- },
933
- );
1058
+
1059
+ export const NormalPropSchema = z
1060
+ .discriminatedUnion(
1061
+ "type",
1062
+ [
1063
+ BasePropFieldsWithoutAdditional,
1064
+ StringPropSchema,
1065
+ StringArrayPropSchema,
1066
+ EnumPropSchema,
1067
+ EnumArrayPropSchema,
1068
+ NumberPropSchema,
1069
+ NumberArrayPropSchema,
1070
+ NumericPropSchema,
1071
+ NumericArrayPropSchema,
1072
+ JsonPropSchema,
1073
+ VirtualPropSchema,
1074
+ VectorPropSchema,
1075
+ VectorArrayPropSchema,
1076
+ ],
1077
+ {
1078
+ error: (iss) =>
1079
+ `type은 ${NormalPropTypes.map((t) => `'${t}'`).join(", ")} 중 하나여야 합니다. 입력값: "${(iss.input as Record<string, unknown>)?.type}"`,
1080
+ },
1081
+ )
1082
+ .superRefine((data, ctx) => {
1083
+ if (!data.generated) {
1084
+ return;
1085
+ }
1086
+
1087
+ // dbDefault와 generated 동시 사용 불가
1088
+ if (data.dbDefault !== undefined) {
1089
+ ctx.addIssue({
1090
+ code: "custom",
1091
+ message: "dbDefault와 generated는 함께 사용할 수 없습니다",
1092
+ path: ["generated"],
1093
+ });
1094
+ }
1095
+
1096
+ // virtual 타입은 generated 불가
1097
+ if (data.type === "virtual") {
1098
+ ctx.addIssue({
1099
+ code: "custom",
1100
+ message: "virtual 타입은 generated column을 지원하지 않습니다",
1101
+ path: ["generated"],
1102
+ });
1103
+ }
1104
+
1105
+ // VIRTUAL Generated Column 타입 제한 검증
1106
+ if (data.generated.type === "VIRTUAL") {
1107
+ if ((VirtualGeneratedDisallowedTypes as readonly string[]).includes(data.type)) {
1108
+ ctx.addIssue({
1109
+ code: "custom",
1110
+ message: `VIRTUAL generated column은 ${data.type} 타입을 지원하지 않습니다. STORED를 사용하세요.`,
1111
+ path: ["generated", "type"],
1112
+ fatal: true,
1113
+ });
1114
+ }
1115
+ }
1116
+ });
934
1117
 
935
1118
  const AllPropTypes = [...NormalPropTypes, "relation"] as const;
936
1119
  const EntityPropSchema = z.discriminatedUnion("type", [NormalPropSchema, RelationPropSchema], {
@@ -938,13 +1121,24 @@ const EntityPropSchema = z.discriminatedUnion("type", [NormalPropSchema, Relatio
938
1121
  `type은 ${AllPropTypes.map((t) => `'${t}'`).join(", ")} 중 하나여야 합니다. 입력값: "${(iss.input as Record<string, unknown>)?.type}"`,
939
1122
  });
940
1123
 
1124
+ const EntityIndexColumnSchema = z.object({
1125
+ name: z.string(),
1126
+ nullsFirst: z.boolean().optional(),
1127
+ sortOrder: z.enum(["ASC", "DESC"]).optional(),
1128
+ vectorOps: z.enum(["vector_cosine_ops", "vector_ip_ops", "vector_l2_ops"]).optional(),
1129
+ });
1130
+
941
1131
  // EntityIndex 스키마 정의
942
1132
  const EntityIndexSchema = z
943
1133
  .object({
944
- type: z.enum(["index", "unique", "fulltext"]),
945
- columns: z.array(z.string()),
1134
+ type: z.enum(["index", "unique", "fulltext", "hnsw", "ivfflat"]),
1135
+ columns: z.array(EntityIndexColumnSchema),
946
1136
  name: z.string().min(1).max(63),
947
1137
  parser: z.enum(["built-in", "ngram"]).optional(),
1138
+ nullsNotDistinct: z.boolean().optional(),
1139
+ m: z.number().optional(),
1140
+ efConstruction: z.number().optional(),
1141
+ lists: z.number().optional(),
948
1142
  })
949
1143
  .strict();
950
1144
 
@@ -1121,6 +1315,8 @@ export type RelationNode = {
1121
1315
 
1122
1316
  // biome-ignore lint/suspicious/noEmptyInterface: sonamu.generated.sso 에서 확장을 위해 준비된 빈 인터페이스
1123
1317
  export interface DatabaseSchemaExtend {}
1318
+ // biome-ignore lint/suspicious/noEmptyInterface: sonamu.generated.sso 에서 확장을 위해 준비된 빈 인터페이스
1319
+ export interface DatabaseForeignKeys {}
1124
1320
  export type ManyToManyBaseSchema<FromIdKey extends string, ToIdKey extends string> = {
1125
1321
  id: number;
1126
1322
  } & {
@@ -1149,22 +1345,4 @@ export type SonamuFastifyConfig = {
1149
1345
  options: ApiDecoratorOptions;
1150
1346
  },
1151
1347
  ) => void;
1152
- cache?: {
1153
- get: (key: string) => Promise<unknown | null>;
1154
- put: (key: string, value: unknown, ttl?: number) => Promise<void>;
1155
- resolveKey: (
1156
- path: string,
1157
- reqBody: {
1158
- [key: string]: unknown;
1159
- },
1160
- ) =>
1161
- | {
1162
- cache: false;
1163
- }
1164
- | {
1165
- cache: true;
1166
- key: string;
1167
- ttl?: number;
1168
- };
1169
- };
1170
1348
  };