sonamu 0.2.48 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -761,6 +761,7 @@ type FixtureRecord = {
761
761
  fetchedRecords: string[];
762
762
  belongsRecords: string[];
763
763
  target?: FixtureRecord;
764
+ unique?: FixtureRecord;
764
765
  override?: boolean;
765
766
  };
766
767
  type FixtureImportResult = {
@@ -1100,7 +1101,10 @@ declare class SonamuClass {
1100
1101
  set secrets(secrets: SonamuSecrets);
1101
1102
  get secrets(): SonamuSecrets | null;
1102
1103
  init(doSilent?: boolean, enableSync?: boolean, apiRootPath?: string): Promise<void>;
1103
- withFastify(server: FastifyInstance<Server, IncomingMessage, ServerResponse>, config: SonamuFastifyConfig): Promise<void>;
1104
+ withFastify(server: FastifyInstance<Server, IncomingMessage, ServerResponse>, config: SonamuFastifyConfig, options?: {
1105
+ enableSync?: boolean;
1106
+ doSilent?: boolean;
1107
+ }): Promise<void>;
1104
1108
  destroy(): Promise<void>;
1105
1109
  }
1106
1110
  declare const Sonamu: SonamuClass;
@@ -1145,12 +1149,12 @@ declare class UpsertBuilder {
1145
1149
  register<T extends string>(tableName: string, row: {
1146
1150
  [key in T]?: UBRef | string | number | boolean | bigint | null | object | unknown;
1147
1151
  }): UBRef;
1148
- upsert(wdb: Knex, tableName: string): Promise<number[]>;
1149
- insertOnly(wdb: Knex, tableName: string): Promise<number[]>;
1150
- upsertOrInsert(wdb: Knex, tableName: string, mode: "upsert" | "insert"): Promise<number[]>;
1152
+ upsert(wdb: Knex, tableName: string, chunkSize?: number): Promise<number[]>;
1153
+ insertOnly(wdb: Knex, tableName: string, chunkSize?: number): Promise<number[]>;
1154
+ upsertOrInsert(wdb: Knex, tableName: string, mode: "upsert" | "insert", chunkSize?: number): Promise<number[]>;
1151
1155
  updateBatch(wdb: Knex, tableName: string, options?: {
1152
1156
  chunkSize?: number;
1153
- where?: string;
1157
+ where?: string | string[];
1154
1158
  }): Promise<void>;
1155
1159
  }
1156
1160
 
@@ -1452,14 +1456,18 @@ declare class FixtureManagerClass {
1452
1456
  getImportQueries(entityId: string, field: string, id: number): Promise<string[]>;
1453
1457
  destory(): Promise<void>;
1454
1458
  getFixtures(sourceDBName: keyof SonamuDBConfig, targetDBName: keyof SonamuDBConfig, searchOptions: FixtureSearchOptions): Promise<FixtureRecord[]>;
1455
- createFixtureRecord(entity: Entity, row: any, visitedEntities: Set<string>, records: FixtureRecord[], singleRecord?: boolean, _db?: Knex): Promise<void>;
1456
- insertFixtures(dbName: keyof SonamuDBConfig, fixtures: FixtureRecord[]): Promise<FixtureImportResult[]>;
1459
+ createFixtureRecord(entity: Entity, row: any, options?: {
1460
+ singleRecord?: boolean;
1461
+ _db?: Knex;
1462
+ }, visitedEntities?: Set<string>): Promise<FixtureRecord[]>;
1463
+ insertFixtures(dbName: keyof SonamuDBConfig, _fixtures: FixtureRecord[]): Promise<FixtureImportResult[]>;
1457
1464
  private getInsertionOrder;
1458
1465
  private prepareInsertData;
1459
1466
  private buildDependencyGraph;
1460
1467
  private insertFixture;
1461
1468
  private handleManyToManyRelations;
1462
1469
  addFixtureLoader(code: string): Promise<void>;
1470
+ private checkUniqueViolation;
1463
1471
  }
1464
1472
  declare const FixtureManager: FixtureManagerClass;
1465
1473
 
package/dist/index.js CHANGED
@@ -80,13 +80,13 @@
80
80
 
81
81
 
82
82
 
83
- var _chunk76VBQWGEjs = require('./chunk-76VBQWGE.js');
83
+ var _chunkHATLA54Zjs = require('./chunk-HATLA54Z.js');
84
84
 
85
85
  // src/exceptions/error-handler.ts
86
86
  function setupErrorHandler(server) {
87
87
  server.setErrorHandler((error, request, reply) => {
88
88
  _nullishCoalesce(error.statusCode, () => ( (error.statusCode = 400)));
89
- if (_chunk76VBQWGEjs.isSoException.call(void 0, error) && error.payload && Array.isArray(error.payload)) {
89
+ if (_chunkHATLA54Zjs.isSoException.call(void 0, error) && error.payload && Array.isArray(error.payload)) {
90
90
  const issues = error.payload;
91
91
  const [issue] = issues;
92
92
  const message = `${issue.message} (${issue.path.join("/")})`;
@@ -423,5 +423,5 @@ function unique(columns) {
423
423
 
424
424
 
425
425
 
426
- exports.AlreadyProcessedException = _chunk76VBQWGEjs.AlreadyProcessedException; exports.ApiParamType = _chunk76VBQWGEjs.ApiParamType; exports.BadRequestException = _chunk76VBQWGEjs.BadRequestException; exports.BaseModel = _chunk76VBQWGEjs.BaseModel; exports.BaseModelClass = _chunk76VBQWGEjs.BaseModelClass; exports.DB = _chunk76VBQWGEjs.DB; exports.DuplicateRowException = _chunk76VBQWGEjs.DuplicateRowException; exports.Entity = _chunk76VBQWGEjs.Entity; exports.EntityManager = _chunk76VBQWGEjs.EntityManager; exports.FixtureManager = _chunk76VBQWGEjs.FixtureManager; exports.FixtureManagerClass = _chunk76VBQWGEjs.FixtureManagerClass; exports.GenerateOptions = _chunk76VBQWGEjs.GenerateOptions; exports.InternalServerErrorException = _chunk76VBQWGEjs.InternalServerErrorException; exports.Migrator = _chunk76VBQWGEjs.Migrator; exports.NotFoundException = _chunk76VBQWGEjs.NotFoundException; exports.PathAndCode = _chunk76VBQWGEjs.PathAndCode; exports.RenderingNode = _chunk76VBQWGEjs.RenderingNode; exports.SQLDateTimeString = _chunk76VBQWGEjs.SQLDateTimeString; exports.ServiceUnavailableException = _chunk76VBQWGEjs.ServiceUnavailableException; exports.SoException = _chunk76VBQWGEjs.SoException; exports.Sonamu = _chunk76VBQWGEjs.Sonamu; exports.SonamuQueryMode = _chunk76VBQWGEjs.SonamuQueryMode; exports.Syncer = _chunk76VBQWGEjs.Syncer; exports.TargetNotFoundException = _chunk76VBQWGEjs.TargetNotFoundException; exports.TemplateKey = _chunk76VBQWGEjs.TemplateKey; exports.TemplateOptions = _chunk76VBQWGEjs.TemplateOptions; exports.UnauthorizedException = _chunk76VBQWGEjs.UnauthorizedException; exports.UpsertBuilder = _chunk76VBQWGEjs.UpsertBuilder; exports.api = _chunk76VBQWGEjs.api; exports.apiParamToTsCode = _chunk76VBQWGEjs.apiParamToTsCode; exports.apiParamTypeToTsType = _chunk76VBQWGEjs.apiParamTypeToTsType; exports.asArray = asArray; exports.findApiRootPath = _chunk76VBQWGEjs.findApiRootPath; exports.findAppRootPath = _chunk76VBQWGEjs.findAppRootPath; exports.getTextTypeLength = _chunk76VBQWGEjs.getTextTypeLength; exports.getZodObjectFromApi = _chunk76VBQWGEjs.getZodObjectFromApi; exports.getZodObjectFromApiParams = _chunk76VBQWGEjs.getZodObjectFromApiParams; exports.getZodTypeFromApiParamType = _chunk76VBQWGEjs.getZodTypeFromApiParamType; exports.globAsync = _chunk76VBQWGEjs.globAsync; exports.i = i; exports.importMultiple = _chunk76VBQWGEjs.importMultiple; exports.isBelongsToOneRelationProp = _chunk76VBQWGEjs.isBelongsToOneRelationProp; exports.isBigIntegerProp = _chunk76VBQWGEjs.isBigIntegerProp; exports.isBooleanProp = _chunk76VBQWGEjs.isBooleanProp; exports.isCustomJoinClause = _chunk76VBQWGEjs.isCustomJoinClause; exports.isDaemonServer = _chunk76VBQWGEjs.isDaemonServer; exports.isDateProp = _chunk76VBQWGEjs.isDateProp; exports.isDateTimeProp = _chunk76VBQWGEjs.isDateTimeProp; exports.isDecimalProp = _chunk76VBQWGEjs.isDecimalProp; exports.isDevelopment = _chunk76VBQWGEjs.isDevelopment; exports.isDoubleProp = _chunk76VBQWGEjs.isDoubleProp; exports.isEnumProp = _chunk76VBQWGEjs.isEnumProp; exports.isFloatProp = _chunk76VBQWGEjs.isFloatProp; exports.isHasManyRelationProp = _chunk76VBQWGEjs.isHasManyRelationProp; exports.isInDocker = _chunk76VBQWGEjs.isInDocker; exports.isIntegerProp = _chunk76VBQWGEjs.isIntegerProp; exports.isJsonProp = _chunk76VBQWGEjs.isJsonProp; exports.isKnexError = _chunk76VBQWGEjs.isKnexError; exports.isLocal = _chunk76VBQWGEjs.isLocal; exports.isManyToManyRelationProp = _chunk76VBQWGEjs.isManyToManyRelationProp; exports.isOneToOneRelationProp = _chunk76VBQWGEjs.isOneToOneRelationProp; exports.isProduction = _chunk76VBQWGEjs.isProduction; exports.isRefField = _chunk76VBQWGEjs.isRefField; exports.isRelationProp = _chunk76VBQWGEjs.isRelationProp; exports.isRemote = _chunk76VBQWGEjs.isRemote; exports.isSoException = _chunk76VBQWGEjs.isSoException; exports.isStaging = _chunk76VBQWGEjs.isStaging; exports.isStringProp = _chunk76VBQWGEjs.isStringProp; exports.isTest = _chunk76VBQWGEjs.isTest; exports.isTextProp = _chunk76VBQWGEjs.isTextProp; exports.isTimeProp = _chunk76VBQWGEjs.isTimeProp; exports.isTimestampProp = _chunk76VBQWGEjs.isTimestampProp; exports.isUuidProp = _chunk76VBQWGEjs.isUuidProp; exports.isVirtualProp = _chunk76VBQWGEjs.isVirtualProp; exports.nonNullable = _chunk76VBQWGEjs.nonNullable; exports.objToMap = objToMap; exports.p = p; exports.propNodeToZodTypeDef = _chunk76VBQWGEjs.propNodeToZodTypeDef; exports.propToZodTypeDef = _chunk76VBQWGEjs.propToZodTypeDef; exports.registeredApis = _chunk76VBQWGEjs.registeredApis; exports.serializeZodType = _chunk76VBQWGEjs.serializeZodType; exports.setupErrorHandler = setupErrorHandler; exports.unwrapPromiseOnce = _chunk76VBQWGEjs.unwrapPromiseOnce; exports.zArrayable = _chunk76VBQWGEjs.zArrayable; exports.zodTypeToTsTypeDef = _chunk76VBQWGEjs.zodTypeToTsTypeDef; exports.zodTypeToZodCode = _chunk76VBQWGEjs.zodTypeToZodCode;
426
+ exports.AlreadyProcessedException = _chunkHATLA54Zjs.AlreadyProcessedException; exports.ApiParamType = _chunkHATLA54Zjs.ApiParamType; exports.BadRequestException = _chunkHATLA54Zjs.BadRequestException; exports.BaseModel = _chunkHATLA54Zjs.BaseModel; exports.BaseModelClass = _chunkHATLA54Zjs.BaseModelClass; exports.DB = _chunkHATLA54Zjs.DB; exports.DuplicateRowException = _chunkHATLA54Zjs.DuplicateRowException; exports.Entity = _chunkHATLA54Zjs.Entity; exports.EntityManager = _chunkHATLA54Zjs.EntityManager; exports.FixtureManager = _chunkHATLA54Zjs.FixtureManager; exports.FixtureManagerClass = _chunkHATLA54Zjs.FixtureManagerClass; exports.GenerateOptions = _chunkHATLA54Zjs.GenerateOptions; exports.InternalServerErrorException = _chunkHATLA54Zjs.InternalServerErrorException; exports.Migrator = _chunkHATLA54Zjs.Migrator; exports.NotFoundException = _chunkHATLA54Zjs.NotFoundException; exports.PathAndCode = _chunkHATLA54Zjs.PathAndCode; exports.RenderingNode = _chunkHATLA54Zjs.RenderingNode; exports.SQLDateTimeString = _chunkHATLA54Zjs.SQLDateTimeString; exports.ServiceUnavailableException = _chunkHATLA54Zjs.ServiceUnavailableException; exports.SoException = _chunkHATLA54Zjs.SoException; exports.Sonamu = _chunkHATLA54Zjs.Sonamu; exports.SonamuQueryMode = _chunkHATLA54Zjs.SonamuQueryMode; exports.Syncer = _chunkHATLA54Zjs.Syncer; exports.TargetNotFoundException = _chunkHATLA54Zjs.TargetNotFoundException; exports.TemplateKey = _chunkHATLA54Zjs.TemplateKey; exports.TemplateOptions = _chunkHATLA54Zjs.TemplateOptions; exports.UnauthorizedException = _chunkHATLA54Zjs.UnauthorizedException; exports.UpsertBuilder = _chunkHATLA54Zjs.UpsertBuilder; exports.api = _chunkHATLA54Zjs.api; exports.apiParamToTsCode = _chunkHATLA54Zjs.apiParamToTsCode; exports.apiParamTypeToTsType = _chunkHATLA54Zjs.apiParamTypeToTsType; exports.asArray = asArray; exports.findApiRootPath = _chunkHATLA54Zjs.findApiRootPath; exports.findAppRootPath = _chunkHATLA54Zjs.findAppRootPath; exports.getTextTypeLength = _chunkHATLA54Zjs.getTextTypeLength; exports.getZodObjectFromApi = _chunkHATLA54Zjs.getZodObjectFromApi; exports.getZodObjectFromApiParams = _chunkHATLA54Zjs.getZodObjectFromApiParams; exports.getZodTypeFromApiParamType = _chunkHATLA54Zjs.getZodTypeFromApiParamType; exports.globAsync = _chunkHATLA54Zjs.globAsync; exports.i = i; exports.importMultiple = _chunkHATLA54Zjs.importMultiple; exports.isBelongsToOneRelationProp = _chunkHATLA54Zjs.isBelongsToOneRelationProp; exports.isBigIntegerProp = _chunkHATLA54Zjs.isBigIntegerProp; exports.isBooleanProp = _chunkHATLA54Zjs.isBooleanProp; exports.isCustomJoinClause = _chunkHATLA54Zjs.isCustomJoinClause; exports.isDaemonServer = _chunkHATLA54Zjs.isDaemonServer; exports.isDateProp = _chunkHATLA54Zjs.isDateProp; exports.isDateTimeProp = _chunkHATLA54Zjs.isDateTimeProp; exports.isDecimalProp = _chunkHATLA54Zjs.isDecimalProp; exports.isDevelopment = _chunkHATLA54Zjs.isDevelopment; exports.isDoubleProp = _chunkHATLA54Zjs.isDoubleProp; exports.isEnumProp = _chunkHATLA54Zjs.isEnumProp; exports.isFloatProp = _chunkHATLA54Zjs.isFloatProp; exports.isHasManyRelationProp = _chunkHATLA54Zjs.isHasManyRelationProp; exports.isInDocker = _chunkHATLA54Zjs.isInDocker; exports.isIntegerProp = _chunkHATLA54Zjs.isIntegerProp; exports.isJsonProp = _chunkHATLA54Zjs.isJsonProp; exports.isKnexError = _chunkHATLA54Zjs.isKnexError; exports.isLocal = _chunkHATLA54Zjs.isLocal; exports.isManyToManyRelationProp = _chunkHATLA54Zjs.isManyToManyRelationProp; exports.isOneToOneRelationProp = _chunkHATLA54Zjs.isOneToOneRelationProp; exports.isProduction = _chunkHATLA54Zjs.isProduction; exports.isRefField = _chunkHATLA54Zjs.isRefField; exports.isRelationProp = _chunkHATLA54Zjs.isRelationProp; exports.isRemote = _chunkHATLA54Zjs.isRemote; exports.isSoException = _chunkHATLA54Zjs.isSoException; exports.isStaging = _chunkHATLA54Zjs.isStaging; exports.isStringProp = _chunkHATLA54Zjs.isStringProp; exports.isTest = _chunkHATLA54Zjs.isTest; exports.isTextProp = _chunkHATLA54Zjs.isTextProp; exports.isTimeProp = _chunkHATLA54Zjs.isTimeProp; exports.isTimestampProp = _chunkHATLA54Zjs.isTimestampProp; exports.isUuidProp = _chunkHATLA54Zjs.isUuidProp; exports.isVirtualProp = _chunkHATLA54Zjs.isVirtualProp; exports.nonNullable = _chunkHATLA54Zjs.nonNullable; exports.objToMap = objToMap; exports.p = p; exports.propNodeToZodTypeDef = _chunkHATLA54Zjs.propNodeToZodTypeDef; exports.propToZodTypeDef = _chunkHATLA54Zjs.propToZodTypeDef; exports.registeredApis = _chunkHATLA54Zjs.registeredApis; exports.serializeZodType = _chunkHATLA54Zjs.serializeZodType; exports.setupErrorHandler = setupErrorHandler; exports.unwrapPromiseOnce = _chunkHATLA54Zjs.unwrapPromiseOnce; exports.zArrayable = _chunkHATLA54Zjs.zArrayable; exports.zodTypeToTsTypeDef = _chunkHATLA54Zjs.zodTypeToTsTypeDef; exports.zodTypeToZodCode = _chunkHATLA54Zjs.zodTypeToZodCode;
427
427
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.2.48",
3
+ "version": "0.2.49",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
package/src/api/sonamu.ts CHANGED
@@ -185,10 +185,11 @@ class SonamuClass {
185
185
 
186
186
  async withFastify(
187
187
  server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
188
- config: SonamuFastifyConfig
188
+ config: SonamuFastifyConfig,
189
+ options?: { enableSync?: boolean; doSilent?: boolean }
189
190
  ) {
190
191
  if (this.isInitialized === false) {
191
- await this.init();
192
+ await this.init(options?.doSilent, options?.enableSync);
192
193
  }
193
194
 
194
195
  // 전체 라우팅 리스트
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ts-node
2
2
 
3
3
  import { spawnSync } from "child_process";
4
- import { resolve } from "path";
4
+ import { extname, resolve } from "path";
5
5
  import { existsSync, readFileSync } from "fs";
6
6
 
7
7
  const cjsPath = resolve(__dirname, "bin/cli.js");
@@ -9,10 +9,29 @@ const esmPath = resolve(__dirname, "bin/cli.mjs");
9
9
 
10
10
  const isESM = () => {
11
11
  const packageJsonPath = resolve(process.cwd(), "package.json");
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
13
+
14
+ // package.json에 "type": "module" 설정 확인
15
+ if (packageJson.type === "module") {
16
+ return true;
17
+ }
18
+
19
+ // 환경 변수에서 ESM 여부 확인
20
+ if (process.env.USE_ESM === "true") {
21
+ return true;
22
+ }
23
+
24
+ // package.json에 "type": "module" 설정
12
25
  if (existsSync(packageJsonPath)) {
13
26
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
14
27
  return packageJson.type === "module";
15
28
  }
29
+
30
+ // main 필드가 .mjs로 끝나는지 확인
31
+ if (packageJson.main && extname(packageJson.main) === ".mjs") {
32
+ return true;
33
+ }
34
+
16
35
  return false;
17
36
  };
18
37
 
package/src/bin/cli.ts CHANGED
@@ -166,9 +166,13 @@ async function fixture_init() {
166
166
  console.log("DUMP...");
167
167
  const dumpFilename = `/tmp/sonamu-fixture-init-${Date.now()}.sql`;
168
168
  const srcConn = srcConfig.connection as Knex.ConnectionConfig;
169
+ const migrationsDump = `/tmp/sonamu-fixture-init-migrations-${Date.now()}.sql`;
169
170
  execSync(
170
171
  `mysqldump -h${srcConn.host} -u${srcConn.user} -p${srcConn.password} --single-transaction -d --no-create-db --triggers ${srcConn.database} > ${dumpFilename}`
171
172
  );
173
+ execSync(
174
+ `mysqldump -h${srcConn.host} -u${srcConn.user} -p${srcConn.password} --single-transaction --no-create-db --triggers ${srcConn.database} knex_migrations knex_migrations_lock > ${migrationsDump}`
175
+ );
172
176
 
173
177
  // 2. 대상DB 각각에 대하여 존재여부 확인 후 붓기
174
178
  for await (const { label, config, toSkip } of targets) {
@@ -200,21 +204,7 @@ async function fixture_init() {
200
204
  execSync(`${mysqlCmd} -e 'DROP DATABASE IF EXISTS \`${conn.database}\`'`);
201
205
  execSync(`${mysqlCmd} -e 'CREATE DATABASE \`${conn.database}\`'`);
202
206
  execSync(`${mysqlCmd} ${conn.database} < ${dumpFilename}`);
203
-
204
- // 3. knex migration 정보 복사
205
- await Promise.all(
206
- ["knex_migrations", "knex_migrations_lock"].map(async (tableName) => {
207
- const [table] = await db.raw(
208
- `SHOW TABLES FROM \`${srcConn.database}\` LIKE '${tableName}'`
209
- );
210
- if (table?.length) {
211
- await db.raw(
212
- `INSERT INTO \`${conn.database}\`.${tableName}
213
- SELECT * FROM \`${srcConn.database}\`.${tableName}`
214
- );
215
- }
216
- })
217
- );
207
+ execSync(`${mysqlCmd} ${conn.database} < ${migrationsDump}`);
218
208
 
219
209
  await db.destroy();
220
210
  }
@@ -13,7 +13,7 @@ export type RowWithId<Id extends string> = {
13
13
  * Batch update rows in a table. Technically its a patch since it only updates the specified columns. Any omitted columns will not be affected
14
14
  * @param knex
15
15
  * @param tableName
16
- * @param id
16
+ * @param ids
17
17
  * @param rows
18
18
  * @param chunkSize
19
19
  * @param trx
@@ -21,7 +21,7 @@ export type RowWithId<Id extends string> = {
21
21
  export async function batchUpdate<Id extends string>(
22
22
  knex: Knex,
23
23
  tableName: string,
24
- id: Id,
24
+ ids: Id[],
25
25
  rows: RowWithId<Id>[],
26
26
  chunkSize = 50,
27
27
  trx: Knex.Transaction | null = null
@@ -35,7 +35,7 @@ export async function batchUpdate<Id extends string>(
35
35
  chunk: RowWithId<Id>[],
36
36
  transaction: Knex.Transaction
37
37
  ) => {
38
- const sql = generateBatchUpdateSQL(knex, tableName, chunk, id);
38
+ const sql = generateBatchUpdateSQL(knex, tableName, chunk, ids);
39
39
  return knex.raw(sql).transacting(transaction);
40
40
  };
41
41
 
@@ -73,20 +73,30 @@ function generateBatchUpdateSQL<Id extends string>(
73
73
  knex: Knex,
74
74
  tableName: string,
75
75
  data: Record<string, any>[],
76
- identifier: Id
76
+ identifiers: Id[]
77
77
  ) {
78
78
  const keySet = generateKeySetFromData(data);
79
79
  const bindings = [];
80
80
 
81
+ const invalidIdentifiers = identifiers.filter((id) => !keySet.has(id));
82
+ if (invalidIdentifiers.length > 0) {
83
+ throw new Error(
84
+ `Invalid identifiers: ${invalidIdentifiers.join(", ")}. Identifiers must exist in the data`
85
+ );
86
+ }
87
+
81
88
  const cases = [];
82
89
  for (const key of keySet) {
83
- if (key === identifier) continue;
90
+ if (identifiers.includes(key as Id)) continue;
84
91
 
85
92
  const rows = [];
86
93
  for (const row of data) {
87
94
  if (Object.hasOwnProperty.call(row, key)) {
88
- rows.push(`WHEN \`${identifier}\` = ? THEN ?`);
89
- bindings.push(row[identifier], row[key]);
95
+ const whereClause = identifiers
96
+ .map((id) => `\`${id}\` = ?`)
97
+ .join(" AND ");
98
+ rows.push(`WHEN (${whereClause}) THEN ?`);
99
+ bindings.push(...identifiers.map((i) => row[i]), row[key]);
90
100
  }
91
101
  }
92
102
 
@@ -94,13 +104,18 @@ function generateBatchUpdateSQL<Id extends string>(
94
104
  cases.push(`\`${key}\` = CASE ${whenThen} ELSE \`${key}\` END`);
95
105
  }
96
106
 
97
- const whereInIds = data.map((row) => row[identifier]);
98
- const whereInPlaceholders = whereInIds.map(() => "?").join(", ");
107
+ const whereInClauses = identifiers
108
+ .map((col) => `${col} IN (${data.map(() => "?").join(", ")})`)
109
+ .join(" AND ");
110
+
111
+ const whereInBindings = identifiers.flatMap((col) =>
112
+ data.map((row) => row[col])
113
+ );
114
+
99
115
  const sql = knex.raw(
100
- `UPDATE \`${tableName}\` SET ${cases.join(
101
- ", "
102
- )} WHERE ${identifier} IN (${whereInPlaceholders})`,
103
- [...bindings, ...whereInIds]
116
+ `UPDATE \`${tableName}\` SET ${cases.join(", ")} WHERE ${whereInClauses}`,
117
+ [...bindings, ...whereInBindings]
104
118
  );
105
- return sql.toString();
119
+
120
+ return sql.toQuery();
106
121
  }
@@ -144,17 +144,26 @@ export class UpsertBuilder {
144
144
  };
145
145
  }
146
146
 
147
- async upsert(wdb: Knex, tableName: string): Promise<number[]> {
148
- return this.upsertOrInsert(wdb, tableName, "upsert");
147
+ async upsert(
148
+ wdb: Knex,
149
+ tableName: string,
150
+ chunkSize?: number
151
+ ): Promise<number[]> {
152
+ return this.upsertOrInsert(wdb, tableName, "upsert", chunkSize);
149
153
  }
150
- async insertOnly(wdb: Knex, tableName: string): Promise<number[]> {
151
- return this.upsertOrInsert(wdb, tableName, "insert");
154
+ async insertOnly(
155
+ wdb: Knex,
156
+ tableName: string,
157
+ chunkSize?: number
158
+ ): Promise<number[]> {
159
+ return this.upsertOrInsert(wdb, tableName, "insert", chunkSize);
152
160
  }
153
161
 
154
162
  async upsertOrInsert(
155
163
  wdb: Knex,
156
164
  tableName: string,
157
- mode: "upsert" | "insert"
165
+ mode: "upsert" | "insert",
166
+ chunkSize?: number
158
167
  ): Promise<number[]> {
159
168
  if (this.hasTable(tableName) === false) {
160
169
  return [];
@@ -177,22 +186,6 @@ export class UpsertBuilder {
177
186
  throw new Error(`${tableName} 해결되지 않은 참조가 있습니다.`);
178
187
  }
179
188
 
180
- // 내부 참조 있는 경우 필터하여 분리
181
- const groups = _.groupBy(table.rows, (row) =>
182
- Object.entries(row).some(([, value]) => isRefField(value))
183
- ? "selfRef"
184
- : "normal"
185
- );
186
- const targetRows = groups.normal;
187
-
188
- // Insert On Duplicate Update
189
- const q = wdb.insert(targetRows).into(tableName);
190
- if (mode === "insert") {
191
- await q;
192
- } else if (mode === "upsert") {
193
- await q.onDuplicateUpdate.apply(q, Object.keys(targetRows[0]));
194
- }
195
-
196
189
  // 전체 테이블 순회하여 현재 테이블 참조하는 모든 테이블 추출
197
190
  const { references, refTables } = Array.from(this.tables).reduce(
198
191
  (r, [, table]) => {
@@ -211,19 +204,39 @@ export class UpsertBuilder {
211
204
  refTables: [] as TableData[],
212
205
  }
213
206
  );
214
-
215
207
  const extractFields = _.uniq(references).map(
216
208
  (reference) => reference.split(".")[1]
217
209
  );
218
210
 
219
- // UUID 기준으로 id 추출
220
- const uuids = table.rows.map((row) => row.uuid);
221
- const upsertedRows = await wdb(tableName)
222
- .select(_.uniq(["uuid", "id", ...extractFields]))
223
- .whereIn("uuid", uuids);
224
- const uuidMap = new Map<string, any>(
225
- upsertedRows.map((row: any) => [row.uuid, row])
211
+ // 내부 참조 있는 경우 필터하여 분리
212
+ const groups = _.groupBy(table.rows, (row) =>
213
+ Object.entries(row).some(([, value]) => isRefField(value))
214
+ ? "selfRef"
215
+ : "normal"
226
216
  );
217
+ const normalRows = groups.normal ?? [];
218
+ const selfRefRows = groups.selfRef ?? [];
219
+
220
+ const chunks = chunkSize ? _.chunk(normalRows, chunkSize) : [normalRows];
221
+ const uuidMap = new Map<string, any>();
222
+
223
+ for (const chunk of chunks) {
224
+ const q = wdb.insert(chunk).into(tableName);
225
+ if (mode === "insert") {
226
+ await q;
227
+ } else if (mode === "upsert") {
228
+ await q.onDuplicateUpdate.apply(q, Object.keys(normalRows[0]));
229
+ }
230
+
231
+ // upsert된 row들을 다시 조회하여 uuidMap에 저장
232
+ const uuids = chunk.map((row) => row.uuid);
233
+ const upsertedRows = await wdb(tableName)
234
+ .select(_.uniq(["uuid", "id", ...extractFields]))
235
+ .whereIn("uuid", uuids);
236
+ upsertedRows.forEach((row: any) => {
237
+ uuidMap.set(row.uuid, row);
238
+ });
239
+ }
227
240
 
228
241
  // 해당 테이블 참조를 실제 밸류로 변경
229
242
  refTables.map((table) => {
@@ -245,14 +258,17 @@ export class UpsertBuilder {
245
258
  });
246
259
  });
247
260
 
248
- const ids = Array.from(uuidMap.values()).map((val) => val.id);
261
+ const allIds = Array.from(uuidMap.values()).map((row) => row.id);
249
262
 
250
- if (groups.selfRef) {
251
- const selfRefIds = await this.upsert(wdb, tableName);
252
- return [...ids, ...selfRefIds];
263
+ // 자기 참조가 있는 경우 재귀적으로 upsert
264
+ if (selfRefRows.length > 0) {
265
+ // 처리된 데이터를 제외하고 다시 upsert
266
+ table.rows = selfRefRows;
267
+ const selfRefIds = await this.upsert(wdb, tableName, chunkSize);
268
+ allIds.push(...selfRefIds);
253
269
  }
254
270
 
255
- return ids;
271
+ return allIds;
256
272
  }
257
273
 
258
274
  async updateBatch(
@@ -260,7 +276,7 @@ export class UpsertBuilder {
260
276
  tableName: string,
261
277
  options?: {
262
278
  chunkSize?: number;
263
- where?: string;
279
+ where?: string | string[];
264
280
  }
265
281
  ): Promise<void> {
266
282
  options = _.defaults(options, {
@@ -276,16 +292,14 @@ export class UpsertBuilder {
276
292
  return;
277
293
  }
278
294
 
295
+ const whereColumns = Array.isArray(options.where)
296
+ ? options.where
297
+ : [options.where ?? "id"];
279
298
  const rows = table.rows.map((_row) => {
280
299
  const { uuid, ...row } = _row;
281
300
  return row as RowWithId<string>;
282
301
  });
283
- await batchUpdate(
284
- wdb,
285
- tableName,
286
- options.where ?? "id",
287
- rows,
288
- options.chunkSize
289
- );
302
+
303
+ await batchUpdate(wdb, tableName, whereColumns, rows, options.chunkSize);
290
304
  }
291
305
  }