sonamu 0.2.48 → 0.2.50
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/bin/cli-wrapper.d.mts +1 -0
- package/dist/bin/cli-wrapper.js +12 -2
- package/dist/bin/cli-wrapper.js.map +1 -1
- package/dist/bin/cli-wrapper.mjs +44 -0
- package/dist/bin/cli-wrapper.mjs.map +1 -0
- package/dist/bin/cli.d.mts +2 -0
- package/dist/bin/cli.js +54 -62
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +879 -0
- package/dist/bin/cli.mjs.map +1 -0
- package/dist/{chunk-76VBQWGE.js → chunk-4EET56IE.js} +173 -99
- package/dist/chunk-4EET56IE.js.map +1 -0
- package/dist/chunk-HEPO4HGK.mjs +7834 -0
- package/dist/chunk-HEPO4HGK.mjs.map +1 -0
- package/dist/chunk-JXJTFHF7.mjs +20 -0
- package/dist/chunk-JXJTFHF7.mjs.map +1 -0
- package/dist/index.d.mts +1492 -0
- package/dist/index.d.ts +15 -7
- package/dist/index.js +3 -3
- package/dist/index.mjs +429 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +1 -1
- package/src/api/sonamu.ts +3 -2
- package/src/bin/cli-wrapper.ts +20 -1
- package/src/bin/cli.ts +5 -15
- package/src/database/_batch_update.ts +29 -14
- package/src/database/upsert-builder.ts +56 -42
- package/src/templates/service.template.ts +9 -1
- package/src/testing/fixture-manager.ts +122 -36
- package/src/types/types.ts +3 -2
- package/src/utils/utils.ts +15 -13
- package/tsup.config.js +1 -4
- package/dist/chunk-76VBQWGE.js.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
119
|
+
|
|
120
|
+
return sql.toQuery();
|
|
106
121
|
}
|
|
@@ -144,17 +144,26 @@ export class UpsertBuilder {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
async upsert(
|
|
148
|
-
|
|
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(
|
|
151
|
-
|
|
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
|
-
//
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
261
|
+
const allIds = Array.from(uuidMap.values()).map((row) => row.id);
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
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
|
}
|
|
@@ -30,6 +30,11 @@ export class Template__service extends Template {
|
|
|
30
30
|
// 서비스 TypeSource
|
|
31
31
|
const { lines, importKeys } = this.getTypeSource(apis);
|
|
32
32
|
|
|
33
|
+
// AxiosProgressEvent 있는지 확인
|
|
34
|
+
const hasAxiosProgressEvent = apis.find((api) =>
|
|
35
|
+
(api.options.clients ?? []).includes("axios-multipart")
|
|
36
|
+
);
|
|
37
|
+
|
|
33
38
|
return {
|
|
34
39
|
...this.getTargetAndPath(names),
|
|
35
40
|
body: lines.join("\n"),
|
|
@@ -41,6 +46,9 @@ export class Template__service extends Template {
|
|
|
41
46
|
`import qs from "qs";`,
|
|
42
47
|
`import useSWR, { SWRResponse } from "swr";`,
|
|
43
48
|
`import { fetch, ListResult, SWRError, SwrOptions, handleConditional, swrPostFetcher } from '../sonamu.shared';`,
|
|
49
|
+
...(hasAxiosProgressEvent
|
|
50
|
+
? [`import { AxiosProgressEvent } from 'axios';`]
|
|
51
|
+
: []),
|
|
44
52
|
],
|
|
45
53
|
};
|
|
46
54
|
}
|
|
@@ -210,7 +218,7 @@ export async function ${methodNameAxios}${typeParamsDef}(${paramsDef}): Promise<
|
|
|
210
218
|
export async function ${api.methodName}${typeParamsDef}(
|
|
211
219
|
${paramsDef}${paramsDefComma}
|
|
212
220
|
file: File,
|
|
213
|
-
onUploadProgress?: (pe:
|
|
221
|
+
onUploadProgress?: (pe:AxiosProgressEvent) => void
|
|
214
222
|
): Promise<${returnTypeDef}> {
|
|
215
223
|
const formData = new FormData();
|
|
216
224
|
${formDataDef}
|
|
@@ -158,6 +158,10 @@ export class FixtureManagerClass {
|
|
|
158
158
|
await transaction(tableName).truncate();
|
|
159
159
|
|
|
160
160
|
const rows = await frdb(tableName);
|
|
161
|
+
if (rows.length === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
161
165
|
console.log(chalk.blue(tableName), rows.length);
|
|
162
166
|
await transaction
|
|
163
167
|
.insert(
|
|
@@ -307,58 +311,72 @@ export class FixtureManagerClass {
|
|
|
307
311
|
throw new Error("No records found");
|
|
308
312
|
}
|
|
309
313
|
|
|
310
|
-
const
|
|
311
|
-
const records: FixtureRecord[] = [];
|
|
314
|
+
const fixtures: FixtureRecord[] = [];
|
|
312
315
|
for (const row of rows) {
|
|
313
|
-
const initialRecordsLength =
|
|
314
|
-
await this.createFixtureRecord(entity, row
|
|
315
|
-
|
|
316
|
+
const initialRecordsLength = fixtures.length;
|
|
317
|
+
const newRecords = await this.createFixtureRecord(entity, row);
|
|
318
|
+
fixtures.push(...newRecords);
|
|
319
|
+
const currentFixtureRecord = fixtures.find(
|
|
316
320
|
(r) => r.fixtureId === `${entityId}#${row.id}`
|
|
317
321
|
);
|
|
318
322
|
|
|
319
323
|
if (currentFixtureRecord) {
|
|
320
324
|
// 현재 fixture로부터 생성된 fetchedRecords 설정
|
|
321
|
-
currentFixtureRecord.fetchedRecords =
|
|
325
|
+
currentFixtureRecord.fetchedRecords = fixtures
|
|
322
326
|
.filter((r) => r.fixtureId !== currentFixtureRecord.fixtureId)
|
|
323
327
|
.slice(initialRecordsLength)
|
|
324
328
|
.map((r) => r.fixtureId);
|
|
325
329
|
}
|
|
326
330
|
}
|
|
327
331
|
|
|
328
|
-
for await (const
|
|
329
|
-
const entity = EntityManager.get(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
for await (const fixture of fixtures) {
|
|
333
|
+
const entity = EntityManager.get(fixture.entityId);
|
|
334
|
+
|
|
335
|
+
// targetDB에 해당 레코드가 존재하는지 확인
|
|
336
|
+
const row = await targetDB(entity.table).where("id", fixture.id).first();
|
|
332
337
|
if (row) {
|
|
333
|
-
await this.createFixtureRecord(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
const [record] = await this.createFixtureRecord(entity, row, {
|
|
339
|
+
singleRecord: true,
|
|
340
|
+
_db: targetDB,
|
|
341
|
+
});
|
|
342
|
+
fixture.target = record;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// targetDB에 해당 레코드가 존재하지 않는 경우, unique 제약을 위반하는지 확인
|
|
347
|
+
const uniqueRow = await this.checkUniqueViolation(
|
|
348
|
+
targetDB,
|
|
349
|
+
entity,
|
|
350
|
+
fixture
|
|
351
|
+
);
|
|
352
|
+
if (uniqueRow) {
|
|
353
|
+
const [record] = await this.createFixtureRecord(entity, uniqueRow, {
|
|
354
|
+
singleRecord: true,
|
|
355
|
+
_db: targetDB,
|
|
356
|
+
});
|
|
357
|
+
fixture.unique = record;
|
|
342
358
|
}
|
|
343
359
|
}
|
|
344
360
|
|
|
345
|
-
return
|
|
361
|
+
return fixtures;
|
|
346
362
|
}
|
|
347
363
|
|
|
348
364
|
async createFixtureRecord(
|
|
349
365
|
entity: Entity,
|
|
350
366
|
row: any,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
367
|
+
options?: {
|
|
368
|
+
singleRecord?: boolean;
|
|
369
|
+
_db?: Knex;
|
|
370
|
+
},
|
|
371
|
+
visitedEntities = new Set<string>()
|
|
372
|
+
): Promise<FixtureRecord[]> {
|
|
356
373
|
const fixtureId = `${entity.id}#${row.id}`;
|
|
357
374
|
if (visitedEntities.has(fixtureId)) {
|
|
358
|
-
return;
|
|
375
|
+
return [];
|
|
359
376
|
}
|
|
360
377
|
visitedEntities.add(fixtureId);
|
|
361
378
|
|
|
379
|
+
const records: FixtureRecord[] = [];
|
|
362
380
|
const record: FixtureRecord = {
|
|
363
381
|
fixtureId,
|
|
364
382
|
entityId: entity.id,
|
|
@@ -378,7 +396,7 @@ export class FixtureManagerClass {
|
|
|
378
396
|
value: row[prop.name],
|
|
379
397
|
};
|
|
380
398
|
|
|
381
|
-
const db = _db ?? BaseModel.getDB("w");
|
|
399
|
+
const db = options?._db ?? BaseModel.getDB("w");
|
|
382
400
|
if (isManyToManyRelationProp(prop)) {
|
|
383
401
|
const relatedEntity = EntityManager.get(prop.with);
|
|
384
402
|
const throughTable = prop.joinTable;
|
|
@@ -412,32 +430,34 @@ export class FixtureManagerClass {
|
|
|
412
430
|
if (relatedId) {
|
|
413
431
|
record.belongsRecords.push(`${prop.with}#${relatedId}`);
|
|
414
432
|
}
|
|
415
|
-
if (!singleRecord && relatedId) {
|
|
433
|
+
if (!options?.singleRecord && relatedId) {
|
|
416
434
|
const relatedEntity = EntityManager.get(prop.with);
|
|
417
435
|
const relatedRow = await db(relatedEntity.table)
|
|
418
436
|
.where("id", relatedId)
|
|
419
437
|
.first();
|
|
420
438
|
if (relatedRow) {
|
|
421
|
-
await this.createFixtureRecord(
|
|
439
|
+
const newRecords = await this.createFixtureRecord(
|
|
422
440
|
relatedEntity,
|
|
423
441
|
relatedRow,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
singleRecord,
|
|
427
|
-
_db
|
|
442
|
+
options,
|
|
443
|
+
visitedEntities
|
|
428
444
|
);
|
|
445
|
+
records.push(...newRecords);
|
|
429
446
|
}
|
|
430
447
|
}
|
|
431
448
|
}
|
|
432
449
|
}
|
|
433
450
|
|
|
434
451
|
records.push(record);
|
|
452
|
+
return records;
|
|
435
453
|
}
|
|
436
454
|
|
|
437
455
|
async insertFixtures(
|
|
438
456
|
dbName: keyof SonamuDBConfig,
|
|
439
|
-
|
|
457
|
+
_fixtures: FixtureRecord[]
|
|
440
458
|
) {
|
|
459
|
+
const fixtures = _.uniqBy(_fixtures, (f) => f.fixtureId);
|
|
460
|
+
|
|
441
461
|
this.buildDependencyGraph(fixtures);
|
|
442
462
|
const insertionOrder = this.getInsertionOrder();
|
|
443
463
|
const db = knex(Sonamu.dbConfig[dbName]);
|
|
@@ -447,7 +467,27 @@ export class FixtureManagerClass {
|
|
|
447
467
|
|
|
448
468
|
for (const fixtureId of insertionOrder) {
|
|
449
469
|
const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
|
|
450
|
-
await this.insertFixture(trx, fixture);
|
|
470
|
+
const result = await this.insertFixture(trx, fixture);
|
|
471
|
+
if (result.id !== fixture.id) {
|
|
472
|
+
// ID가 변경된 경우, 다른 fixture에서 참조하는 경우가 찾아서 수정
|
|
473
|
+
console.log(
|
|
474
|
+
chalk.yellow(
|
|
475
|
+
`Unique constraint violation: ${fixture.entityId}#${fixture.id} -> ${fixture.entityId}#${result.id}`
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
fixtures.forEach((f) => {
|
|
479
|
+
Object.values(f.columns).forEach((column) => {
|
|
480
|
+
if (
|
|
481
|
+
column.prop.type === "relation" &&
|
|
482
|
+
column.prop.with === result.entityId &&
|
|
483
|
+
column.value === fixture.id
|
|
484
|
+
) {
|
|
485
|
+
column.value = result.id;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
fixture.id = result.id;
|
|
490
|
+
}
|
|
451
491
|
}
|
|
452
492
|
|
|
453
493
|
for (const fixtureId of insertionOrder) {
|
|
@@ -468,7 +508,7 @@ export class FixtureManagerClass {
|
|
|
468
508
|
});
|
|
469
509
|
}
|
|
470
510
|
|
|
471
|
-
return records;
|
|
511
|
+
return _.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
|
|
472
512
|
}
|
|
473
513
|
|
|
474
514
|
private getInsertionOrder() {
|
|
@@ -598,6 +638,14 @@ export class FixtureManagerClass {
|
|
|
598
638
|
const entity = EntityManager.get(fixture.entityId);
|
|
599
639
|
|
|
600
640
|
try {
|
|
641
|
+
const uniqueFound = await this.checkUniqueViolation(db, entity, fixture);
|
|
642
|
+
if (uniqueFound) {
|
|
643
|
+
return {
|
|
644
|
+
entityId: fixture.entityId,
|
|
645
|
+
id: uniqueFound.id,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
601
649
|
const found = await db(entity.table).where("id", fixture.id).first();
|
|
602
650
|
if (found && !fixture.override) {
|
|
603
651
|
return {
|
|
@@ -688,5 +736,43 @@ export class FixtureManagerClass {
|
|
|
688
736
|
throw new Error("Failed to find fixtureLoader in fixture.ts");
|
|
689
737
|
}
|
|
690
738
|
}
|
|
739
|
+
|
|
740
|
+
// 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
|
|
741
|
+
private async checkUniqueViolation(
|
|
742
|
+
db: Knex,
|
|
743
|
+
entity: Entity,
|
|
744
|
+
fixture: FixtureRecord
|
|
745
|
+
) {
|
|
746
|
+
const uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
|
|
747
|
+
if (uniqueIndexes.length === 0) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let uniqueQuery = db(entity.table);
|
|
752
|
+
for (const index of uniqueIndexes) {
|
|
753
|
+
// 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
|
|
754
|
+
if (
|
|
755
|
+
index.columns.some(
|
|
756
|
+
(column) => fixture.columns[column.split("_id")[0]].value === null
|
|
757
|
+
)
|
|
758
|
+
) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
uniqueQuery = uniqueQuery.orWhere((qb) => {
|
|
763
|
+
for (const column of index.columns) {
|
|
764
|
+
const field = column.split("_id")[0];
|
|
765
|
+
|
|
766
|
+
if (Array.isArray(fixture.columns[field].value)) {
|
|
767
|
+
qb.whereIn(column, fixture.columns[field].value);
|
|
768
|
+
} else {
|
|
769
|
+
qb.andWhere(column, fixture.columns[field].value);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const [uniqueFound] = await uniqueQuery;
|
|
775
|
+
return uniqueFound;
|
|
776
|
+
}
|
|
691
777
|
}
|
|
692
778
|
export const FixtureManager = new FixtureManagerClass();
|
package/src/types/types.ts
CHANGED
|
@@ -738,8 +738,9 @@ export type FixtureRecord = {
|
|
|
738
738
|
};
|
|
739
739
|
};
|
|
740
740
|
fetchedRecords: string[];
|
|
741
|
-
belongsRecords: string[];
|
|
742
|
-
target?: FixtureRecord; // Import 대상 DB 레코드
|
|
741
|
+
belongsRecords: string[];
|
|
742
|
+
target?: FixtureRecord; // Import 대상 DB 레코드(id가 같은)
|
|
743
|
+
unique?: FixtureRecord; // Import 대상 DB 레코드(unique key가 같은)
|
|
743
744
|
override?: boolean;
|
|
744
745
|
};
|
|
745
746
|
|
package/src/utils/utils.ts
CHANGED
|
@@ -17,19 +17,21 @@ export async function importMultiple(
|
|
|
17
17
|
filePaths: string[],
|
|
18
18
|
doRefresh: boolean = false
|
|
19
19
|
): Promise<{ filePath: string; imported: any }[]> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
})
|
|
32
|
-
|
|
20
|
+
const results: { filePath: string; imported: any }[] = [];
|
|
21
|
+
|
|
22
|
+
for (const filePath of filePaths) {
|
|
23
|
+
const importPath = "./" + path.relative(__dirname, filePath);
|
|
24
|
+
if (doRefresh) {
|
|
25
|
+
delete require.cache[require.resolve(importPath)];
|
|
26
|
+
}
|
|
27
|
+
const imported = await import(importPath);
|
|
28
|
+
results.push({
|
|
29
|
+
filePath,
|
|
30
|
+
imported,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return results;
|
|
33
35
|
}
|
|
34
36
|
export async function findAppRootPath() {
|
|
35
37
|
const apiRootPath = await findApiRootPath();
|
package/tsup.config.js
CHANGED
|
@@ -4,10 +4,7 @@ import { defineConfig } from "tsup";
|
|
|
4
4
|
export default defineConfig({
|
|
5
5
|
entry: ["src/index.ts", "src/bin/cli.ts", "src/bin/cli-wrapper.ts"],
|
|
6
6
|
dts: true,
|
|
7
|
-
format: [
|
|
8
|
-
// "esm",
|
|
9
|
-
"cjs",
|
|
10
|
-
],
|
|
7
|
+
format: ["esm", "cjs"],
|
|
11
8
|
target: "es2020",
|
|
12
9
|
clean: true,
|
|
13
10
|
sourcemap: true,
|