sonamu 0.5.2 → 0.5.3
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/package.json +5 -1
- package/src/api/code-converters.ts +1 -1
- package/src/api/decorators.ts +23 -9
- package/src/bin/cli-wrapper.ts +1 -1
- package/src/database/puri-wrapper.ts +24 -4
- package/src/database/puri.ts +128 -25
- package/src/database/puri.types.ts +46 -41
- package/src/index.ts +2 -1
- package/src/migration/code-generation.ts +3 -3
- package/src/migration/types.ts +2 -1
- package/src/templates/generated_http.template.ts +42 -11
- package/src/templates/generated_sso.template.ts +3 -1
- package/src/templates/service.template.ts +20 -7
- package/src/testing/fixture-manager.ts +2 -0
- package/.swcrc +0 -15
- package/import-to-require.js +0 -27
- package/nodemon.json +0 -6
- package/tsconfig.json +0 -56
- package/tsup.config.js +0 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"url": "https://github.com/ping-alive/sonamu.git"
|
|
25
25
|
},
|
|
26
26
|
"bin": "./dist/bin/cli-wrapper.js",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
27
31
|
"dependencies": {
|
|
28
32
|
"@aws-sdk/client-s3": "^3.921.0",
|
|
29
33
|
"@aws-sdk/s3-request-presigner": "^3.921.0",
|
|
@@ -400,7 +400,7 @@ export function zodTypeToZodCode(zt: z.ZodType<any>): string {
|
|
|
400
400
|
.join(",")}])`;
|
|
401
401
|
case "enum":
|
|
402
402
|
// NOTE: z.enum(["A", "B"])도 z.enum({ A: "A", B: "B" })로 처리됨.
|
|
403
|
-
return `z.enum(
|
|
403
|
+
return `z.enum({${Object.entries((zt as z.ZodEnum).def.entries)
|
|
404
404
|
.map(([key, val]) =>
|
|
405
405
|
typeof val === "string" ? `${key}: "${val}"` : `${key}: ${val}`)
|
|
406
406
|
.join(", ")}})`;
|
package/src/api/decorators.ts
CHANGED
|
@@ -40,12 +40,16 @@ export type StreamDecoratorOptions = {
|
|
|
40
40
|
guards?: GuardKey[];
|
|
41
41
|
description?: string;
|
|
42
42
|
};
|
|
43
|
+
export type UploadDecoratorOptions = {
|
|
44
|
+
mode?: "single" | "multiple";
|
|
45
|
+
};
|
|
43
46
|
export const registeredApis: {
|
|
44
47
|
modelName: string;
|
|
45
48
|
methodName: string;
|
|
46
49
|
path: string;
|
|
47
50
|
options: ApiDecoratorOptions;
|
|
48
51
|
streamOptions?: StreamDecoratorOptions;
|
|
52
|
+
uploadOptions?: UploadDecoratorOptions;
|
|
49
53
|
}[] = [];
|
|
50
54
|
export type ExtendedApi = {
|
|
51
55
|
modelName: string;
|
|
@@ -53,6 +57,7 @@ export type ExtendedApi = {
|
|
|
53
57
|
path: string;
|
|
54
58
|
options: ApiDecoratorOptions;
|
|
55
59
|
streamOptions?: StreamDecoratorOptions;
|
|
60
|
+
uploadOptions?: UploadDecoratorOptions;
|
|
56
61
|
typeParameters: ApiParamType.TypeParam[];
|
|
57
62
|
parameters: ApiParam[];
|
|
58
63
|
returnType: ApiParamType;
|
|
@@ -176,13 +181,23 @@ export function transactional(options: TransactionalOptions = {}) {
|
|
|
176
181
|
};
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
export function upload() {
|
|
184
|
+
export function upload(options: UploadDecoratorOptions = {}) {
|
|
180
185
|
return function (
|
|
181
186
|
_target: Object,
|
|
182
187
|
_propertyKey: string,
|
|
183
188
|
descriptor: PropertyDescriptor
|
|
184
189
|
) {
|
|
185
190
|
const originalMethod = descriptor.value;
|
|
191
|
+
const modelName = _target.constructor.name.match(/(.+)Class$/)![1];
|
|
192
|
+
const methodName = _propertyKey;
|
|
193
|
+
|
|
194
|
+
// registeredApis에서 해당 API 찾아서 uploadOptions 추가
|
|
195
|
+
const existingApi = registeredApis.find(
|
|
196
|
+
(api) => api.modelName === modelName && api.methodName === methodName
|
|
197
|
+
);
|
|
198
|
+
if (existingApi) {
|
|
199
|
+
existingApi.uploadOptions = options;
|
|
200
|
+
}
|
|
186
201
|
|
|
187
202
|
descriptor.value = async function (this: any, ...args: any[]) {
|
|
188
203
|
const { request } = Sonamu.getContext();
|
|
@@ -196,20 +211,19 @@ export function upload() {
|
|
|
196
211
|
throw new Error("Storage가 설정되지 않았습니다.");
|
|
197
212
|
}
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (rawFile) {
|
|
202
|
-
const { FileStorage } = await import("../file-storage/file-storage");
|
|
203
|
-
uploadContext.file = new FileStorage(rawFile, storage);
|
|
204
|
-
}
|
|
205
|
-
} else if (request.files) {
|
|
206
|
-
const { FileStorage } = await import("../file-storage/file-storage");
|
|
214
|
+
const { FileStorage } = await import("../file-storage/file-storage");
|
|
215
|
+
if (options.mode === "multiple") {
|
|
207
216
|
const rawFilesIterator = request.files();
|
|
208
217
|
for await (const rawFile of rawFilesIterator) {
|
|
209
218
|
if (rawFile) {
|
|
210
219
|
uploadContext.files.push(new FileStorage(rawFile, storage));
|
|
211
220
|
}
|
|
212
221
|
}
|
|
222
|
+
} else {
|
|
223
|
+
const rawFile = await request.file();
|
|
224
|
+
if (rawFile) {
|
|
225
|
+
uploadContext.file = new FileStorage(rawFile, storage);
|
|
226
|
+
}
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
return Sonamu.uploadStorage.run({ uploadContext }, () => {
|
package/src/bin/cli-wrapper.ts
CHANGED
|
@@ -17,9 +17,12 @@ export type TransactionalOptions = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export class PuriWrapper<
|
|
20
|
-
DBSchema extends DatabaseSchemaExtend = DatabaseSchemaExtend
|
|
20
|
+
DBSchema extends DatabaseSchemaExtend = DatabaseSchemaExtend,
|
|
21
21
|
> {
|
|
22
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
public knex: Knex,
|
|
24
|
+
public upsertBuilder: UpsertBuilder
|
|
25
|
+
) {}
|
|
23
26
|
|
|
24
27
|
raw(sql: string): Knex.Raw {
|
|
25
28
|
return this.knex.raw(sql);
|
|
@@ -40,14 +43,14 @@ export class PuriWrapper<
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
async transaction<T>(
|
|
43
|
-
callback: (trx:
|
|
46
|
+
callback: (trx: PuriTransactionWrapper) => Promise<T>,
|
|
44
47
|
options: TransactionalOptions = {}
|
|
45
48
|
): Promise<T> {
|
|
46
49
|
const { isolation, readOnly } = options;
|
|
47
50
|
|
|
48
51
|
return this.knex.transaction(
|
|
49
52
|
async (trx) => {
|
|
50
|
-
return callback(new
|
|
53
|
+
return callback(new PuriTransactionWrapper(trx, this.upsertBuilder));
|
|
51
54
|
},
|
|
52
55
|
{ isolationLevel: isolation, readOnly }
|
|
53
56
|
);
|
|
@@ -127,3 +130,20 @@ export class PuriWrapper<
|
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
}
|
|
133
|
+
|
|
134
|
+
export class PuriTransactionWrapper extends PuriWrapper {
|
|
135
|
+
constructor(
|
|
136
|
+
public trx: Knex.Transaction,
|
|
137
|
+
public upsertBuilder: UpsertBuilder
|
|
138
|
+
) {
|
|
139
|
+
super(trx, upsertBuilder);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async rollback(): Promise<void> {
|
|
143
|
+
await this.trx.rollback();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async commit(): Promise<void> {
|
|
147
|
+
await this.trx.commit();
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/database/puri.ts
CHANGED
|
@@ -224,7 +224,9 @@ export class Puri<
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// WhereIn (조인된 테이블 컬럼도 지원)
|
|
227
|
-
whereIn<
|
|
227
|
+
whereIn<
|
|
228
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
229
|
+
>(
|
|
228
230
|
column: TColumn,
|
|
229
231
|
values: ExtractColumnType<
|
|
230
232
|
TSchema,
|
|
@@ -264,7 +266,10 @@ export class Puri<
|
|
|
264
266
|
|
|
265
267
|
whereMatch<
|
|
266
268
|
TColumn extends FulltextColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
267
|
-
>(
|
|
269
|
+
>(
|
|
270
|
+
column: TColumn,
|
|
271
|
+
value: string
|
|
272
|
+
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
268
273
|
this.knexQuery.whereRaw(`MATCH (${String(column)}) AGAINST (?)`, [value]);
|
|
269
274
|
return this;
|
|
270
275
|
}
|
|
@@ -276,7 +281,9 @@ export class Puri<
|
|
|
276
281
|
) => WhereGroup<TSchema, TTable, TOriginal, TJoined>
|
|
277
282
|
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
278
283
|
this.knexQuery.where((builder) => {
|
|
279
|
-
const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
|
|
284
|
+
const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
|
|
285
|
+
builder
|
|
286
|
+
);
|
|
280
287
|
callback(group);
|
|
281
288
|
});
|
|
282
289
|
return this;
|
|
@@ -288,7 +295,9 @@ export class Puri<
|
|
|
288
295
|
) => WhereGroup<TSchema, TTable, TOriginal, TJoined>
|
|
289
296
|
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
290
297
|
this.knexQuery.orWhere((builder) => {
|
|
291
|
-
const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
|
|
298
|
+
const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
|
|
299
|
+
builder
|
|
300
|
+
);
|
|
292
301
|
callback(group);
|
|
293
302
|
});
|
|
294
303
|
return this;
|
|
@@ -297,12 +306,22 @@ export class Puri<
|
|
|
297
306
|
// Join
|
|
298
307
|
join<
|
|
299
308
|
TJoinTable extends keyof TSchema,
|
|
300
|
-
TLColumn extends AvailableColumns<
|
|
301
|
-
|
|
309
|
+
TLColumn extends AvailableColumns<
|
|
310
|
+
TSchema,
|
|
311
|
+
TTable,
|
|
312
|
+
TOriginal,
|
|
313
|
+
TJoined & Record<TJoinTable, TSchema[TJoinTable]>
|
|
314
|
+
>,
|
|
315
|
+
TRColumn extends AvailableColumns<
|
|
316
|
+
TSchema,
|
|
317
|
+
TTable,
|
|
318
|
+
TOriginal,
|
|
319
|
+
TJoined & Record<TJoinTable, TSchema[TJoinTable]>
|
|
320
|
+
>,
|
|
302
321
|
>(
|
|
303
322
|
table: TJoinTable,
|
|
304
323
|
left: TLColumn,
|
|
305
|
-
right: TRColumn
|
|
324
|
+
right: TRColumn
|
|
306
325
|
): Puri<
|
|
307
326
|
TSchema,
|
|
308
327
|
TTable,
|
|
@@ -327,7 +346,13 @@ export class Puri<
|
|
|
327
346
|
alias: TAlias,
|
|
328
347
|
left: string,
|
|
329
348
|
right: string
|
|
330
|
-
): Puri<
|
|
349
|
+
): Puri<
|
|
350
|
+
TSchema,
|
|
351
|
+
TTable,
|
|
352
|
+
TOriginal,
|
|
353
|
+
TResult,
|
|
354
|
+
TJoined & Record<TAlias, TSubResult>
|
|
355
|
+
>;
|
|
331
356
|
join(
|
|
332
357
|
table: string,
|
|
333
358
|
left: string,
|
|
@@ -361,12 +386,22 @@ export class Puri<
|
|
|
361
386
|
|
|
362
387
|
leftJoin<
|
|
363
388
|
TJoinTable extends keyof TSchema,
|
|
364
|
-
TLColumn extends AvailableColumns<
|
|
365
|
-
|
|
389
|
+
TLColumn extends AvailableColumns<
|
|
390
|
+
TSchema,
|
|
391
|
+
TTable,
|
|
392
|
+
TOriginal,
|
|
393
|
+
TJoined & Record<TJoinTable, TSchema[TJoinTable]>
|
|
394
|
+
>,
|
|
395
|
+
TRColumn extends AvailableColumns<
|
|
396
|
+
TSchema,
|
|
397
|
+
TTable,
|
|
398
|
+
TOriginal,
|
|
399
|
+
TJoined & Record<TJoinTable, TSchema[TJoinTable]>
|
|
400
|
+
>,
|
|
366
401
|
>(
|
|
367
402
|
table: TJoinTable,
|
|
368
403
|
left: TLColumn,
|
|
369
|
-
right: TRColumn
|
|
404
|
+
right: TRColumn
|
|
370
405
|
): Puri<
|
|
371
406
|
TSchema,
|
|
372
407
|
TTable,
|
|
@@ -407,7 +442,15 @@ export class Puri<
|
|
|
407
442
|
}
|
|
408
443
|
|
|
409
444
|
// OrderBy
|
|
410
|
-
orderBy<
|
|
445
|
+
orderBy<
|
|
446
|
+
TColumn extends ResultAvailableColumns<
|
|
447
|
+
TSchema,
|
|
448
|
+
TTable,
|
|
449
|
+
TOriginal,
|
|
450
|
+
TResult,
|
|
451
|
+
TJoined
|
|
452
|
+
>,
|
|
453
|
+
>(
|
|
411
454
|
column: TColumn,
|
|
412
455
|
direction: "asc" | "desc"
|
|
413
456
|
): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
|
|
@@ -431,21 +474,39 @@ export class Puri<
|
|
|
431
474
|
}
|
|
432
475
|
|
|
433
476
|
// Group by (조인된 테이블 컬럼도 지원)
|
|
434
|
-
groupBy<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
477
|
+
groupBy<
|
|
478
|
+
TColumns extends ResultAvailableColumns<
|
|
479
|
+
TSchema,
|
|
480
|
+
TTable,
|
|
481
|
+
TOriginal,
|
|
482
|
+
TResult,
|
|
483
|
+
TJoined
|
|
484
|
+
>,
|
|
485
|
+
>(...columns: TColumns[]): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
|
|
486
|
+
groupBy(
|
|
487
|
+
...columns: string[]
|
|
488
|
+
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
438
489
|
this.knexQuery.groupBy(...(columns as string[]));
|
|
439
490
|
return this;
|
|
440
491
|
}
|
|
441
492
|
|
|
442
493
|
having(condition: string): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
|
|
443
|
-
having<
|
|
494
|
+
having<
|
|
495
|
+
TColumn extends ResultAvailableColumns<
|
|
496
|
+
TSchema,
|
|
497
|
+
TTable,
|
|
498
|
+
TOriginal,
|
|
499
|
+
TResult,
|
|
500
|
+
TJoined
|
|
501
|
+
>,
|
|
502
|
+
>(
|
|
444
503
|
condition: TColumn,
|
|
445
504
|
operator: ComparisonOperator,
|
|
446
505
|
value: any
|
|
447
506
|
): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
|
|
448
|
-
having(
|
|
507
|
+
having(
|
|
508
|
+
...conditions: string[]
|
|
509
|
+
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
449
510
|
this.knexQuery.having(...(conditions as [string, string, string]));
|
|
450
511
|
return this;
|
|
451
512
|
}
|
|
@@ -641,6 +702,32 @@ export class Puri<
|
|
|
641
702
|
raw(): Knex.QueryBuilder {
|
|
642
703
|
return this.knexQuery;
|
|
643
704
|
}
|
|
705
|
+
|
|
706
|
+
increment<
|
|
707
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
708
|
+
>(
|
|
709
|
+
column: TColumn,
|
|
710
|
+
value: number
|
|
711
|
+
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
712
|
+
if (value <= 0) {
|
|
713
|
+
throw new Error("Increment value must be greater than 0");
|
|
714
|
+
}
|
|
715
|
+
this.knexQuery.increment(column, value);
|
|
716
|
+
return this;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
decrement<
|
|
720
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
721
|
+
>(
|
|
722
|
+
column: TColumn,
|
|
723
|
+
value: number
|
|
724
|
+
): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
|
|
725
|
+
if (value <= 0) {
|
|
726
|
+
throw new Error("Decrement value must be greater than 0");
|
|
727
|
+
}
|
|
728
|
+
this.knexQuery.decrement(column, value);
|
|
729
|
+
return this;
|
|
730
|
+
}
|
|
644
731
|
}
|
|
645
732
|
|
|
646
733
|
// 11. Database 클래스
|
|
@@ -685,7 +772,9 @@ class WhereGroup<
|
|
|
685
772
|
orWhere(
|
|
686
773
|
conditions: WhereCondition<TSchema, TTable, TOriginal, TJoined>
|
|
687
774
|
): WhereGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
688
|
-
orWhere<
|
|
775
|
+
orWhere<
|
|
776
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
777
|
+
>(
|
|
689
778
|
column: TColumn,
|
|
690
779
|
value: ExtractColumnType<
|
|
691
780
|
TSchema,
|
|
@@ -695,7 +784,9 @@ class WhereGroup<
|
|
|
695
784
|
TJoined
|
|
696
785
|
>
|
|
697
786
|
): WhereGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
698
|
-
orWhere<
|
|
787
|
+
orWhere<
|
|
788
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
789
|
+
>(
|
|
699
790
|
column: TColumn,
|
|
700
791
|
operator: ComparisonOperator | "like",
|
|
701
792
|
value: ExtractColumnType<
|
|
@@ -712,7 +803,9 @@ class WhereGroup<
|
|
|
712
803
|
return this;
|
|
713
804
|
}
|
|
714
805
|
|
|
715
|
-
whereIn<
|
|
806
|
+
whereIn<
|
|
807
|
+
TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
|
|
808
|
+
>(
|
|
716
809
|
column: TColumn,
|
|
717
810
|
values: ExtractColumnType<
|
|
718
811
|
TSchema,
|
|
@@ -789,18 +882,28 @@ export class JoinClauseGroup<
|
|
|
789
882
|
constructor(private callback: Knex.JoinClause) {}
|
|
790
883
|
|
|
791
884
|
on(
|
|
792
|
-
callback: (
|
|
885
|
+
callback: (
|
|
886
|
+
joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>
|
|
887
|
+
) => void
|
|
888
|
+
): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
889
|
+
on(
|
|
890
|
+
column: string,
|
|
891
|
+
value: any
|
|
793
892
|
): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
794
|
-
on(column: string, value: any): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
795
893
|
on(...args: any[]): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined> {
|
|
796
894
|
this.callback.on(...(args as [string, string]));
|
|
797
895
|
return this;
|
|
798
896
|
}
|
|
799
897
|
|
|
800
898
|
orOn(
|
|
801
|
-
callback: (
|
|
899
|
+
callback: (
|
|
900
|
+
joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>
|
|
901
|
+
) => void
|
|
902
|
+
): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
903
|
+
orOn(
|
|
904
|
+
column: string,
|
|
905
|
+
value: any
|
|
802
906
|
): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
803
|
-
orOn(column: string, value: any): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
|
|
804
907
|
orOn(...args: any[]): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined> {
|
|
805
908
|
this.callback.orOn(...(args as [string, string]));
|
|
806
909
|
return this;
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
export type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "<>" | "!=";
|
|
2
2
|
export type Expand<T> = T extends any[]
|
|
3
3
|
? { [K in keyof T[0]]: T[0][K] }[] // 배열이면 첫 번째 요소를 Expand하고 배열로 감쌈
|
|
4
|
-
: T extends object
|
|
5
|
-
|
|
4
|
+
: T extends object
|
|
5
|
+
? { [K in keyof T]: T[K] }
|
|
6
|
+
: T;
|
|
7
|
+
|
|
6
8
|
// EmptyRecord가 남아있으면 AvailableColumns 추론이 제대로 되지 않음 (EmptyRecord를 {}로 변경하면 정상 동작함)
|
|
7
|
-
export type MergeJoined<TExisting, TNew> =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
: TExisting & TNew; // 이후 join: 누적
|
|
9
|
+
export type MergeJoined<TExisting, TNew> = TExisting extends EmptyRecord
|
|
10
|
+
? TNew // 첫 join: EmptyRecord 제거하고 대체
|
|
11
|
+
: TExisting & TNew; // 이후 join: 누적
|
|
11
12
|
|
|
12
|
-
type DeepEqual<T, U> = [T] extends [U]
|
|
13
|
-
|
|
13
|
+
type DeepEqual<T, U> = [T] extends [U]
|
|
14
|
+
? [U] extends [T]
|
|
15
|
+
? true
|
|
16
|
+
: false
|
|
17
|
+
: false;
|
|
18
|
+
type Extends<T, U> =
|
|
19
|
+
DeepEqual<T, Record<string, never>> extends true
|
|
20
|
+
? false
|
|
21
|
+
: T extends U
|
|
22
|
+
? true
|
|
23
|
+
: false;
|
|
14
24
|
type NullableToOptional<T> = {
|
|
15
|
-
[K in keyof T as T[K] extends null | undefined ? K : never]?: Exclude<
|
|
25
|
+
[K in keyof T as T[K] extends null | undefined ? K : never]?: Exclude<
|
|
26
|
+
T[K],
|
|
27
|
+
null | undefined
|
|
28
|
+
>;
|
|
16
29
|
} & Partial<{
|
|
17
|
-
[K in keyof T as T[K] extends null | undefined ? never : K]: T[K]
|
|
30
|
+
[K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
|
|
18
31
|
}>;
|
|
19
32
|
|
|
20
33
|
// Join 등이 Empty 상태일 떄 {}가 아니라 EmptyRecord를 써서
|
|
@@ -27,7 +40,9 @@ export type ResultAvailableColumns<
|
|
|
27
40
|
TOriginal = any,
|
|
28
41
|
TResult = any,
|
|
29
42
|
TJoined = EmptyRecord,
|
|
30
|
-
> =
|
|
43
|
+
> =
|
|
44
|
+
| AvailableColumns<TSchema, T, TOriginal, TJoined>
|
|
45
|
+
| `${keyof TResult & string}`;
|
|
31
46
|
|
|
32
47
|
// 사용 가능한 컬럼 경로 타입 (메인 테이블 + 조인된 테이블들)
|
|
33
48
|
export type AvailableColumns<
|
|
@@ -37,14 +52,14 @@ export type AvailableColumns<
|
|
|
37
52
|
TJoined = EmptyRecord,
|
|
38
53
|
> = T extends keyof TSchema
|
|
39
54
|
? // 기존 테이블 케이스
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
| (Extends<TJoined, Record<string, any>> extends false
|
|
56
|
+
? // 이게 TSchema[T]에 존재하면
|
|
57
|
+
keyof TSchema[T]
|
|
58
|
+
: {
|
|
59
|
+
[K in keyof TJoined]: TJoined[K] extends Record<string, any>
|
|
60
|
+
? `${string & K}.${keyof TJoined[K] & string}`
|
|
61
|
+
: never;
|
|
62
|
+
}[keyof TJoined])
|
|
48
63
|
| `${T & string}.${keyof TSchema[T] & string}`
|
|
49
64
|
: // 서브쿼리 케이스 (T는 alias string)
|
|
50
65
|
| keyof TOriginal
|
|
@@ -156,27 +171,15 @@ export type WhereCondition<
|
|
|
156
171
|
T extends keyof TSchema | string,
|
|
157
172
|
TOriginal = any,
|
|
158
173
|
TJoined = EmptyRecord,
|
|
159
|
-
> =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// 조인된 테이블들의 조건들
|
|
169
|
-
(TJoined extends Record<string, any>
|
|
170
|
-
? {
|
|
171
|
-
[K in keyof TJoined as TJoined[K] extends Record<string, any>
|
|
172
|
-
? keyof TJoined[K] & string
|
|
173
|
-
: never]?: TJoined[K] extends Record<string, any>
|
|
174
|
-
?
|
|
175
|
-
| TJoined[K][K extends keyof TJoined[K] ? K : never]
|
|
176
|
-
| TJoined[K][K extends keyof TJoined[K] ? K : never][]
|
|
177
|
-
: never;
|
|
178
|
-
}
|
|
179
|
-
: Record<string, never>);
|
|
174
|
+
> = {
|
|
175
|
+
[key in AvailableColumns<TSchema, T, TOriginal, TJoined>]?: ExtractColumnType<
|
|
176
|
+
TSchema,
|
|
177
|
+
T,
|
|
178
|
+
key & string,
|
|
179
|
+
TOriginal,
|
|
180
|
+
TJoined
|
|
181
|
+
>;
|
|
182
|
+
};
|
|
180
183
|
|
|
181
184
|
// Fulltext index 컬럼 추출 타입 (메인 테이블 + 조인된 테이블)
|
|
182
185
|
export type FulltextColumns<
|
|
@@ -219,4 +222,6 @@ export type FulltextColumns<
|
|
|
219
222
|
: never);
|
|
220
223
|
|
|
221
224
|
// Insert 타입: id, created_at 제외
|
|
222
|
-
export type InsertData<T> = NullableToOptional<
|
|
225
|
+
export type InsertData<T> = NullableToOptional<
|
|
226
|
+
Omit<T, "id" | "created_at" | "__fulltext__">
|
|
227
|
+
>;
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,10 @@ export * from "./utils/controller";
|
|
|
16
16
|
export * from "./utils/model";
|
|
17
17
|
export * from "./utils/utils";
|
|
18
18
|
export * from "./testing/fixture-manager";
|
|
19
|
-
export * from "./migration/migrator";
|
|
20
19
|
export * from "./entity/entity-manager";
|
|
21
20
|
export * from "./entity/entity";
|
|
21
|
+
export * from "./migration/migrator";
|
|
22
|
+
export * from "./migration/types";
|
|
22
23
|
export * from "./file-storage/driver";
|
|
23
24
|
|
|
24
25
|
// export * from "./api/code-converters";
|
|
@@ -77,7 +77,7 @@ function genColumnDefinitions(columns: MigrationColumn[]): string[] {
|
|
|
77
77
|
columnType = "text";
|
|
78
78
|
}
|
|
79
79
|
chains.push(
|
|
80
|
-
`${
|
|
80
|
+
`${columnType}('${column.name}'${
|
|
81
81
|
column.length ? `, ${column.length}` : ""
|
|
82
82
|
}${extraType ? `, '${extraType}'` : ""})`
|
|
83
83
|
);
|
|
@@ -680,9 +680,9 @@ export async function generateAlterCode(
|
|
|
680
680
|
// boolean인 경우 기본값 정규화 (MySQL에서는 TINYINT(1)로 저장되므로 0 또는 1로 정규화)
|
|
681
681
|
// TODO: db.ts에 typeCase 설정 확인하여 처리하도록 수정 필요
|
|
682
682
|
if (col.type === "boolean" && col.defaultTo !== undefined) {
|
|
683
|
-
if (col.defaultTo === "0" || col.defaultTo === "false") {
|
|
683
|
+
if (col.defaultTo === "0" || col.defaultTo.toLowerCase() === "false") {
|
|
684
684
|
col.defaultTo = "0";
|
|
685
|
-
} else if (col.defaultTo === "1" || col.defaultTo === "true") {
|
|
685
|
+
} else if (col.defaultTo === "1" || col.defaultTo.toLowerCase() === "true") {
|
|
686
686
|
col.defaultTo = "1";
|
|
687
687
|
}
|
|
688
688
|
}
|
package/src/migration/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SonamuDBConfig } from "../database/db";
|
|
1
2
|
import { GenMigrationCode } from "../types/types";
|
|
2
3
|
|
|
3
4
|
export type MigrationCode = {
|
|
@@ -10,7 +11,7 @@ export type MigrationStatus = {
|
|
|
10
11
|
codes: MigrationCode[];
|
|
11
12
|
conns: {
|
|
12
13
|
name: string;
|
|
13
|
-
connKey:
|
|
14
|
+
connKey: keyof SonamuDBConfig;
|
|
14
15
|
connString: ConnString;
|
|
15
16
|
currentVersion: string;
|
|
16
17
|
status: string | number;
|
|
@@ -84,7 +84,12 @@ export class Template__generated_http extends Template {
|
|
|
84
84
|
])
|
|
85
85
|
);
|
|
86
86
|
} else if (zodType instanceof z.ZodArray) {
|
|
87
|
-
return [
|
|
87
|
+
return [
|
|
88
|
+
this.zodTypeToReqDefault(
|
|
89
|
+
(zodType as z.ZodArray<z.ZodType>).element,
|
|
90
|
+
name
|
|
91
|
+
),
|
|
92
|
+
];
|
|
88
93
|
} else if (zodType instanceof z.ZodString) {
|
|
89
94
|
if (name.endsWith("_at") || name.endsWith("_date") || name === "range") {
|
|
90
95
|
return "2000-01-01";
|
|
@@ -97,17 +102,23 @@ export class Template__generated_http extends Template {
|
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
const minValue = zodType.minValue ?? 0;
|
|
100
|
-
return minValue > Number.MIN_SAFE_INTEGER
|
|
105
|
+
return minValue > Number.MIN_SAFE_INTEGER ? minValue : 0;
|
|
101
106
|
} else if (zodType instanceof z.ZodBoolean) {
|
|
102
107
|
return false;
|
|
103
108
|
} else if (zodType instanceof z.ZodEnum) {
|
|
104
109
|
return zodType.options[0];
|
|
105
110
|
} else if (zodType instanceof z.ZodOptional) {
|
|
106
|
-
return this.zodTypeToReqDefault(
|
|
111
|
+
return this.zodTypeToReqDefault(
|
|
112
|
+
(zodType as z.ZodOptional<z.ZodType>).def.innerType,
|
|
113
|
+
name
|
|
114
|
+
);
|
|
107
115
|
} else if (zodType instanceof z.ZodNullable) {
|
|
108
116
|
return null;
|
|
109
117
|
} else if (zodType instanceof z.ZodUnion) {
|
|
110
|
-
return this.zodTypeToReqDefault(
|
|
118
|
+
return this.zodTypeToReqDefault(
|
|
119
|
+
(zodType as z.ZodUnion<z.ZodType[]>).def.options[0],
|
|
120
|
+
name
|
|
121
|
+
);
|
|
111
122
|
} else if (zodType instanceof z.ZodUnknown) {
|
|
112
123
|
return "unknown";
|
|
113
124
|
} else if (zodType instanceof z.ZodTuple) {
|
|
@@ -119,16 +130,29 @@ export class Template__generated_http extends Template {
|
|
|
119
130
|
} else if (zodType instanceof z.ZodLiteral) {
|
|
120
131
|
return zodType.value;
|
|
121
132
|
} else if (zodType instanceof z.ZodRecord || zodType instanceof z.ZodMap) {
|
|
122
|
-
const kvDef = (
|
|
133
|
+
const kvDef = (
|
|
134
|
+
zodType as z.ZodRecord<any, z.ZodType> | z.ZodMap<z.ZodType, z.ZodType>
|
|
135
|
+
).def;
|
|
123
136
|
const key = this.zodTypeToReqDefault(kvDef.keyType, name) as any;
|
|
124
137
|
const value = this.zodTypeToReqDefault(kvDef.valueType, name);
|
|
125
138
|
return { [key]: value };
|
|
126
139
|
} else if (zodType instanceof z.ZodSet) {
|
|
127
|
-
return [
|
|
140
|
+
return [
|
|
141
|
+
this.zodTypeToReqDefault(
|
|
142
|
+
(zodType as z.ZodSet<z.ZodType>).def.valueType,
|
|
143
|
+
name
|
|
144
|
+
),
|
|
145
|
+
];
|
|
128
146
|
} else if (zodType instanceof z.ZodIntersection) {
|
|
129
|
-
return this.zodTypeToReqDefault(
|
|
147
|
+
return this.zodTypeToReqDefault(
|
|
148
|
+
(zodType as z.ZodIntersection<z.ZodType, z.ZodType>).def.right,
|
|
149
|
+
name
|
|
150
|
+
);
|
|
130
151
|
} else if (zodType instanceof z.ZodDefault) {
|
|
131
|
-
return this.zodTypeToReqDefault(
|
|
152
|
+
return this.zodTypeToReqDefault(
|
|
153
|
+
(zodType as z.ZodDefault<z.ZodType>).def.innerType,
|
|
154
|
+
name
|
|
155
|
+
);
|
|
132
156
|
} else {
|
|
133
157
|
// console.log(zodType);
|
|
134
158
|
return `unknown-${zodType.type}`;
|
|
@@ -140,8 +164,15 @@ export class Template__generated_http extends Template {
|
|
|
140
164
|
references: { [typeName: string]: z.ZodObject<any> }
|
|
141
165
|
): { [key: string]: unknown } {
|
|
142
166
|
const reqType = getZodObjectFromApi(api, references);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
try {
|
|
168
|
+
const def = this.zodTypeToReqDefault(reqType, "unknownName") as {
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
};
|
|
171
|
+
return def;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Invalid zod type detected on ${api.modelName}:${api.methodName}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
}
|
|
@@ -89,12 +89,14 @@ export class Template__generated_sso extends Template {
|
|
|
89
89
|
} as Omit<SourceCode, "label">
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
+
const body = sourceCode.lines.join("\n");
|
|
93
|
+
const isUsingManyToManyBaseSchema = body.includes("ManyToManyBaseSchema");
|
|
92
94
|
return {
|
|
93
95
|
...this.getTargetAndPath(),
|
|
94
96
|
body: sourceCode.lines.join("\n"),
|
|
95
97
|
importKeys: sourceCode.importKeys,
|
|
96
98
|
customHeaders: [
|
|
97
|
-
`import { SubsetQuery, ManyToManyBaseSchema } from "sonamu";`,
|
|
99
|
+
`import { SubsetQuery, ${isUsingManyToManyBaseSchema ? "ManyToManyBaseSchema" : ""} } from "sonamu";`,
|
|
98
100
|
],
|
|
99
101
|
};
|
|
100
102
|
}
|
|
@@ -219,18 +219,31 @@ export async function ${methodNameAxios}${typeParamsDef}(${paramsDef}): Promise<
|
|
|
219
219
|
returnTypeDef: string,
|
|
220
220
|
paramsWithoutContext: ApiParam[]
|
|
221
221
|
) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
const isMultiple = api.uploadOptions?.mode === "multiple";
|
|
223
|
+
const fileParamName = isMultiple ? "files" : "file";
|
|
224
|
+
const fileParamType = isMultiple ? "File[]" : "File";
|
|
225
|
+
|
|
226
|
+
const formDataDef = isMultiple
|
|
227
|
+
? [
|
|
228
|
+
`${fileParamName}.forEach(f => formData.append("${fileParamName}", f));`,
|
|
229
|
+
...paramsWithoutContext.map(
|
|
230
|
+
(param) =>
|
|
231
|
+
`formData.append('${param.name}', String(${param.name}));`
|
|
232
|
+
),
|
|
233
|
+
].join("\n")
|
|
234
|
+
: [
|
|
235
|
+
`formData.append("${fileParamName}", ${fileParamName});`,
|
|
236
|
+
...paramsWithoutContext.map(
|
|
237
|
+
(param) =>
|
|
238
|
+
`formData.append('${param.name}', String(${param.name}));`
|
|
239
|
+
),
|
|
240
|
+
].join("\n");
|
|
228
241
|
|
|
229
242
|
const paramsDefComma = paramsDef !== "" ? ", " : "";
|
|
230
243
|
return `
|
|
231
244
|
export async function ${api.methodName}${typeParamsDef}(
|
|
232
245
|
${paramsDef}${paramsDefComma}
|
|
233
|
-
|
|
246
|
+
${fileParamName}: ${fileParamType},
|
|
234
247
|
onUploadProgress?: (pe:AxiosProgressEvent) => void
|
|
235
248
|
): Promise<${returnTypeDef}> {
|
|
236
249
|
const formData = new FormData();
|
|
@@ -568,6 +568,8 @@ export class FixtureManagerClass {
|
|
|
568
568
|
if (!isRelationProp(prop)) {
|
|
569
569
|
if (prop.type === "json") {
|
|
570
570
|
insertData[propName] = JSON.stringify(column.value);
|
|
571
|
+
} else if (prop.type === "timestamp" || prop.type === "datetime") {
|
|
572
|
+
insertData[propName] = new Date(column.value);
|
|
571
573
|
} else {
|
|
572
574
|
insertData[propName] = column.value;
|
|
573
575
|
}
|
package/.swcrc
DELETED
package/import-to-require.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const fs = require("fs/promises");
|
|
2
|
-
|
|
3
|
-
export const ImportToRequirePlugin = {
|
|
4
|
-
name: "import-to-require",
|
|
5
|
-
setup(build) {
|
|
6
|
-
if (build.initialOptions.define.TSUP_FORMAT === '"cjs"') {
|
|
7
|
-
// 빌드 전에 src/database/db.ts 파일을 읽어서 변환
|
|
8
|
-
build.onLoad({ filter: /database\/db.ts/ }, async (args) => {
|
|
9
|
-
console.debug(`reading ${args.path}`);
|
|
10
|
-
let contents = await fs.readFile(args.path, "utf8");
|
|
11
|
-
|
|
12
|
-
// 'await import(' 패턴을 찾아 'require('로 변환
|
|
13
|
-
contents = contents.replace(
|
|
14
|
-
/\bawait import\(([^)]+)\)/g,
|
|
15
|
-
(_, modulePath) => {
|
|
16
|
-
return `require(${modulePath})`;
|
|
17
|
-
}
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
contents,
|
|
22
|
-
loader: "ts", // TypeScript를 지원하도록 'ts' 로더 설정
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
};
|
package/nodemon.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
/* Basic Options */
|
|
4
|
-
"target": "ESNext",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"sourceMap": true,
|
|
8
|
-
"lib": ["esnext", "dom"],
|
|
9
|
-
|
|
10
|
-
// NOTE(Haze, 251106): SSE 관련 fastify 타입 이슈로 명시적으로 추가함.
|
|
11
|
-
"types": ["fastify-sse-v2"],
|
|
12
|
-
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
|
|
16
|
-
/* Strict Type-Checking Options */
|
|
17
|
-
"strict": true,
|
|
18
|
-
"noImplicitAny": true,
|
|
19
|
-
"strictNullChecks": true,
|
|
20
|
-
"strictFunctionTypes": true,
|
|
21
|
-
"strictBindCallApply": true,
|
|
22
|
-
"strictPropertyInitialization": true,
|
|
23
|
-
"noImplicitThis": true,
|
|
24
|
-
"alwaysStrict": true,
|
|
25
|
-
|
|
26
|
-
/* Additional Checks */
|
|
27
|
-
"noUnusedLocals": true,
|
|
28
|
-
"noUnusedParameters": true,
|
|
29
|
-
"noImplicitReturns": true,
|
|
30
|
-
"noFallthroughCasesInSwitch": true,
|
|
31
|
-
"skipLibCheck": true,
|
|
32
|
-
// "noUncheckedIndexedAccess": true, // FIXME
|
|
33
|
-
|
|
34
|
-
/* Module Resolution Options */
|
|
35
|
-
"moduleResolution": "node",
|
|
36
|
-
"esModuleInterop": true,
|
|
37
|
-
|
|
38
|
-
/* Experimental Options */
|
|
39
|
-
"experimentalDecorators": true,
|
|
40
|
-
"emitDecoratorMetadata": true,
|
|
41
|
-
|
|
42
|
-
/* Advanced Options */
|
|
43
|
-
"forceConsistentCasingInFileNames": true,
|
|
44
|
-
"noErrorTruncation": true
|
|
45
|
-
},
|
|
46
|
-
"exclude": [
|
|
47
|
-
"node_modules",
|
|
48
|
-
"dist",
|
|
49
|
-
"src/**/*.test.ts",
|
|
50
|
-
"src/**/*.test-hold.ts",
|
|
51
|
-
"src/**/*.ignore.ts",
|
|
52
|
-
"wasted_src/**",
|
|
53
|
-
"_templates/**",
|
|
54
|
-
"**/__mocks__/**"
|
|
55
|
-
]
|
|
56
|
-
}
|
package/tsup.config.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// tsup.config.js
|
|
2
|
-
import { defineConfig } from "tsup";
|
|
3
|
-
import { ImportToRequirePlugin } from "./import-to-require";
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
entry: [
|
|
7
|
-
"src/index.ts",
|
|
8
|
-
"src/bin/cli.ts",
|
|
9
|
-
"src/bin/cli-wrapper.ts",
|
|
10
|
-
"src/database/drivers/knex/base-model.ts",
|
|
11
|
-
"src/database/drivers/kysely/base-model.ts",
|
|
12
|
-
],
|
|
13
|
-
dts: true,
|
|
14
|
-
format: [
|
|
15
|
-
"cjs",
|
|
16
|
-
// "esm"
|
|
17
|
-
],
|
|
18
|
-
target: "esnext",
|
|
19
|
-
clean: true,
|
|
20
|
-
sourcemap: true,
|
|
21
|
-
shims: true,
|
|
22
|
-
platform: "node",
|
|
23
|
-
splitting: true,
|
|
24
|
-
esbuildPlugins: [ImportToRequirePlugin],
|
|
25
|
-
external: [
|
|
26
|
-
"chalk",
|
|
27
|
-
"dotenv",
|
|
28
|
-
"fast-deep-equal",
|
|
29
|
-
"fastify",
|
|
30
|
-
"glob",
|
|
31
|
-
"inflection",
|
|
32
|
-
"knex",
|
|
33
|
-
"lodash",
|
|
34
|
-
"luxon",
|
|
35
|
-
"mysql2",
|
|
36
|
-
"node-sql-parser",
|
|
37
|
-
"prompts",
|
|
38
|
-
"qs",
|
|
39
|
-
"tsicli",
|
|
40
|
-
"uuid",
|
|
41
|
-
"zod",
|
|
42
|
-
"prettier",
|
|
43
|
-
"source-map-support",
|
|
44
|
-
"tsup",
|
|
45
|
-
"typescript",
|
|
46
|
-
],
|
|
47
|
-
});
|